Skip to content
Open
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions contract-tests/src/contracts/drand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Drand precompile address: 0x80e = 2062
export const IDRAND_ADDRESS = "0x000000000000000000000000000000000000080e";

export const IDrandABI = [
{
inputs: [
{
internalType: "uint64",
name: "round",
type: "uint64",
},
],
name: "getRandomness",
outputs: [
{
internalType: "bytes32",
name: "",
type: "bytes32",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "getLastStoredRound",
outputs: [
{
internalType: "uint64",
name: "",
type: "uint64",
},
],
stateMutability: "view",
type: "function",
},
] as const;
98 changes: 98 additions & 0 deletions contract-tests/test/drand.precompile.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import * as assert from "assert";

import { getDevnetApi } from "../src/substrate";
import { getPublicClient } from "../src/utils";
import { ETH_LOCAL_URL } from "../src/config";
import { devnet } from "@polkadot-api/descriptors";
import { PublicClient } from "viem";
import { TypedApi } from "polkadot-api";
import { toViemAddress } from "../src/address-utils";
import { IDrandABI, IDRAND_ADDRESS } from "../src/contracts/drand";

describe("Test Drand Precompile", () => {
let publicClient: PublicClient;
let api: TypedApi<typeof devnet>;

before(async () => {
publicClient = await getPublicClient(ETH_LOCAL_URL);
api = await getDevnetApi();
});

describe("Drand Randomness Functions", () => {
it("getLastStoredRound returns a value", async () => {
const lastRound = await publicClient.readContract({
abi: IDrandABI,
address: toViemAddress(IDRAND_ADDRESS),
functionName: "getLastStoredRound",
args: [],
});

const lastRoundFromApi = await api.query.Drand.LastStoredRound.getValue({ at: "best" });

assert.ok(lastRound !== undefined, "getLastStoredRound should return a value");
assert.strictEqual(
typeof lastRound,
"bigint",
"getLastStoredRound should return a bigint"
);
assert.ok(lastRound === lastRoundFromApi, "Last stored round should match the value from the API");
});

it("getRandomness returns bytes32 for a round", async () => {
const lastRound = await publicClient.readContract({
abi: IDrandABI,
address: toViemAddress(IDRAND_ADDRESS),
functionName: "getLastStoredRound",
args: [],
});

const randomness = await publicClient.readContract({
abi: IDrandABI,
address: toViemAddress(IDRAND_ADDRESS),
functionName: "getRandomness",
args: [lastRound],
});

const pulseFromApi = await api.query.Drand.Pulses.getValue(lastRound, { at: "best" });
const randomnessFromApi = pulseFromApi?.randomness.asHex();

assert.ok(randomness !== undefined, "getRandomness should return a value");
assert.strictEqual(
typeof randomness,
"string",
"getRandomness should return a hex string (bytes32)"
);
assert.strictEqual(
randomness.length,
66,
"bytes32 should be 0x + 64 hex chars"
);
assert.strictEqual(
randomness,
randomnessFromApi,
"Randomness should match the value from the API"
);
});

it("getRandomness for non-existent round returns zero bytes", async () => {
// Use a very high round number that will not have a stored pulse
const nonExistentRound = BigInt(999999999);
const randomness = await publicClient.readContract({
abi: IDrandABI,
address: toViemAddress(IDRAND_ADDRESS),
functionName: "getRandomness",
args: [nonExistentRound],
});

console.log("randomness", randomness);

assert.ok(randomness !== undefined, "getRandomness should return a value");
const zeroBytes32 = "0x" + "0".repeat(64);
assert.strictEqual(
randomness.toLowerCase(),
zeroBytes32,
"getRandomness for non-existent round should return zero bytes32"
);
});
});
});
2 changes: 2 additions & 0 deletions pallets/admin-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ pub mod pallet {
AddressMapping,
/// Voting power precompile
VotingPower,
/// Drand randomness precompile
Drand,
}

#[pallet::type_value]
Expand Down
2 changes: 2 additions & 0 deletions precompiles/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ pallet-admin-utils.workspace = true
subtensor-swap-interface.workspace = true
pallet-crowdloan.workspace = true
pallet-shield.workspace = true
pallet-drand.workspace = true

[lints]
workspace = true
Expand All @@ -65,6 +66,7 @@ std = [
"pallet-subtensor-swap/std",
"pallet-subtensor/std",
"pallet-shield/std",
"pallet-drand/std",
"precompile-utils/std",
"scale-info/std",
"sp-core/std",
Expand Down
57 changes: 57 additions & 0 deletions precompiles/src/drand.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use core::marker::PhantomData;

use fp_evm::PrecompileHandle;
use precompile_utils::EvmResult;
use sp_core::H256;

use crate::PrecompileExt;

/// Drand precompile for smart contract access to Drand beacon randomness.
///
/// This precompile allows smart contracts to read verifiable randomness from the
/// Drand Quicknet beacon that is bridged on-chain by the Drand pallet.
pub struct DrandPrecompile<R>(PhantomData<R>);

impl<R> PrecompileExt<R::AccountId> for DrandPrecompile<R>
where
R: frame_system::Config + pallet_drand::Config,
R::AccountId: From<[u8; 32]>,
{
const INDEX: u64 = 2062;
}

#[precompile_utils::precompile]
impl<R> DrandPrecompile<R>
where
R: frame_system::Config + pallet_drand::Config,
R::AccountId: From<[u8; 32]>,
{
/// Get the 32-byte randomness for a specific Drand round.
///
/// Returns the SHA256 hash of the BLS signature for the given round.
/// Returns 32 zero bytes if no pulse exists for the round.
///
/// # Arguments
/// * `round` - The Drand round number (u64)
///
/// # Returns
/// * `bytes32` - The 32-byte randomness, or zeros if round not stored
#[precompile::public("getRandomness(uint64)")]
#[precompile::view]
fn get_randomness(_: &mut impl PrecompileHandle, round: u64) -> EvmResult<H256> {
let randomness = pallet_drand::Pallet::<R>::random_at(round);
Ok(H256::from(randomness))
}

/// Get the last Drand round that has been stored on-chain.
///
/// Returns 0 if no pulses have been stored yet.
///
/// # Returns
/// * `uint64` - The last stored round number
#[precompile::public("getLastStoredRound()")]
#[precompile::view]
fn get_last_stored_round(_: &mut impl PrecompileHandle) -> EvmResult<u64> {
Ok(pallet_drand::LastStoredRound::<R>::get())
}
}
11 changes: 10 additions & 1 deletion precompiles/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub use address_mapping::AddressMappingPrecompile;
pub use alpha::AlphaPrecompile;
pub use balance_transfer::BalanceTransferPrecompile;
pub use crowdloan::CrowdloanPrecompile;
pub use drand::DrandPrecompile;
pub use ed25519::Ed25519Verify;
pub use extensions::PrecompileExt;
pub use leasing::LeasingPrecompile;
Expand All @@ -48,6 +49,7 @@ mod address_mapping;
mod alpha;
mod balance_transfer;
mod crowdloan;
mod drand;
mod ed25519;
mod extensions;
mod leasing;
Expand Down Expand Up @@ -75,6 +77,7 @@ where
+ pallet_crowdloan::Config
+ pallet_shield::Config
+ pallet_subtensor_proxy::Config
+ pallet_drand::Config
+ Send
+ Sync
+ scale_info::TypeInfo,
Expand Down Expand Up @@ -112,6 +115,7 @@ where
+ pallet_crowdloan::Config
+ pallet_shield::Config
+ pallet_subtensor_proxy::Config
+ pallet_drand::Config
+ Send
+ Sync
+ scale_info::TypeInfo,
Expand All @@ -136,7 +140,7 @@ where
Self(Default::default())
}

pub fn used_addresses() -> [H160; 27] {
pub fn used_addresses() -> [H160; 28] {
[
hash(1),
hash(2),
Expand Down Expand Up @@ -165,6 +169,7 @@ where
hash(VotingPowerPrecompile::<R>::INDEX),
hash(ProxyPrecompile::<R>::INDEX),
hash(AddressMappingPrecompile::<R>::INDEX),
hash(DrandPrecompile::<R>::INDEX),
]
}
}
Expand All @@ -180,6 +185,7 @@ where
+ pallet_crowdloan::Config
+ pallet_shield::Config
+ pallet_subtensor_proxy::Config
+ pallet_drand::Config
+ Send
+ Sync
+ scale_info::TypeInfo,
Expand Down Expand Up @@ -273,6 +279,9 @@ where
PrecompileEnum::AddressMapping,
)
}
a if a == hash(DrandPrecompile::<R>::INDEX) => {
DrandPrecompile::<R>::try_execute::<R>(handle, PrecompileEnum::Drand)
}
_ => None,
}
}
Expand Down
2 changes: 1 addition & 1 deletion runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
// `spec_version`, and `authoring_version` are the same between Wasm and native.
// This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use
// the compatible custom types.
spec_version: 389,
spec_version: 390,
impl_version: 1,
apis: RUNTIME_API_VERSIONS,
transaction_version: 1,
Expand Down
Loading
Loading