diff --git a/Cargo.lock b/Cargo.lock index caa4f5a194..f0b128cc56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18191,6 +18191,7 @@ dependencies = [ "pallet-admin-utils", "pallet-balances", "pallet-crowdloan", + "pallet-drand", "pallet-evm", "pallet-evm-precompile-bn128", "pallet-evm-precompile-dispatch", diff --git a/contract-tests/src/contracts/drand.ts b/contract-tests/src/contracts/drand.ts new file mode 100644 index 0000000000..32a0c5c9c0 --- /dev/null +++ b/contract-tests/src/contracts/drand.ts @@ -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; diff --git a/contract-tests/test/drand.precompile.test.ts b/contract-tests/test/drand.precompile.test.ts new file mode 100644 index 0000000000..76612012f7 --- /dev/null +++ b/contract-tests/test/drand.precompile.test.ts @@ -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; + + 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" + ); + }); + }); +}); diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index 7f882fcf9c..1c078ae857 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -147,6 +147,8 @@ pub mod pallet { AddressMapping, /// Voting power precompile VotingPower, + /// Drand randomness precompile + Drand, } #[pallet::type_value] diff --git a/precompiles/Cargo.toml b/precompiles/Cargo.toml index be1824cf91..8bf412c503 100644 --- a/precompiles/Cargo.toml +++ b/precompiles/Cargo.toml @@ -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 @@ -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", diff --git a/precompiles/src/drand.rs b/precompiles/src/drand.rs new file mode 100644 index 0000000000..3a1449afba --- /dev/null +++ b/precompiles/src/drand.rs @@ -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(PhantomData); + +impl PrecompileExt for DrandPrecompile +where + R: frame_system::Config + pallet_drand::Config, + R::AccountId: From<[u8; 32]>, +{ + const INDEX: u64 = 2062; +} + +#[precompile_utils::precompile] +impl DrandPrecompile +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 { + let randomness = pallet_drand::Pallet::::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 { + Ok(pallet_drand::LastStoredRound::::get()) + } +} diff --git a/precompiles/src/lib.rs b/precompiles/src/lib.rs index a824ac39d4..2a4c4e42f3 100644 --- a/precompiles/src/lib.rs +++ b/precompiles/src/lib.rs @@ -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; @@ -48,6 +49,7 @@ mod address_mapping; mod alpha; mod balance_transfer; mod crowdloan; +mod drand; mod ed25519; mod extensions; mod leasing; @@ -75,6 +77,7 @@ where + pallet_crowdloan::Config + pallet_shield::Config + pallet_subtensor_proxy::Config + + pallet_drand::Config + Send + Sync + scale_info::TypeInfo, @@ -112,6 +115,7 @@ where + pallet_crowdloan::Config + pallet_shield::Config + pallet_subtensor_proxy::Config + + pallet_drand::Config + Send + Sync + scale_info::TypeInfo, @@ -136,7 +140,7 @@ where Self(Default::default()) } - pub fn used_addresses() -> [H160; 27] { + pub fn used_addresses() -> [H160; 28] { [ hash(1), hash(2), @@ -165,6 +169,7 @@ where hash(VotingPowerPrecompile::::INDEX), hash(ProxyPrecompile::::INDEX), hash(AddressMappingPrecompile::::INDEX), + hash(DrandPrecompile::::INDEX), ] } } @@ -180,6 +185,7 @@ where + pallet_crowdloan::Config + pallet_shield::Config + pallet_subtensor_proxy::Config + + pallet_drand::Config + Send + Sync + scale_info::TypeInfo, @@ -273,6 +279,9 @@ where PrecompileEnum::AddressMapping, ) } + a if a == hash(DrandPrecompile::::INDEX) => { + DrandPrecompile::::try_execute::(handle, PrecompileEnum::Drand) + } _ => None, } } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index e554ce0b25..e6626e0121 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -267,7 +267,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: 387, + spec_version: 388, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, diff --git a/runtime/tests/precompiles.rs b/runtime/tests/precompiles.rs index 815d055ab7..a9fc7a222f 100644 --- a/runtime/tests/precompiles.rs +++ b/runtime/tests/precompiles.rs @@ -5,13 +5,16 @@ use core::iter::IntoIterator; use std::collections::BTreeSet; use fp_evm::{Context, ExitError, PrecompileFailure, PrecompileResult}; +use frame_support::BoundedVec; use node_subtensor_runtime::{BuildStorage, Runtime, RuntimeGenesisConfig, System}; +use pallet_drand::{LastStoredRound, Pulses, types::Pulse}; use pallet_evm::{AddressMapping, BalanceConverter, PrecompileSet}; use precompile_utils::testing::{MockHandle, PrecompileTesterExt}; use sp_core::{H160, H256, U256}; use sp_runtime::traits::Hash; use subtensor_precompiles::{ - AddressMappingPrecompile, BalanceTransferPrecompile, PrecompileExt, Precompiles, + AddressMappingPrecompile, BalanceTransferPrecompile, DrandPrecompile, PrecompileExt, + Precompiles, }; type AccountId = ::AccountId; @@ -217,3 +220,99 @@ mod balance_transfer { }); } } + +mod drand { + use super::*; + + fn get_last_stored_round_call_data() -> Vec { + let selector = sp_io::hashing::keccak_256(b"getLastStoredRound()"); + selector[..4].to_vec() + } + + fn get_randomness_call_data(round: u64) -> Vec { + let selector = sp_io::hashing::keccak_256(b"getRandomness(uint64)"); + let mut input = Vec::with_capacity(4 + 32); + input.extend_from_slice(&selector[..4]); + input.extend_from_slice(&[0u8; 24]); + input.extend_from_slice(&round.to_be_bytes()); + input + } + + #[test] + fn drand_precompile_get_last_stored_round_returns_value() { + new_test_ext().execute_with(|| { + let round = 1000; + LastStoredRound::::put(round); + let precompiles = Precompiles::::new(); + let caller = addr_from_index(1); + let precompile_addr = addr_from_index(DrandPrecompile::::INDEX); + let input = get_last_stored_round_call_data(); + + let result = + execute_precompile(&precompiles, precompile_addr, caller, input, U256::zero()); + let precompile_result = + result.expect("expected precompile call to be routed to drand precompile"); + let output = precompile_result + .expect("expected successful getLastStoredRound call") + .output; + + assert_eq!( + output.len(), + 32, + "getLastStoredRound should return 32 bytes (uint64 ABI)" + ); + #[allow(clippy::indexing_slicing)] + let output_u64 = u64::from_be_bytes(output[24..32].try_into().unwrap()); + assert_eq!(output_u64, round); + }); + } + + #[test] + fn drand_precompile_get_randomness_returns_bytes32() { + new_test_ext().execute_with(|| { + let round = 1000; + let value = 1u8; + Pulses::::insert( + round, + Pulse { + round: 1000, + randomness: BoundedVec::truncate_from(vec![value; 32]), + signature: BoundedVec::truncate_from(vec![0u8; 96]), + }, + ); + let precompiles = Precompiles::::new(); + let caller = addr_from_index(1); + let precompile_addr = addr_from_index(DrandPrecompile::::INDEX); + let input = get_randomness_call_data(round); + + let result = + execute_precompile(&precompiles, precompile_addr, caller, input, U256::zero()); + let precompile_result = + result.expect("expected precompile call to be routed to drand precompile"); + let output = precompile_result + .expect("expected successful getRandomness call") + .output; + + assert_eq!( + output.len(), + 32, + "getRandomness should return 32 bytes (bytes32)" + ); + assert!( + output.iter().all(|&b| b == value), + "getRandomness for round 1000 should return the inserted randomness (32 bytes of 1s)" + ); + + let non_existent_round = 999_999_999u64; + let input = get_randomness_call_data(non_existent_round); + let result = + execute_precompile(&precompiles, precompile_addr, caller, input, U256::zero()); + let precompile_result = + result.expect("expected precompile call to be routed to drand precompile"); + let output = precompile_result + .expect("expected successful getRandomness call") + .output; + assert!(output.iter().all(|&b| b == 0), "getRandomness for non-existent round should return 32 bytes of 0s"); + }); + } +}