From 6d1668d9d5444d79f3f7c0eea5eae0a583f14ab7 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Wed, 26 Feb 2025 13:03:32 -0800 Subject: [PATCH 01/28] initial tlock commitments impl --- Cargo.lock | 5 ++ pallets/commitments/Cargo.toml | 7 +++ pallets/commitments/src/lib.rs | 88 ++++++++++++++++++++++++++++++-- pallets/commitments/src/mock.rs | 1 + pallets/commitments/src/types.rs | 58 +++++++++++++++++++++ pallets/drand/src/lib.rs | 2 +- 6 files changed, 157 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 70dcd3d37..167febc2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6087,11 +6087,15 @@ dependencies = [ name = "pallet-commitments" version = "4.0.0-dev" dependencies = [ + "ark-bls12-381", + "ark-serialize", "enumflags2", "frame-benchmarking", "frame-support", "frame-system", "pallet-balances", + "pallet-drand", + "pallet-subtensor", "parity-scale-codec", "scale-info", "sp-core", @@ -6099,6 +6103,7 @@ dependencies = [ "sp-runtime", "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk.git?tag=polkadot-stable2409)", "subtensor-macros", + "tle", ] [[package]] diff --git a/pallets/commitments/Cargo.toml b/pallets/commitments/Cargo.toml index 7fb22aa1f..77a2397e8 100644 --- a/pallets/commitments/Cargo.toml +++ b/pallets/commitments/Cargo.toml @@ -29,6 +29,13 @@ sp-runtime = { workspace = true } sp-std = { workspace = true } enumflags2 = { workspace = true } +pallet-subtensor = { default-features = false, path = "../subtensor" } + +pallet-drand = { path = "../drand", default-features = false } +tle = { workspace = true, default-features = false } +ark-bls12-381 = { workspace = true, default-features = false } +ark-serialize = { workspace = true, default-features = false } + [dev-dependencies] sp-core = { workspace = true } sp-io = { workspace = true } diff --git a/pallets/commitments/src/lib.rs b/pallets/commitments/src/lib.rs index ba11dbe52..1830eb203 100644 --- a/pallets/commitments/src/lib.rs +++ b/pallets/commitments/src/lib.rs @@ -31,7 +31,7 @@ pub mod pallet { // Configure the pallet by specifying the parameters and types on which it depends. #[pallet::config] - pub trait Config: frame_system::Config { + pub trait Config: frame_system::Config + pallet_drand::Config { /// Because this pallet emits events, it depends on the runtime's definition of an event. type RuntimeEvent: From> + IsType<::RuntimeEvent>; @@ -71,6 +71,22 @@ pub mod pallet { /// The account who: T::AccountId, }, + /// A timelock-encrypted commitment was set + TimelockCommitment { + /// The netuid of the commitment + netuid: u16, + /// The account + who: T::AccountId, + /// The drand round to reveal + reveal_round: u64, + }, + /// A timelock-encrypted commitment was auto-revealed + CommitmentRevealed { + /// The netuid of the commitment + netuid: u16, + /// The account + who: T::AccountId, + }, } #[pallet::error] @@ -117,13 +133,24 @@ pub mod pallet { BlockNumberFor, OptionQuery, >; + #[pallet::storage] + #[pallet::getter(fn timelock_commitment_of)] + pub(super) type TimelockCommitmentOf = StorageDoubleMap< + _, + Identity, + u16, // netuid + Twox64Concat, + T::AccountId, + TimelockCommitment>, + OptionQuery, + >; #[pallet::call] impl Pallet { /// Set the commitment for a given netuid #[pallet::call_index(0)] #[pallet::weight(( - T::WeightInfo::set_commitment(), + ::WeightInfo::set_commitment(), DispatchClass::Operational, Pays::No ))] @@ -187,7 +214,7 @@ pub mod pallet { /// Sudo-set the commitment rate limit #[pallet::call_index(1)] #[pallet::weight(( - T::WeightInfo::set_rate_limit(), + ::WeightInfo::set_rate_limit(), DispatchClass::Operational, Pays::No ))] @@ -196,6 +223,61 @@ pub mod pallet { RateLimit::::set(rate_limit_blocks.into()); Ok(()) } + /// Set a timelock-encrypted commitment for a given netuid + #[pallet::call_index(2)] + #[pallet::weight(( + ::WeightInfo::set_commitment(), + DispatchClass::Operational, + Pays::No + ))] + pub fn set_timelock_commitment( + origin: OriginFor, + netuid: u16, + encrypted_commitment: BoundedVec>, + reveal_round: u64, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + ensure!( + T::CanCommit::can_commit(netuid, &who), + Error::::AccountNotAllowedCommit + ); + + let cur_block = >::block_number(); + if let Some(last_commit) = >::get(netuid, &who) { + ensure!( + cur_block >= last_commit.saturating_add(RateLimit::::get()), + Error::::CommitmentSetRateLimitExceeded + ); + } + + // Calculate reveal block + let last_drand_round = pallet_drand::LastStoredRound::::get(); + let blocks_per_round = 12_u64.checked_div(3).unwrap_or(0); // 4 blocks per round (12s blocktime / 3s round) + let rounds_since_last = reveal_round.saturating_sub(last_drand_round); + let blocks_to_reveal = rounds_since_last.saturating_mul(blocks_per_round); + let blocks_to_reveal: BlockNumberFor = blocks_to_reveal.try_into().map_err(|_| "Block number conversion failed")?; + let reveal_block = cur_block.saturating_add(blocks_to_reveal); + + // Construct the TimelockCommitment + let timelock_commitment = TimelockCommitment { + encrypted_commitment, + reveal_round, + reveal_block, + }; + + // Store the timelock commitment + >::insert(netuid, &who, timelock_commitment.clone()); + >::insert(netuid, &who, cur_block); + + // Emit event with hash computed on-demand + Self::deposit_event(Event::TimelockCommitment { + netuid, + who, + reveal_round, + }); + + Ok(()) + } } } diff --git a/pallets/commitments/src/mock.rs b/pallets/commitments/src/mock.rs index 8866e1c0d..2d28a44c0 100644 --- a/pallets/commitments/src/mock.rs +++ b/pallets/commitments/src/mock.rs @@ -14,6 +14,7 @@ frame_support::construct_runtime!( { System: frame_system = 1, Commitments: pallet_commitments = 2, + SubtensorModule: pallet_subtensor = 3, } ); diff --git a/pallets/commitments/src/types.rs b/pallets/commitments/src/types.rs index bc0531ece..5f5ada908 100644 --- a/pallets/commitments/src/types.rs +++ b/pallets/commitments/src/types.rs @@ -31,6 +31,10 @@ use sp_runtime::{ use sp_std::{fmt::Debug, iter::once, prelude::*}; use subtensor_macros::freeze_struct; +use crate::Config; +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +use tle::curves::drand::TinyBLS381; + /// Either underlying data blob if it is at most 32 bytes, or a hash of it. If the data is greater /// than 32-bytes then it will be truncated when encoding. /// @@ -295,6 +299,60 @@ pub struct CommitmentInfo> { pub fields: BoundedVec, } +/// Maximum size of the serialized timelock commitment in bytes +pub const MAX_TIMELOCK_COMMITMENT_SIZE_BYTES: u32 = 1024; + +/// Represents a timelock-encrypted commitment with reveal metadata +#[derive(Clone, Eq, PartialEq, Encode, Decode, TypeInfo, Debug)] +pub struct TimelockCommitment { + /// The timelock-encrypted commitment data + pub encrypted_commitment: BoundedVec>, + /// The drand round number when this commitment can be revealed + pub reveal_round: u64, + /// The block number when the commitment should be revealed + pub reveal_block: BlockNumber, +} +/// Represents a revealed commitment after decryption +#[derive(Clone, Eq, PartialEq, Encode, Decode, TypeInfo, Debug)] +pub struct RevealedCommitment, BlockNumber> { + /// The decrypted commitment info + pub info: CommitmentInfo, + /// The block it was revealed + pub revealed_block: BlockNumber, + /// The deposit held for the commitment + pub deposit: Balance, +} + +impl> TimelockCommitment { + /// Create a new TimelockCommitment from a TLECiphertext and reveal round + pub fn from_tle_ciphertext( + ciphertext: tle::tlock::TLECiphertext, + reveal_round: u64, + reveal_block: BlockNumber, + ) -> Result { + let mut encrypted_data = Vec::new(); + ciphertext + .serialize_compressed(&mut encrypted_data) + .map_err(|_| "Failed to serialize TLECiphertext")?; + + let bounded_encrypted = BoundedVec::try_from(encrypted_data) + .map_err(|_| "Encrypted commitment exceeds max size")?; + + Ok(TimelockCommitment { + encrypted_commitment: bounded_encrypted, + reveal_round, + reveal_block, + }) + } + + /// Attempt to deserialize the encrypted commitment back into a TLECiphertext + pub fn to_tle_ciphertext(&self) -> Result, &'static str> { + let mut reader = &self.encrypted_commitment[..]; + tle::tlock::TLECiphertext::::deserialize_compressed(&mut reader) + .map_err(|_| "Failed to deserialize TLECiphertext") + } +} + /// Information concerning the identity of the controller of an account. /// /// NOTE: This is stored separately primarily to facilitate the addition of extra fields in a diff --git a/pallets/drand/src/lib.rs b/pallets/drand/src/lib.rs index 40bf7ccb9..d9da28821 100644 --- a/pallets/drand/src/lib.rs +++ b/pallets/drand/src/lib.rs @@ -219,7 +219,7 @@ pub mod pallet { pub type Pulses = StorageMap<_, Blake2_128Concat, RoundNumber, Pulse, OptionQuery>; #[pallet::storage] - pub(super) type LastStoredRound = StorageValue<_, RoundNumber, ValueQuery>; + pub type LastStoredRound = StorageValue<_, RoundNumber, ValueQuery>; /// Defines the block when next unsigned transaction will be accepted. /// From b9d69cac2b6b8acc9d5363aab781bd46d5fa073e Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Wed, 26 Feb 2025 13:23:44 -0800 Subject: [PATCH 02/28] Update tests.rs --- pallets/commitments/src/tests.rs | 132 ++++++++++++++++++++++--------- 1 file changed, 95 insertions(+), 37 deletions(-) diff --git a/pallets/commitments/src/tests.rs b/pallets/commitments/src/tests.rs index 15675d8ad..5dcb3ba95 100644 --- a/pallets/commitments/src/tests.rs +++ b/pallets/commitments/src/tests.rs @@ -2,7 +2,7 @@ use crate as pallet_commitments; use frame_support::derive_impl; -use frame_support::traits::ConstU64; +use frame_support::traits::{ConstU32, ConstU64}; use sp_core::H256; use sp_runtime::{ testing::Header, @@ -10,7 +10,8 @@ use sp_runtime::{ }; pub type Block = sp_runtime::generic::Block; -pub type UncheckedExtrinsic = sp_runtime::generic::UncheckedExtrinsic; +pub type UncheckedExtrinsic = + sp_runtime::generic::UncheckedExtrinsic; frame_support::construct_runtime!( pub enum Test @@ -18,39 +19,12 @@ frame_support::construct_runtime!( System: frame_system = 1, Balances: pallet_balances = 2, Commitments: pallet_commitments = 3, + Drand: pallet_drand = 4, } ); -#[allow(dead_code)] pub type AccountId = u64; -// The address format for describing accounts. -#[allow(dead_code)] -pub type Address = AccountId; - -// Balance of an account. -#[allow(dead_code)] -pub type Balance = u64; - -// An index to a block. -#[allow(dead_code)] -pub type BlockNumber = u64; - -#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] -impl pallet_balances::Config for Test { - type MaxLocks = (); - type MaxReserves = (); - type ReserveIdentifier = [u8; 8]; - type Balance = u64; - type RuntimeEvent = RuntimeEvent; - type DustRemoval = (); - type ExistentialDeposit = ConstU64<1>; - type AccountStore = System; - type WeightInfo = (); - type FreezeIdentifier = (); - type MaxFreezes = (); -} - #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] impl frame_system::Config for Test { type BaseCallFilter = frame_support::traits::Everything; @@ -73,23 +47,107 @@ impl frame_system::Config for Test { type SystemWeightInfo = (); type SS58Prefix = ConstU16<42>; type OnSetCode = (); - type MaxConsumers = frame_support::traits::ConstU32<16>; + type MaxConsumers = ConstU32<16>; type Block = Block; - type Nonce = u64; + type Nonce = u32; +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] +impl pallet_balances::Config for Test { + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type Balance = u64; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ConstU64<1>; + type AccountStore = System; + type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); } impl pallet_commitments::Config for Test { type RuntimeEvent = RuntimeEvent; type Currency = Balances; type WeightInfo = (); - type MaxFields = frame_support::traits::ConstU32<16>; + type MaxFields = ConstU32<16>; type CanCommit = (); - type FieldDeposit = frame_support::traits::ConstU64<0>; - type InitialDeposit = frame_support::traits::ConstU64<0>; - type DefaultRateLimit = frame_support::traits::ConstU64<0>; + type FieldDeposit = ConstU64<0>; + type InitialDeposit = ConstU64<0>; + type DefaultRateLimit = ConstU64<0>; +} + +impl pallet_drand::Config for Test { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = pallet_drand::weights::SubstrateWeight; + type AuthorityId = test_crypto::TestAuthId; + type Verifier = pallet_drand::verifier::QuicknetVerifier; + type UnsignedPriority = ConstU64<{ 1 << 20 }>; + type HttpFetchTimeout = ConstU64<1_000>; +} + +pub mod test_crypto { + use sp_core::sr25519::{Public as Sr25519Public, Signature as Sr25519Signature}; + use sp_runtime::{ + app_crypto::{app_crypto, sr25519}, + traits::IdentifyAccount, + }; + + pub const KEY_TYPE: sp_runtime::KeyTypeId = sp_runtime::KeyTypeId(*b"test"); + + app_crypto!(sr25519, KEY_TYPE); + + pub struct TestAuthId; + + impl frame_system::offchain::AppCrypto for TestAuthId { + type RuntimeAppPublic = Public; + type GenericSignature = Sr25519Signature; + type GenericPublic = Sr25519Public; + } + + impl IdentifyAccount for Public { + type AccountId = u64; + + fn into_account(self) -> u64 { + let mut bytes = [0u8; 32]; + bytes.copy_from_slice(self.as_ref()); + u64::from_le_bytes(bytes[..8].try_into().unwrap()) + } + } +} + +impl frame_system::offchain::SigningTypes for Test { + type Public = test_crypto::Public; + type Signature = test_crypto::Signature; +} + +impl frame_system::offchain::CreateSignedTransaction> for Test { + fn create_transaction>( + call: RuntimeCall, + _public: Self::Public, + account: Self::AccountId, + _nonce: u32, + ) -> Option<( + RuntimeCall, + ::SignaturePayload, + )> { + // Create a dummy sr25519 signature from a raw byte array + let dummy_raw = [0u8; 64]; + let dummy_signature = sp_core::sr25519::Signature::from(dummy_raw); + let signature = test_crypto::Signature::from(dummy_signature); + Some((call, (account, signature, ()))) + } +} + +impl frame_system::offchain::SendTransactionTypes for Test +where + RuntimeCall: From, +{ + type Extrinsic = UncheckedExtrinsic; + type OverarchingCall = RuntimeCall; } -// // Build genesis storage according to the mock runtime. // pub fn new_test_ext() -> sp_io::TestExternalities { // let t = frame_system::GenesisConfig::::default() // .build_storage() From 6938c1ae07a426881c50439e4fab0eb28c43b1a8 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Wed, 26 Feb 2025 13:23:50 -0800 Subject: [PATCH 03/28] fmt --- pallets/commitments/src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pallets/commitments/src/lib.rs b/pallets/commitments/src/lib.rs index 1830eb203..244baa93b 100644 --- a/pallets/commitments/src/lib.rs +++ b/pallets/commitments/src/lib.rs @@ -81,7 +81,7 @@ pub mod pallet { reveal_round: u64, }, /// A timelock-encrypted commitment was auto-revealed - CommitmentRevealed { + CommitmentRevealed { /// The netuid of the commitment netuid: u16, /// The account @@ -255,7 +255,9 @@ pub mod pallet { let blocks_per_round = 12_u64.checked_div(3).unwrap_or(0); // 4 blocks per round (12s blocktime / 3s round) let rounds_since_last = reveal_round.saturating_sub(last_drand_round); let blocks_to_reveal = rounds_since_last.saturating_mul(blocks_per_round); - let blocks_to_reveal: BlockNumberFor = blocks_to_reveal.try_into().map_err(|_| "Block number conversion failed")?; + let blocks_to_reveal: BlockNumberFor = blocks_to_reveal + .try_into() + .map_err(|_| "Block number conversion failed")?; let reveal_block = cur_block.saturating_add(blocks_to_reveal); // Construct the TimelockCommitment From 3f3c0e0507fe508a157013bea0d913c78695080b Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Thu, 27 Feb 2025 15:51:27 -0800 Subject: [PATCH 04/28] refactor timelock commitments --- pallets/commitments/src/lib.rs | 160 ++++++++++++++----------------- pallets/commitments/src/tests.rs | 25 ++++- pallets/commitments/src/types.rs | 129 ++++++++++++++++++------- runtime/src/lib.rs | 10 +- 4 files changed, 200 insertions(+), 124 deletions(-) diff --git a/pallets/commitments/src/lib.rs b/pallets/commitments/src/lib.rs index 244baa93b..b30c17cb1 100644 --- a/pallets/commitments/src/lib.rs +++ b/pallets/commitments/src/lib.rs @@ -46,7 +46,7 @@ pub mod pallet { /// The maximum number of additional fields that can be added to a commitment #[pallet::constant] - type MaxFields: Get; + type MaxFields: Get + TypeInfo + 'static; /// The amount held on deposit for a registered identity #[pallet::constant] @@ -141,7 +141,7 @@ pub mod pallet { u16, // netuid Twox64Concat, T::AccountId, - TimelockCommitment>, + CommitmentState, T::MaxFields, BlockNumberFor>, OptionQuery, >; @@ -179,36 +179,81 @@ pub mod pallet { ); } - let fd = >::from(extra_fields).saturating_mul(T::FieldDeposit::get()); - let mut id = match >::get(netuid, &who) { - Some(mut id) => { - id.info = *info; - id.block = cur_block; - id + let is_timelock = info.fields.iter().any(|data| data.is_timelock_encrypted()); + + if !is_timelock { + let fd = >::from(extra_fields).saturating_mul(T::FieldDeposit::get()); + let mut id = match >::get(netuid, &who) { + Some(mut id) => { + id.info = *info; + id.block = cur_block; + id + } + None => Registration { + info: *info, + block: cur_block, + deposit: Zero::zero(), + }, + }; + + let old_deposit = id.deposit; + id.deposit = T::InitialDeposit::get().saturating_add(fd); + if id.deposit > old_deposit { + T::Currency::reserve(&who, id.deposit.saturating_sub(old_deposit))?; + } + if old_deposit > id.deposit { + let err_amount = + T::Currency::unreserve(&who, old_deposit.saturating_sub(id.deposit)); + debug_assert!(err_amount.is_zero()); } - None => Registration { - info: *info, - block: cur_block, - deposit: Zero::zero(), - }, - }; - - let old_deposit = id.deposit; - id.deposit = T::InitialDeposit::get().saturating_add(fd); - if id.deposit > old_deposit { - T::Currency::reserve(&who, id.deposit.saturating_sub(old_deposit))?; - } - if old_deposit > id.deposit { - let err_amount = - T::Currency::unreserve(&who, old_deposit.saturating_sub(id.deposit)); - debug_assert!(err_amount.is_zero()); - } - >::insert(netuid, &who, id); - >::insert(netuid, &who, cur_block); - Self::deposit_event(Event::Commitment { netuid, who }); + >::insert(netuid, &who, id); + >::insert(netuid, &who, cur_block); + Self::deposit_event(Event::Commitment { netuid, who }); - Ok(()) + Ok(()) + } else { + ensure!( + info.fields.len() == 1, + Error::::TooManyFieldsInCommitmentInfo, + ); + + if let Data::TimelockEncrypted { + encrypted, + reveal_round, + } = &info.fields[0] + { + // Calculate reveal block + let last_drand_round = pallet_drand::LastStoredRound::::get(); + let blocks_per_round = 12_u64.checked_div(3).unwrap_or(0); + let rounds_since_last = reveal_round.saturating_sub(last_drand_round); + let blocks_to_reveal = rounds_since_last.saturating_mul(blocks_per_round); + let blocks_to_reveal: BlockNumberFor = blocks_to_reveal + .try_into() + .map_err(|_| "Block number conversion failed")?; + let reveal_block = cur_block.saturating_add(blocks_to_reveal); + + // Construct CommitmentState for timelock commitment + let commitment_state = CommitmentState { + encrypted_commitment: encrypted.clone(), + reveal_round: *reveal_round, + reveal_block, + revealed: None, + }; + + // Store in TimelockCommitmentOf + >::insert(netuid, &who, commitment_state); + >::insert(netuid, &who, cur_block); + + // Emit timelock-specific event + Self::deposit_event(Event::TimelockCommitment { + netuid, + who, + reveal_round: *reveal_round, + }); + } + Ok(()) + } } /// Sudo-set the commitment rate limit @@ -223,63 +268,6 @@ pub mod pallet { RateLimit::::set(rate_limit_blocks.into()); Ok(()) } - /// Set a timelock-encrypted commitment for a given netuid - #[pallet::call_index(2)] - #[pallet::weight(( - ::WeightInfo::set_commitment(), - DispatchClass::Operational, - Pays::No - ))] - pub fn set_timelock_commitment( - origin: OriginFor, - netuid: u16, - encrypted_commitment: BoundedVec>, - reveal_round: u64, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - ensure!( - T::CanCommit::can_commit(netuid, &who), - Error::::AccountNotAllowedCommit - ); - - let cur_block = >::block_number(); - if let Some(last_commit) = >::get(netuid, &who) { - ensure!( - cur_block >= last_commit.saturating_add(RateLimit::::get()), - Error::::CommitmentSetRateLimitExceeded - ); - } - - // Calculate reveal block - let last_drand_round = pallet_drand::LastStoredRound::::get(); - let blocks_per_round = 12_u64.checked_div(3).unwrap_or(0); // 4 blocks per round (12s blocktime / 3s round) - let rounds_since_last = reveal_round.saturating_sub(last_drand_round); - let blocks_to_reveal = rounds_since_last.saturating_mul(blocks_per_round); - let blocks_to_reveal: BlockNumberFor = blocks_to_reveal - .try_into() - .map_err(|_| "Block number conversion failed")?; - let reveal_block = cur_block.saturating_add(blocks_to_reveal); - - // Construct the TimelockCommitment - let timelock_commitment = TimelockCommitment { - encrypted_commitment, - reveal_round, - reveal_block, - }; - - // Store the timelock commitment - >::insert(netuid, &who, timelock_commitment.clone()); - >::insert(netuid, &who, cur_block); - - // Emit event with hash computed on-demand - Self::deposit_event(Event::TimelockCommitment { - netuid, - who, - reveal_round, - }); - - Ok(()) - } } } diff --git a/pallets/commitments/src/tests.rs b/pallets/commitments/src/tests.rs index 5dcb3ba95..074227111 100644 --- a/pallets/commitments/src/tests.rs +++ b/pallets/commitments/src/tests.rs @@ -1,8 +1,11 @@ #![allow(non_camel_case_types)] use crate as pallet_commitments; -use frame_support::derive_impl; -use frame_support::traits::{ConstU32, ConstU64}; +use frame_support::{ + derive_impl, + pallet_prelude::{Get, TypeInfo}, + traits::{ConstU32, ConstU64}, +}; use sp_core::H256; use sp_runtime::{ testing::Header, @@ -67,11 +70,27 @@ impl pallet_balances::Config for Test { type MaxFreezes = (); } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct TestMaxFields; +impl Get for TestMaxFields { + fn get() -> u32 { + 16 + } +} +impl TypeInfo for TestMaxFields { + type Identity = Self; + fn type_info() -> scale_info::Type { + scale_info::Type::builder() + .path(scale_info::Path::new("TestMaxFields", module_path!())) + .composite(scale_info::build::Fields::unit()) + } +} + impl pallet_commitments::Config for Test { type RuntimeEvent = RuntimeEvent; type Currency = Balances; type WeightInfo = (); - type MaxFields = ConstU32<16>; + type MaxFields = TestMaxFields; type CanCommit = (); type FieldDeposit = ConstU64<0>; type InitialDeposit = ConstU64<0>; diff --git a/pallets/commitments/src/types.rs b/pallets/commitments/src/types.rs index 5f5ada908..40b997589 100644 --- a/pallets/commitments/src/types.rs +++ b/pallets/commitments/src/types.rs @@ -57,12 +57,22 @@ pub enum Data { /// Only the SHA3-256 hash of the data is stored. The preimage of the hash may be retrieved /// through some hash-lookup service. ShaThree256([u8; 32]), + /// A timelock-encrypted commitment with a reveal round. + TimelockEncrypted { + encrypted: BoundedVec>, + reveal_round: u64, + }, } impl Data { pub fn is_none(&self) -> bool { self == &Data::None } + + /// Check if this is a timelock-encrypted commitment. + pub fn is_timelock_encrypted(&self) -> bool { + matches!(self, Data::TimelockEncrypted { .. }) + } } impl Decode for Data { @@ -81,6 +91,15 @@ impl Decode for Data { 131 => Data::Sha256(<[u8; 32]>::decode(input)?), 132 => Data::Keccak256(<[u8; 32]>::decode(input)?), 133 => Data::ShaThree256(<[u8; 32]>::decode(input)?), + 134 => { + let encrypted = + BoundedVec::>::decode(input)?; + let reveal_round = u64::decode(input)?; + Data::TimelockEncrypted { + encrypted, + reveal_round, + } + } _ => return Err(codec::Error::from("invalid leading byte")), }) } @@ -100,6 +119,15 @@ impl Encode for Data { Data::Sha256(h) => once(131).chain(h.iter().cloned()).collect(), Data::Keccak256(h) => once(132).chain(h.iter().cloned()).collect(), Data::ShaThree256(h) => once(133).chain(h.iter().cloned()).collect(), + Data::TimelockEncrypted { + encrypted, + reveal_round, + } => { + let mut r = vec![134]; + r.extend_from_slice(&encrypted.encode()); + r.extend_from_slice(&reveal_round.encode()); + r + } } } } @@ -274,6 +302,17 @@ impl TypeInfo for Data { .variant("ShaThree256", |v| { v.index(133) .fields(Fields::unnamed().field(|f| f.ty::<[u8; 32]>())) + }) + .variant("TimelockEncrypted", |v| { + v.index(134).fields( + Fields::named() + .field(|f| { + f.name("encrypted") + .ty::>>( + ) + }) + .field(|f| f.name("reveal_round").ty::()), + ) }); Type::builder() @@ -302,29 +341,26 @@ pub struct CommitmentInfo> { /// Maximum size of the serialized timelock commitment in bytes pub const MAX_TIMELOCK_COMMITMENT_SIZE_BYTES: u32 = 1024; -/// Represents a timelock-encrypted commitment with reveal metadata +/// Represents a commitment that can be either unrevealed (timelock-encrypted) or revealed. #[derive(Clone, Eq, PartialEq, Encode, Decode, TypeInfo, Debug)] -pub struct TimelockCommitment { - /// The timelock-encrypted commitment data +pub struct CommitmentState, BlockNumber> { pub encrypted_commitment: BoundedVec>, - /// The drand round number when this commitment can be revealed pub reveal_round: u64, - /// The block number when the commitment should be revealed pub reveal_block: BlockNumber, + pub revealed: Option>, } -/// Represents a revealed commitment after decryption + +/// Contains the decrypted data of a revealed commitment. #[derive(Clone, Eq, PartialEq, Encode, Decode, TypeInfo, Debug)] -pub struct RevealedCommitment, BlockNumber> { - /// The decrypted commitment info +pub struct RevealedData, BlockNumber> { pub info: CommitmentInfo, - /// The block it was revealed pub revealed_block: BlockNumber, - /// The deposit held for the commitment pub deposit: Balance, } -impl> TimelockCommitment { - /// Create a new TimelockCommitment from a TLECiphertext and reveal round +impl, BlockNumber: Clone + From> + CommitmentState +{ pub fn from_tle_ciphertext( ciphertext: tle::tlock::TLECiphertext, reveal_round: u64, @@ -334,18 +370,16 @@ impl> TimelockCommitment { ciphertext .serialize_compressed(&mut encrypted_data) .map_err(|_| "Failed to serialize TLECiphertext")?; - let bounded_encrypted = BoundedVec::try_from(encrypted_data) .map_err(|_| "Encrypted commitment exceeds max size")?; - - Ok(TimelockCommitment { + Ok(CommitmentState { encrypted_commitment: bounded_encrypted, reveal_round, reveal_block, + revealed: None, }) } - /// Attempt to deserialize the encrypted commitment back into a TLECiphertext pub fn to_tle_ciphertext(&self) -> Result, &'static str> { let mut reader = &self.encrypted_commitment[..]; tle::tlock::TLECiphertext::::deserialize_compressed(&mut reader) @@ -424,6 +458,7 @@ mod tests { Data::Keccak256(_) => "Keccak256".to_string(), Data::ShaThree256(_) => "ShaThree256".to_string(), Data::Raw(bytes) => format!("Raw{}", bytes.len()), + Data::TimelockEncrypted { .. } => "TimelockEncrypted".to_string(), }; if let scale_info::TypeDef::Variant(variant) = &type_info.type_def { let variant = variant @@ -432,25 +467,45 @@ mod tests { .find(|v| v.name == variant_name) .unwrap_or_else(|| panic!("Expected to find variant {}", variant_name)); - let field_arr_len = variant - .fields - .first() - .and_then(|f| registry.resolve(f.ty.id)) - .map(|ty| { - if let scale_info::TypeDef::Array(arr) = &ty.type_def { - arr.len - } else { - panic!("Should be an array type") - } - }) - .unwrap_or(0); - let encoded = data.encode(); assert_eq!(encoded[0], variant.index); - assert_eq!(encoded.len() as u32 - 1, field_arr_len); + + // For variants with fields, check the encoded length matches expected field lengths + if !variant.fields.is_empty() { + let expected_len = match data { + Data::None => 0, + Data::Raw(bytes) => bytes.len() as u32, + Data::BlakeTwo256(_) + | Data::Sha256(_) + | Data::Keccak256(_) + | Data::ShaThree256(_) => 32, + Data::TimelockEncrypted { + encrypted, + reveal_round, + } => { + // Calculate length: encrypted (length prefixed) + reveal_round (u64) + let encrypted_len = encrypted.encode().len() as u32; // Includes length prefix + let reveal_round_len = reveal_round.encode().len() as u32; // Typically 8 bytes + encrypted_len + reveal_round_len + } + }; + assert_eq!( + encoded.len() as u32 - 1, // Subtract variant byte + expected_len, + "Encoded length mismatch for variant {}", + variant_name + ); + } else { + assert_eq!( + encoded.len() as u32 - 1, + 0, + "Expected no fields for {}", + variant_name + ); + } } else { - panic!("Should be a variant type") - }; + panic!("Should be a variant type"); + } }; let mut data = vec![ @@ -461,11 +516,17 @@ mod tests { Data::ShaThree256(Default::default()), ]; - // A Raw instance for all possible sizes of the Raw data + // Add Raw instances for all possible sizes for n in 0..128 { - data.push(Data::Raw(vec![0u8; n as usize].try_into().unwrap())) + data.push(Data::Raw(vec![0u8; n as usize].try_into().unwrap())); } + // Add a TimelockEncrypted instance + data.push(Data::TimelockEncrypted { + encrypted: vec![0u8; 64].try_into().unwrap(), // Example encrypted data + reveal_round: 12345, + }); + for d in data.iter() { check_type_info(d); } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 11ca6d6d0..fba6d5984 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -990,12 +990,20 @@ impl pallet_registry::Config for Runtime { } parameter_types! { - pub const MaxCommitFields: u32 = 1; + pub const MaxCommitFieldsInner: u32 = 1; pub const CommitmentInitialDeposit: Balance = 0; // Free pub const CommitmentFieldDeposit: Balance = 0; // Free pub const CommitmentRateLimit: BlockNumber = 100; // Allow commitment every 100 blocks } +#[derive(Clone, Eq, PartialEq, Encode, Decode, TypeInfo)] +pub struct MaxCommitFields; +impl Get for MaxCommitFields { + fn get() -> u32 { + MaxCommitFieldsInner::get() + } +} + pub struct AllowCommitments; impl CanCommit for AllowCommitments { #[cfg(not(feature = "runtime-benchmarks"))] From 3c6bf20367928471b46a61880f1d8879e04b3e72 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Fri, 28 Feb 2025 16:10:26 -0800 Subject: [PATCH 05/28] add reveal_timelocked_commitments to block_step --- Cargo.lock | 6 +- pallets/admin-utils/Cargo.toml | 1 + pallets/admin-utils/src/tests/mock.rs | 37 ++- pallets/commitments/Cargo.toml | 5 +- pallets/commitments/src/lib.rs | 269 +++++++++++++------ pallets/commitments/src/types.rs | 42 --- pallets/subtensor/Cargo.toml | 2 + pallets/subtensor/src/coinbase/block_step.rs | 10 + pallets/subtensor/src/macros/config.rs | 3 + pallets/subtensor/src/tests/mock.rs | 36 +++ pallets/subtensor/src/utils/misc.rs | 6 + runtime/src/lib.rs | 1 + 12 files changed, 293 insertions(+), 125 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 167febc2a..cf2492051 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5989,6 +5989,7 @@ dependencies = [ "frame-system", "log", "pallet-balances", + "pallet-commitments", "pallet-drand", "pallet-evm-chain-id", "pallet-grandpa", @@ -6087,15 +6088,14 @@ dependencies = [ name = "pallet-commitments" version = "4.0.0-dev" dependencies = [ - "ark-bls12-381", "ark-serialize", "enumflags2", "frame-benchmarking", "frame-support", "frame-system", + "log", "pallet-balances", "pallet-drand", - "pallet-subtensor", "parity-scale-codec", "scale-info", "sp-core", @@ -6104,6 +6104,7 @@ dependencies = [ "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk.git?tag=polkadot-stable2409)", "subtensor-macros", "tle", + "w3f-bls", ] [[package]] @@ -6424,6 +6425,7 @@ dependencies = [ "num-traits", "pallet-balances", "pallet-collective", + "pallet-commitments", "pallet-drand", "pallet-membership", "pallet-preimage", diff --git a/pallets/admin-utils/Cargo.toml b/pallets/admin-utils/Cargo.toml index b3c1410cc..d0c4248aa 100644 --- a/pallets/admin-utils/Cargo.toml +++ b/pallets/admin-utils/Cargo.toml @@ -32,6 +32,7 @@ substrate-fixed = { workspace = true } pallet-evm-chain-id = { workspace = true } pallet-drand = { workspace = true, default-features = false } sp-consensus-grandpa = { workspace = true } +pallet-commitments = { default-features = false, path = "../commitments" } [dev-dependencies] sp-core = { workspace = true } diff --git a/pallets/admin-utils/src/tests/mock.rs b/pallets/admin-utils/src/tests/mock.rs index 40e29e54d..b23e44270 100644 --- a/pallets/admin-utils/src/tests/mock.rs +++ b/pallets/admin-utils/src/tests/mock.rs @@ -10,7 +10,7 @@ use frame_system::{EnsureNever, EnsureRoot, limits}; use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_consensus_grandpa::AuthorityList as GrandpaAuthorityList; use sp_core::U256; -use sp_core::{ConstU64, H256}; +use sp_core::{ConstU64, H256, Encode, Decode, Get}; use sp_runtime::{ BuildStorage, KeyTypeId, Perbill, testing::TestXt, @@ -18,6 +18,7 @@ use sp_runtime::{ }; use sp_std::cmp::Ordering; use sp_weights::Weight; +use scale_info::TypeInfo; type Block = frame_system::mocking::MockBlock; @@ -32,6 +33,7 @@ frame_support::construct_runtime!( Drand: pallet_drand::{Pallet, Call, Storage, Event} = 6, Grandpa: pallet_grandpa = 7, EVMChainId: pallet_evm_chain_id = 8, + Commitments: pallet_commitments::{Pallet, Call, Storage, Event} = 9, } ); @@ -200,6 +202,7 @@ impl pallet_subtensor::Config for Test { type InitialColdkeySwapScheduleDuration = InitialColdkeySwapScheduleDuration; type InitialDissolveNetworkScheduleDuration = InitialDissolveNetworkScheduleDuration; type InitialTaoWeight = InitialTaoWeight; + type CommitmentRuntime = Test; } #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] @@ -376,6 +379,38 @@ where type OverarchingCall = RuntimeCall; } +parameter_types! { + pub const MaxCommitFieldsInner: u32 = 1; + pub const CommitmentInitialDeposit: Balance = 0; + pub const CommitmentFieldDeposit: Balance = 0; + pub const CommitmentRateLimit: BlockNumber = 100; +} + +#[derive(Clone, Eq, PartialEq, Encode, Decode, TypeInfo)] +pub struct MaxCommitFields; +impl Get for MaxCommitFields { + fn get() -> u32 { + MaxCommitFieldsInner::get() + } +} + +pub struct AllowCommitments; +impl pallet_commitments::CanCommit for AllowCommitments { + fn can_commit(_netuid: u16, _address: &AccountId) -> bool { + true + } +} + +impl pallet_commitments::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type WeightInfo = pallet_commitments::weights::SubstrateWeight; + type CanCommit = AllowCommitments; + type MaxFields = MaxCommitFields; + type InitialDeposit = CommitmentInitialDeposit; + type FieldDeposit = CommitmentFieldDeposit; + type DefaultRateLimit = CommitmentRateLimit; +} // Build genesis storage according to the mock runtime. pub fn new_test_ext() -> sp_io::TestExternalities { sp_tracing::try_init_simple(); diff --git a/pallets/commitments/Cargo.toml b/pallets/commitments/Cargo.toml index 77a2397e8..31b4001c1 100644 --- a/pallets/commitments/Cargo.toml +++ b/pallets/commitments/Cargo.toml @@ -29,12 +29,11 @@ sp-runtime = { workspace = true } sp-std = { workspace = true } enumflags2 = { workspace = true } -pallet-subtensor = { default-features = false, path = "../subtensor" } - pallet-drand = { path = "../drand", default-features = false } tle = { workspace = true, default-features = false } -ark-bls12-381 = { workspace = true, default-features = false } ark-serialize = { workspace = true, default-features = false } +w3f-bls = { workspace = true, default-features = false } +log = { workspace = true } [dev-dependencies] sp-core = { workspace = true } diff --git a/pallets/commitments/src/lib.rs b/pallets/commitments/src/lib.rs index b30c17cb1..86738e517 100644 --- a/pallets/commitments/src/lib.rs +++ b/pallets/commitments/src/lib.rs @@ -12,9 +12,17 @@ use subtensor_macros::freeze_struct; pub use types::*; pub use weights::WeightInfo; -use frame_support::traits::Currency; +use ark_serialize::CanonicalDeserialize; +use frame_support::{BoundedVec, traits::Currency}; +use frame_system::pallet_prelude::BlockNumberFor; use sp_runtime::{Saturating, traits::Zero}; -use sp_std::boxed::Box; +use sp_std::{boxed::Box, vec::Vec}; +use tle::{ + curves::drand::TinyBLS381, + stream_ciphers::AESGCMStreamCipherProvider, + tlock::{TLECiphertext, tld}, +}; +use w3f_bls::EngineBLS; type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; @@ -134,14 +142,14 @@ pub mod pallet { OptionQuery, >; #[pallet::storage] - #[pallet::getter(fn timelock_commitment_of)] - pub(super) type TimelockCommitmentOf = StorageDoubleMap< + #[pallet::getter(fn revealed_commitments)] + pub(super) type RevealedCommitments = StorageDoubleMap< _, Identity, - u16, // netuid + u16, Twox64Concat, T::AccountId, - CommitmentState, T::MaxFields, BlockNumberFor>, + RevealedData, T::MaxFields, BlockNumberFor>, OptionQuery, >; @@ -179,81 +187,49 @@ pub mod pallet { ); } - let is_timelock = info.fields.iter().any(|data| data.is_timelock_encrypted()); - - if !is_timelock { - let fd = >::from(extra_fields).saturating_mul(T::FieldDeposit::get()); - let mut id = match >::get(netuid, &who) { - Some(mut id) => { - id.info = *info; - id.block = cur_block; - id - } - None => Registration { - info: *info, - block: cur_block, - deposit: Zero::zero(), - }, - }; - - let old_deposit = id.deposit; - id.deposit = T::InitialDeposit::get().saturating_add(fd); - if id.deposit > old_deposit { - T::Currency::reserve(&who, id.deposit.saturating_sub(old_deposit))?; - } - if old_deposit > id.deposit { - let err_amount = - T::Currency::unreserve(&who, old_deposit.saturating_sub(id.deposit)); - debug_assert!(err_amount.is_zero()); + let fd = >::from(extra_fields).saturating_mul(T::FieldDeposit::get()); + let mut id = match >::get(netuid, &who) { + Some(mut id) => { + id.info = *info.clone(); + id.block = cur_block; + id } + None => Registration { + info: *info.clone(), + block: cur_block, + deposit: Zero::zero(), + }, + }; + + let old_deposit = id.deposit; + id.deposit = T::InitialDeposit::get().saturating_add(fd); + if id.deposit > old_deposit { + T::Currency::reserve(&who, id.deposit.saturating_sub(old_deposit))?; + } + if old_deposit > id.deposit { + let err_amount = + T::Currency::unreserve(&who, old_deposit.saturating_sub(id.deposit)); + debug_assert!(err_amount.is_zero()); + } - >::insert(netuid, &who, id); - >::insert(netuid, &who, cur_block); - Self::deposit_event(Event::Commitment { netuid, who }); - - Ok(()) + >::insert(netuid, &who, id); + >::insert(netuid, &who, cur_block); + + if let Some(Data::TimelockEncrypted { reveal_round, .. }) = info + .fields + .iter() + .find(|data| matches!(data, Data::TimelockEncrypted { .. })) + { + Self::deposit_event(Event::TimelockCommitment { + netuid, + who, + reveal_round: *reveal_round, + }); } else { - ensure!( - info.fields.len() == 1, - Error::::TooManyFieldsInCommitmentInfo, - ); - - if let Data::TimelockEncrypted { - encrypted, - reveal_round, - } = &info.fields[0] - { - // Calculate reveal block - let last_drand_round = pallet_drand::LastStoredRound::::get(); - let blocks_per_round = 12_u64.checked_div(3).unwrap_or(0); - let rounds_since_last = reveal_round.saturating_sub(last_drand_round); - let blocks_to_reveal = rounds_since_last.saturating_mul(blocks_per_round); - let blocks_to_reveal: BlockNumberFor = blocks_to_reveal - .try_into() - .map_err(|_| "Block number conversion failed")?; - let reveal_block = cur_block.saturating_add(blocks_to_reveal); - - // Construct CommitmentState for timelock commitment - let commitment_state = CommitmentState { - encrypted_commitment: encrypted.clone(), - reveal_round: *reveal_round, - reveal_block, - revealed: None, - }; - - // Store in TimelockCommitmentOf - >::insert(netuid, &who, commitment_state); - >::insert(netuid, &who, cur_block); - - // Emit timelock-specific event - Self::deposit_event(Event::TimelockCommitment { - netuid, - who, - reveal_round: *reveal_round, - }); - } - Ok(()) + Self::deposit_event(Event::Commitment { netuid, who }); } + + Ok(()) } /// Sudo-set the commitment rate limit @@ -400,3 +376,142 @@ where Ok(()) } } + +impl Pallet { + pub fn reveal_timelocked_commitments(current_block: u64) -> DispatchResult { + let current_block = current_block + .try_into() + .map_err(|_| "Failed to convert u64 to BlockNumberFor")?; + + for (netuid, who, mut registration) in >::iter() { + if let Some(Data::TimelockEncrypted { + encrypted, + reveal_round, + .. + }) = registration + .info + .fields + .clone() + .iter() + .find(|data| matches!(data, Data::TimelockEncrypted { .. })) + { + // Calculate reveal block + let reveal_block = Self::calculate_reveal_block(*reveal_round, registration.block)?; + + // Check if the current block has reached or exceeded the reveal block + if current_block >= reveal_block { + // Deserialize the encrypted commitment into a TLECiphertext + let reader = &mut &encrypted[..]; + let commit = TLECiphertext::::deserialize_compressed(reader) + .map_err(|e| { + log::warn!("Failed to deserialize TLECiphertext for {:?}: {:?}", who, e) + }) + .ok(); + + let commit = match commit { + Some(c) => c, + None => continue, + }; + + // Get the drand pulse for the reveal round + let pulse = match pallet_drand::Pulses::::get(*reveal_round) { + Some(p) => p, + None => { + log::warn!( + "Failed to reveal commit for subnet {} by {:?}: missing drand round {}", + netuid, + who, + reveal_round + ); + continue; + } + }; + + // Prepare the signature bytes + let signature_bytes = pulse + .signature + .strip_prefix(b"0x") + .unwrap_or(&pulse.signature); + let sig_reader = &mut &signature_bytes[..]; + let sig = ::SignatureGroup::deserialize_compressed( + sig_reader, + ) + .map_err(|e| { + log::warn!( + "Failed to deserialize drand signature for {:?}: {:?}", + who, + e + ) + }) + .ok(); + + let sig = match sig { + Some(s) => s, + None => continue, + }; + + // Decrypt the timelock commitment + let decrypted_bytes: Vec = + tld::(commit, sig) + .map_err(|e| { + log::warn!("Failed to decrypt timelock for {:?}: {:?}", who, e) + }) + .ok() + .unwrap_or_default(); + + if decrypted_bytes.is_empty() { + continue; + } + + // Decode the decrypted bytes into CommitmentInfo (assuming it’s SCALE-encoded CommitmentInfo) + let mut reader = &decrypted_bytes[..]; + let revealed_info: CommitmentInfo = Decode::decode(&mut reader) + .map_err(|e| { + log::warn!("Failed to decode decrypted data for {:?}: {:?}", who, e) + }) + .ok() + .unwrap_or_else(|| CommitmentInfo { + fields: BoundedVec::default(), + }); + + // Create RevealedData for storage + let revealed_data = RevealedData { + info: revealed_info, + revealed_block: current_block, + deposit: registration.deposit, + }; + + // Store the revealed data in RevealedCommitments + >::insert(netuid, &who, revealed_data); + + // Remove the TimelockEncrypted field from the original commitment + let filtered_fields: Vec = registration.info.fields.into_iter() + .filter(|data| !matches!(data, Data::TimelockEncrypted { reveal_round: r, .. } if r == reveal_round)) + .collect(); + registration.info.fields = BoundedVec::try_from(filtered_fields) + .map_err(|_| "Failed to filter timelock fields")?; + + Self::deposit_event(Event::CommitmentRevealed { netuid, who }); + } + } + } + + Ok(()) + } + + fn calculate_reveal_block( + reveal_round: u64, + commit_block: BlockNumberFor, + ) -> Result, &'static str> { + let last_drand_round = pallet_drand::LastStoredRound::::get(); + let blocks_per_round = 12_u64.checked_div(3).unwrap_or(0); // 4 blocks per round (12s blocktime / 3s round) + let rounds_since_last = reveal_round.saturating_sub(last_drand_round); + let blocks_to_reveal = rounds_since_last.saturating_mul(blocks_per_round); + let reveal_block = commit_block.saturating_add( + blocks_to_reveal + .try_into() + .map_err(|_| "Block number conversion failed")?, + ); + Ok(reveal_block) + } +} diff --git a/pallets/commitments/src/types.rs b/pallets/commitments/src/types.rs index 40b997589..7fe76a719 100644 --- a/pallets/commitments/src/types.rs +++ b/pallets/commitments/src/types.rs @@ -31,10 +31,6 @@ use sp_runtime::{ use sp_std::{fmt::Debug, iter::once, prelude::*}; use subtensor_macros::freeze_struct; -use crate::Config; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use tle::curves::drand::TinyBLS381; - /// Either underlying data blob if it is at most 32 bytes, or a hash of it. If the data is greater /// than 32-bytes then it will be truncated when encoding. /// @@ -341,15 +337,6 @@ pub struct CommitmentInfo> { /// Maximum size of the serialized timelock commitment in bytes pub const MAX_TIMELOCK_COMMITMENT_SIZE_BYTES: u32 = 1024; -/// Represents a commitment that can be either unrevealed (timelock-encrypted) or revealed. -#[derive(Clone, Eq, PartialEq, Encode, Decode, TypeInfo, Debug)] -pub struct CommitmentState, BlockNumber> { - pub encrypted_commitment: BoundedVec>, - pub reveal_round: u64, - pub reveal_block: BlockNumber, - pub revealed: Option>, -} - /// Contains the decrypted data of a revealed commitment. #[derive(Clone, Eq, PartialEq, Encode, Decode, TypeInfo, Debug)] pub struct RevealedData, BlockNumber> { @@ -358,35 +345,6 @@ pub struct RevealedData, BlockNumber> { pub deposit: Balance, } -impl, BlockNumber: Clone + From> - CommitmentState -{ - pub fn from_tle_ciphertext( - ciphertext: tle::tlock::TLECiphertext, - reveal_round: u64, - reveal_block: BlockNumber, - ) -> Result { - let mut encrypted_data = Vec::new(); - ciphertext - .serialize_compressed(&mut encrypted_data) - .map_err(|_| "Failed to serialize TLECiphertext")?; - let bounded_encrypted = BoundedVec::try_from(encrypted_data) - .map_err(|_| "Encrypted commitment exceeds max size")?; - Ok(CommitmentState { - encrypted_commitment: bounded_encrypted, - reveal_round, - reveal_block, - revealed: None, - }) - } - - pub fn to_tle_ciphertext(&self) -> Result, &'static str> { - let mut reader = &self.encrypted_commitment[..]; - tle::tlock::TLECiphertext::::deserialize_compressed(&mut reader) - .map_err(|_| "Failed to deserialize TLECiphertext") - } -} - /// Information concerning the identity of the controller of an account. /// /// NOTE: This is stored separately primarily to facilitate the addition of extra fields in a diff --git a/pallets/subtensor/Cargo.toml b/pallets/subtensor/Cargo.toml index f240245c4..34c7ee50d 100644 --- a/pallets/subtensor/Cargo.toml +++ b/pallets/subtensor/Cargo.toml @@ -56,6 +56,8 @@ w3f-bls = { workspace = true, default-features = false } sha2 = { workspace = true } rand_chacha = { workspace = true } +pallet-commitments = { default-features = false, path = "../commitments" } + [dev-dependencies] pallet-balances = { workspace = true, features = ["std"] } pallet-scheduler = { workspace = true } diff --git a/pallets/subtensor/src/coinbase/block_step.rs b/pallets/subtensor/src/coinbase/block_step.rs index 669f8e09d..6943a0265 100644 --- a/pallets/subtensor/src/coinbase/block_step.rs +++ b/pallets/subtensor/src/coinbase/block_step.rs @@ -18,6 +18,16 @@ impl Pallet { Self::run_coinbase(block_emission); // --- 4. Set pending children on the epoch; but only after the coinbase has been run. Self::try_set_pending_children(block_number); + + // --- 5. Unveil all matured timelocked entries + if let Err(e) = Self::reveal_timelocked_commitments(block_number) { + log::debug!( + "Failed to unveil matured commitments on block {} due to error: {:?}", + block_number, + e + ); + } + // Return ok. Ok(()) } diff --git a/pallets/subtensor/src/macros/config.rs b/pallets/subtensor/src/macros/config.rs index 72a4c5f0d..e7ed269f9 100644 --- a/pallets/subtensor/src/macros/config.rs +++ b/pallets/subtensor/src/macros/config.rs @@ -47,6 +47,9 @@ mod config { /// the preimage to store the call data. type Preimages: QueryPreimage + StorePreimage; + /// The commitment pallet's runtime + type CommitmentRuntime: pallet_commitments::Config; + /// ================================= /// ==== Initial Value Constants ==== /// ================================= diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index aae6aa60e..2f2eb5d8a 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -38,6 +38,7 @@ frame_support::construct_runtime!( Scheduler: pallet_scheduler::{Pallet, Call, Storage, Event} = 9, Preimage: pallet_preimage::{Pallet, Call, Storage, Event} = 10, Drand: pallet_drand::{Pallet, Call, Storage, Event} = 11, + Commitments: pallet_commitments::{Pallet, Call, Storage, Event} = 12, } ); @@ -409,6 +410,15 @@ impl crate::Config for Test { type InitialColdkeySwapScheduleDuration = InitialColdkeySwapScheduleDuration; type InitialDissolveNetworkScheduleDuration = InitialDissolveNetworkScheduleDuration; type InitialTaoWeight = InitialTaoWeight; + type CommitmentRuntime = Test; +} + + +parameter_types! { + pub const MaxCommitFieldsInner: u32 = 1; + pub const CommitmentInitialDeposit: Balance = 0; + pub const CommitmentFieldDeposit: Balance = 0; + pub const CommitmentRateLimit: BlockNumber = 100; } pub struct OriginPrivilegeCmp; @@ -532,6 +542,32 @@ impl frame_system::offchain::CreateSignedTransaction> f } } +#[derive(Clone, Eq, PartialEq, Encode, Decode, TypeInfo)] +pub struct MaxCommitFields; +impl Get for MaxCommitFields { + fn get() -> u32 { + MaxCommitFieldsInner::get() + } +} + +pub struct AllowCommitments; +impl pallet_commitments::CanCommit for AllowCommitments { + fn can_commit(_netuid: u16, _address: &AccountId) -> bool { + true + } +} + +impl pallet_commitments::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type WeightInfo = pallet_commitments::weights::SubstrateWeight; + type CanCommit = AllowCommitments; + type MaxFields = MaxCommitFields; + type InitialDeposit = CommitmentInitialDeposit; + type FieldDeposit = CommitmentFieldDeposit; + type DefaultRateLimit = CommitmentRateLimit; +} + #[allow(dead_code)] // Build genesis storage according to the mock runtime. pub fn new_test_ext(block_number: BlockNumber) -> sp_io::TestExternalities { diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index bd093a76b..8615c8c7c 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -743,4 +743,10 @@ impl Pallet { DissolveNetworkScheduleDuration::::set(duration); Self::deposit_event(Event::DissolveNetworkScheduleDurationSet(duration)); } + + pub fn reveal_timelocked_commitments(block_number: u64) -> DispatchResult { + pallet_commitments::Pallet::::reveal_timelocked_commitments( + block_number, + ) + } } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index fba6d5984..8c2fbd43b 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1162,6 +1162,7 @@ impl pallet_subtensor::Config for Runtime { type Preimages = Preimage; type InitialColdkeySwapScheduleDuration = InitialColdkeySwapScheduleDuration; type InitialDissolveNetworkScheduleDuration = InitialDissolveNetworkScheduleDuration; + type CommitmentRuntime = Runtime; } use sp_runtime::BoundedVec; From c7bcf7d9f0100ac051ab15733caffdca02d0da3f Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Mon, 3 Mar 2025 10:53:29 -0800 Subject: [PATCH 06/28] add basic commitments tetsts --- pallets/admin-utils/Cargo.toml | 3 + pallets/admin-utils/src/tests/mock.rs | 4 +- pallets/commitments/Cargo.toml | 13 ++- pallets/commitments/src/tests.rs | 26 +++-- pallets/commitments/src/types.rs | 161 +++++++++++++++++++++++++- pallets/subtensor/Cargo.toml | 9 +- pallets/subtensor/src/tests/mock.rs | 3 +- 7 files changed, 199 insertions(+), 20 deletions(-) diff --git a/pallets/admin-utils/Cargo.toml b/pallets/admin-utils/Cargo.toml index d0c4248aa..d8a413977 100644 --- a/pallets/admin-utils/Cargo.toml +++ b/pallets/admin-utils/Cargo.toml @@ -68,6 +68,7 @@ std = [ "sp-tracing/std", "sp-weights/std", "substrate-fixed/std", + "pallet-commitments/std" ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", @@ -79,6 +80,7 @@ runtime-benchmarks = [ "pallet-scheduler/runtime-benchmarks", "pallet-subtensor/runtime-benchmarks", "sp-runtime/runtime-benchmarks", + "pallet-commitments/runtime-benchmarks" ] try-runtime = [ "frame-support/try-runtime", @@ -90,4 +92,5 @@ try-runtime = [ "pallet-scheduler/try-runtime", "pallet-subtensor/try-runtime", "sp-runtime/try-runtime", + "pallet-commitments/try-runtime" ] diff --git a/pallets/admin-utils/src/tests/mock.rs b/pallets/admin-utils/src/tests/mock.rs index b23e44270..aa7d8cd1d 100644 --- a/pallets/admin-utils/src/tests/mock.rs +++ b/pallets/admin-utils/src/tests/mock.rs @@ -7,10 +7,11 @@ use frame_support::{ }; use frame_system as system; use frame_system::{EnsureNever, EnsureRoot, limits}; +use scale_info::TypeInfo; use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_consensus_grandpa::AuthorityList as GrandpaAuthorityList; use sp_core::U256; -use sp_core::{ConstU64, H256, Encode, Decode, Get}; +use sp_core::{ConstU64, Decode, Encode, Get, H256}; use sp_runtime::{ BuildStorage, KeyTypeId, Perbill, testing::TestXt, @@ -18,7 +19,6 @@ use sp_runtime::{ }; use sp_std::cmp::Ordering; use sp_weights::Weight; -use scale_info::TypeInfo; type Block = frame_system::mocking::MockBlock; diff --git a/pallets/commitments/Cargo.toml b/pallets/commitments/Cargo.toml index 31b4001c1..f8cb5e2cf 100644 --- a/pallets/commitments/Cargo.toml +++ b/pallets/commitments/Cargo.toml @@ -53,18 +53,25 @@ std = [ "enumflags2/std", "pallet-balances/std", "sp-core/std", - "sp-io/std" + "sp-io/std", + "ark-serialize/std", + "log/std", + "pallet-drand/std", + "tle/std", + "w3f-bls/std" ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", "sp-runtime/runtime-benchmarks", - "pallet-balances/runtime-benchmarks" + "pallet-balances/runtime-benchmarks", + "pallet-drand/runtime-benchmarks" ] try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", "pallet-balances/try-runtime", - "sp-runtime/try-runtime" + "sp-runtime/try-runtime", + "pallet-drand/try-runtime" ] diff --git a/pallets/commitments/src/tests.rs b/pallets/commitments/src/tests.rs index 074227111..b33d07ca2 100644 --- a/pallets/commitments/src/tests.rs +++ b/pallets/commitments/src/tests.rs @@ -8,6 +8,7 @@ use frame_support::{ }; use sp_core::H256; use sp_runtime::{ + BuildStorage, testing::Header, traits::{BlakeTwo256, ConstU16, IdentityLookup}, }; @@ -86,12 +87,19 @@ impl TypeInfo for TestMaxFields { } } +pub struct TestCanCommit; +impl pallet_commitments::CanCommit for TestCanCommit { + fn can_commit(_netuid: u16, _who: &u64) -> bool { + true + } +} + impl pallet_commitments::Config for Test { type RuntimeEvent = RuntimeEvent; type Currency = Balances; type WeightInfo = (); type MaxFields = TestMaxFields; - type CanCommit = (); + type CanCommit = TestCanCommit; type FieldDeposit = ConstU64<0>; type InitialDeposit = ConstU64<0>; type DefaultRateLimit = ConstU64<0>; @@ -167,11 +175,11 @@ where type OverarchingCall = RuntimeCall; } -// pub fn new_test_ext() -> sp_io::TestExternalities { -// let t = frame_system::GenesisConfig::::default() -// .build_storage() -// .unwrap(); -// let mut ext = sp_io::TestExternalities::new(t); -// ext.execute_with(|| System::set_block_number(1)); -// ext -// } +pub fn new_test_ext() -> sp_io::TestExternalities { + let t = frame_system::GenesisConfig::::default() + .build_storage() + .unwrap(); + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} diff --git a/pallets/commitments/src/types.rs b/pallets/commitments/src/types.rs index 7fe76a719..8a43394aa 100644 --- a/pallets/commitments/src/types.rs +++ b/pallets/commitments/src/types.rs @@ -400,6 +400,12 @@ impl< #[allow(clippy::indexing_slicing, clippy::unwrap_used)] mod tests { use super::*; + use crate::{ + Config, Error, Event, Pallet, RateLimit, + tests::{RuntimeEvent, RuntimeOrigin, Test, new_test_ext}, + }; + use frame_support::{BoundedVec, assert_noop, assert_ok}; + use frame_system::Pallet as System; #[test] fn manual_data_type_info() { @@ -481,7 +487,7 @@ mod tests { // Add a TimelockEncrypted instance data.push(Data::TimelockEncrypted { - encrypted: vec![0u8; 64].try_into().unwrap(), // Example encrypted data + encrypted: vec![0u8; 64].try_into().unwrap(), reveal_round: 12345, }); @@ -489,4 +495,157 @@ mod tests { check_type_info(d); } } + + #[test] + fn set_commitment_works() { + new_test_ext().execute_with(|| { + System::::set_block_number(1); + let info = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![]).unwrap(), + ..Default::default() + }); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(1), + 1, + info.clone() + )); + + let commitment = Pallet::::commitment_of(1, &1).unwrap(); + let initial_deposit: u64 = ::InitialDeposit::get(); + assert_eq!(commitment.deposit, initial_deposit); + assert_eq!(commitment.block, 1); + assert_eq!(Pallet::::last_commitment(1, &1), Some(1)); + }); + } + + #[test] + #[should_panic(expected = "BoundedVec::try_from failed")] + fn set_commitment_too_many_fields_panics() { + new_test_ext().execute_with(|| { + let max_fields: u32 = ::MaxFields::get(); + let fields = vec![Data::None; (max_fields + 1) as usize]; + + // This line will panic when 'BoundedVec::try_from(...)' sees too many items. + let info = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(fields).expect("BoundedVec::try_from failed"), + ..Default::default() + }); + + // We never get here, because the constructor panics above. + let _ = + Pallet::::set_commitment(frame_system::RawOrigin::Signed(1).into(), 1, info); + }); + } + + #[test] + fn set_commitment_rate_limit_exceeded() { + new_test_ext().execute_with(|| { + let rate_limit = ::DefaultRateLimit::get(); + System::::set_block_number(1); + let info = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![]).unwrap(), + ..Default::default() + }); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(1), + 1, + info.clone() + )); + + // Set block number to just before rate limit expires + System::::set_block_number(rate_limit); + assert_noop!( + Pallet::::set_commitment(RuntimeOrigin::signed(1), 1, info.clone()), + Error::::CommitmentSetRateLimitExceeded + ); + + // Set block number to after rate limit + System::::set_block_number(rate_limit + 1); + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(1), + 1, + info + )); + }); + } + + #[test] + fn set_commitment_updates_deposit() { + new_test_ext().execute_with(|| { + System::::set_block_number(1); + let info1 = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![Default::default(); 2]).unwrap(), + ..Default::default() + }); + let info2 = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![Default::default(); 3]).unwrap(), + ..Default::default() + }); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(1), + 1, + info1 + )); + let initial_deposit: u64 = ::InitialDeposit::get(); + let field_deposit: u64 = ::FieldDeposit::get(); + let expected_deposit1: u64 = initial_deposit + 2u64 * field_deposit; + assert_eq!( + Pallet::::commitment_of(1, &1).unwrap().deposit, + expected_deposit1 + ); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(1), + 1, + info2 + )); + let expected_deposit2: u64 = initial_deposit + 3u64 * field_deposit; + assert_eq!( + Pallet::::commitment_of(1, &1).unwrap().deposit, + expected_deposit2 + ); + }); + } + + #[test] + fn set_rate_limit_works() { + new_test_ext().execute_with(|| { + let default_rate_limit: u64 = ::DefaultRateLimit::get(); + assert_eq!(RateLimit::::get(), default_rate_limit); + + assert_ok!(Pallet::::set_rate_limit(RuntimeOrigin::root(), 200)); + assert_eq!(RateLimit::::get(), 200); + + assert_noop!( + Pallet::::set_rate_limit(RuntimeOrigin::signed(1), 300), + sp_runtime::DispatchError::BadOrigin + ); + }); + } + + #[test] + fn event_emission_works() { + new_test_ext().execute_with(|| { + System::::set_block_number(1); + let info = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![]).unwrap(), + ..Default::default() + }); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(1), + 1, + info + )); + + let events = System::::events(); + assert!(events.iter().any(|e| matches!( + &e.event, + RuntimeEvent::Commitments(Event::Commitment { netuid: 1, who: 1 }) + ))); + }); + } } diff --git a/pallets/subtensor/Cargo.toml b/pallets/subtensor/Cargo.toml index 34c7ee50d..42b468b1d 100644 --- a/pallets/subtensor/Cargo.toml +++ b/pallets/subtensor/Cargo.toml @@ -109,7 +109,8 @@ std = [ "rand_chacha/std", "safe-math/std", "sha2/std", - "share-pool/std" + "share-pool/std", + "pallet-commitments/std" ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", @@ -122,7 +123,8 @@ runtime-benchmarks = [ "pallet-collective/runtime-benchmarks", "pallet-preimage/runtime-benchmarks", "pallet-scheduler/runtime-benchmarks", - "pallet-drand/runtime-benchmarks" + "pallet-drand/runtime-benchmarks", + "pallet-commitments/runtime-benchmarks" ] try-runtime = [ "frame-support/try-runtime", @@ -135,7 +137,8 @@ try-runtime = [ "pallet-utility/try-runtime", "sp-runtime/try-runtime", "pallet-collective/try-runtime", - "pallet-drand/try-runtime" + "pallet-drand/try-runtime", + "pallet-commitments/try-runtime" ] pow-faucet = [] fast-blocks = [] diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index 2f2eb5d8a..bdaca2269 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -413,7 +413,6 @@ impl crate::Config for Test { type CommitmentRuntime = Test; } - parameter_types! { pub const MaxCommitFieldsInner: u32 = 1; pub const CommitmentInitialDeposit: Balance = 0; @@ -553,7 +552,7 @@ impl Get for MaxCommitFields { pub struct AllowCommitments; impl pallet_commitments::CanCommit for AllowCommitments { fn can_commit(_netuid: u16, _address: &AccountId) -> bool { - true + true } } From f95ca6401d0a51584a367c9939165f8351a5f44b Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Mon, 3 Mar 2025 11:25:26 -0800 Subject: [PATCH 07/28] restructure commitments pallet --- pallets/commitments/src/lib.rs | 3 + pallets/commitments/src/mock.rs | 155 ++++++++++-- pallets/commitments/src/tests.rs | 399 ++++++++++++++++++------------- pallets/commitments/src/types.rs | 254 -------------------- 4 files changed, 378 insertions(+), 433 deletions(-) diff --git a/pallets/commitments/src/lib.rs b/pallets/commitments/src/lib.rs index 86738e517..77cc06ddb 100644 --- a/pallets/commitments/src/lib.rs +++ b/pallets/commitments/src/lib.rs @@ -4,6 +4,9 @@ mod benchmarking; #[cfg(test)] mod tests; +#[cfg(test)] +mod mock; + pub mod types; pub mod weights; diff --git a/pallets/commitments/src/mock.rs b/pallets/commitments/src/mock.rs index 2d28a44c0..b77c10548 100644 --- a/pallets/commitments/src/mock.rs +++ b/pallets/commitments/src/mock.rs @@ -1,23 +1,33 @@ use crate as pallet_commitments; -use frame_support::traits::{ConstU16, ConstU64}; +use frame_support::{ + derive_impl, + pallet_prelude::{Get, TypeInfo}, + traits::{ConstU32, ConstU64}, +}; use sp_core::H256; use sp_runtime::{ - traits::{BlakeTwo256, IdentityLookup}, BuildStorage, + testing::Header, + traits::{BlakeTwo256, ConstU16, IdentityLookup}, }; -type Block = frame_system::mocking::MockBlock; +pub type Block = sp_runtime::generic::Block; +pub type UncheckedExtrinsic = + sp_runtime::generic::UncheckedExtrinsic; -// Configure a mock runtime to test the pallet. frame_support::construct_runtime!( pub enum Test { System: frame_system = 1, - Commitments: pallet_commitments = 2, - SubtensorModule: pallet_subtensor = 3, + Balances: pallet_balances = 2, + Commitments: pallet_commitments = 3, + Drand: pallet_drand = 4, } ); +pub type AccountId = u64; + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] impl frame_system::Config for Test { type BaseCallFilter = frame_support::traits::Everything; type BlockWeights = (); @@ -25,36 +35,149 @@ impl frame_system::Config for Test { type DbWeight = (); type RuntimeOrigin = RuntimeOrigin; type RuntimeCall = RuntimeCall; - type Nonce = u64; type Hash = H256; type Hashing = BlakeTwo256; type AccountId = u64; type Lookup = IdentityLookup; - type Block = Block; type RuntimeEvent = RuntimeEvent; type BlockHashCount = ConstU64<250>; type Version = (); type PalletInfo = PalletInfo; - type AccountData = (); + type AccountData = pallet_balances::AccountData; type OnNewAccount = (); type OnKilledAccount = (); type SystemWeightInfo = (); type SS58Prefix = ConstU16<42>; type OnSetCode = (); - type MaxConsumers = frame_support::traits::ConstU32<16>; + type MaxConsumers = ConstU32<16>; + type Block = Block; + type Nonce = u32; +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] +impl pallet_balances::Config for Test { + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type Balance = u64; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ConstU64<1>; + type AccountStore = System; + type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct TestMaxFields; +impl Get for TestMaxFields { + fn get() -> u32 { + 16 + } +} +impl TypeInfo for TestMaxFields { + type Identity = Self; + fn type_info() -> scale_info::Type { + scale_info::Type::builder() + .path(scale_info::Path::new("TestMaxFields", module_path!())) + .composite(scale_info::build::Fields::unit()) + } +} + +pub struct TestCanCommit; +impl pallet_commitments::CanCommit for TestCanCommit { + fn can_commit(_netuid: u16, _who: &u64) -> bool { + true + } } impl pallet_commitments::Config for Test { type RuntimeEvent = RuntimeEvent; + type Currency = Balances; type WeightInfo = (); - type MaxAdditionalFields = frame_support::traits::ConstU32<16>; - type CanRegisterIdentity = (); + type MaxFields = TestMaxFields; + type CanCommit = TestCanCommit; + type FieldDeposit = ConstU64<0>; + type InitialDeposit = ConstU64<0>; + type DefaultRateLimit = ConstU64<0>; +} + +impl pallet_drand::Config for Test { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = pallet_drand::weights::SubstrateWeight; + type AuthorityId = test_crypto::TestAuthId; + type Verifier = pallet_drand::verifier::QuicknetVerifier; + type UnsignedPriority = ConstU64<{ 1 << 20 }>; + type HttpFetchTimeout = ConstU64<1_000>; +} + +pub mod test_crypto { + use sp_core::sr25519::{Public as Sr25519Public, Signature as Sr25519Signature}; + use sp_runtime::{ + app_crypto::{app_crypto, sr25519}, + traits::IdentifyAccount, + }; + + pub const KEY_TYPE: sp_runtime::KeyTypeId = sp_runtime::KeyTypeId(*b"test"); + + app_crypto!(sr25519, KEY_TYPE); + + pub struct TestAuthId; + + impl frame_system::offchain::AppCrypto for TestAuthId { + type RuntimeAppPublic = Public; + type GenericSignature = Sr25519Signature; + type GenericPublic = Sr25519Public; + } + + impl IdentifyAccount for Public { + type AccountId = u64; + + fn into_account(self) -> u64 { + let mut bytes = [0u8; 32]; + bytes.copy_from_slice(self.as_ref()); + u64::from_le_bytes(bytes[..8].try_into().unwrap()) + } + } +} + +impl frame_system::offchain::SigningTypes for Test { + type Public = test_crypto::Public; + type Signature = test_crypto::Signature; +} + +impl frame_system::offchain::CreateSignedTransaction> for Test { + fn create_transaction>( + call: RuntimeCall, + _public: Self::Public, + account: Self::AccountId, + _nonce: u32, + ) -> Option<( + RuntimeCall, + ::SignaturePayload, + )> { + // Create a dummy sr25519 signature from a raw byte array + let dummy_raw = [0u8; 64]; + let dummy_signature = sp_core::sr25519::Signature::from(dummy_raw); + let signature = test_crypto::Signature::from(dummy_signature); + Some((call, (account, signature, ()))) + } +} + +impl frame_system::offchain::SendTransactionTypes for Test +where + RuntimeCall: From, +{ + type Extrinsic = UncheckedExtrinsic; + type OverarchingCall = RuntimeCall; } -// Build genesis storage according to the mock runtime. pub fn new_test_ext() -> sp_io::TestExternalities { - frame_system::GenesisConfig::::default() + let t = frame_system::GenesisConfig::::default() .build_storage() - .unwrap() - .into() + .unwrap(); + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext } diff --git a/pallets/commitments/src/tests.rs b/pallets/commitments/src/tests.rs index b33d07ca2..c6b8d93d6 100644 --- a/pallets/commitments/src/tests.rs +++ b/pallets/commitments/src/tests.rs @@ -1,185 +1,258 @@ -#![allow(non_camel_case_types)] - -use crate as pallet_commitments; -use frame_support::{ - derive_impl, - pallet_prelude::{Get, TypeInfo}, - traits::{ConstU32, ConstU64}, -}; -use sp_core::H256; -use sp_runtime::{ - BuildStorage, - testing::Header, - traits::{BlakeTwo256, ConstU16, IdentityLookup}, -}; - -pub type Block = sp_runtime::generic::Block; -pub type UncheckedExtrinsic = - sp_runtime::generic::UncheckedExtrinsic; - -frame_support::construct_runtime!( - pub enum Test - { - System: frame_system = 1, - Balances: pallet_balances = 2, - Commitments: pallet_commitments = 3, - Drand: pallet_drand = 4, - } -); - -pub type AccountId = u64; - -#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] -impl frame_system::Config for Test { - type BaseCallFilter = frame_support::traits::Everything; - type BlockWeights = (); - type BlockLength = (); - type DbWeight = (); - type RuntimeOrigin = RuntimeOrigin; - type RuntimeCall = RuntimeCall; - type Hash = H256; - type Hashing = BlakeTwo256; - type AccountId = u64; - type Lookup = IdentityLookup; - type RuntimeEvent = RuntimeEvent; - type BlockHashCount = ConstU64<250>; - type Version = (); - type PalletInfo = PalletInfo; - type AccountData = pallet_balances::AccountData; - type OnNewAccount = (); - type OnKilledAccount = (); - type SystemWeightInfo = (); - type SS58Prefix = ConstU16<42>; - type OnSetCode = (); - type MaxConsumers = ConstU32<16>; - type Block = Block; - type Nonce = u32; -} +use crate::{CommitmentInfo, Data}; +use codec::Encode; +use frame_support::traits::Get; +use sp_std::prelude::*; -#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] -impl pallet_balances::Config for Test { - type MaxLocks = (); - type MaxReserves = (); - type ReserveIdentifier = [u8; 8]; - type Balance = u64; - type RuntimeEvent = RuntimeEvent; - type DustRemoval = (); - type ExistentialDeposit = ConstU64<1>; - type AccountStore = System; - type WeightInfo = (); - type FreezeIdentifier = (); - type MaxFreezes = (); -} +#[cfg(test)] +#[allow(clippy::indexing_slicing, clippy::unwrap_used)] +mod tests { + use super::*; + use crate::{ + Config, Error, Event, Pallet, RateLimit, + mock::{RuntimeEvent, RuntimeOrigin, Test, new_test_ext}, + }; + use frame_support::{BoundedVec, assert_noop, assert_ok}; + use frame_system::Pallet as System; -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct TestMaxFields; -impl Get for TestMaxFields { - fn get() -> u32 { - 16 - } -} -impl TypeInfo for TestMaxFields { - type Identity = Self; - fn type_info() -> scale_info::Type { - scale_info::Type::builder() - .path(scale_info::Path::new("TestMaxFields", module_path!())) - .composite(scale_info::build::Fields::unit()) + #[test] + fn manual_data_type_info() { + let mut registry = scale_info::Registry::new(); + let type_id = registry.register_type(&scale_info::meta_type::()); + let registry: scale_info::PortableRegistry = registry.into(); + let type_info = registry.resolve(type_id.id).unwrap(); + + let check_type_info = |data: &Data| { + let variant_name = match data { + Data::None => "None".to_string(), + Data::BlakeTwo256(_) => "BlakeTwo256".to_string(), + Data::Sha256(_) => "Sha256".to_string(), + Data::Keccak256(_) => "Keccak256".to_string(), + Data::ShaThree256(_) => "ShaThree256".to_string(), + Data::Raw(bytes) => format!("Raw{}", bytes.len()), + Data::TimelockEncrypted { .. } => "TimelockEncrypted".to_string(), + }; + if let scale_info::TypeDef::Variant(variant) = &type_info.type_def { + let variant = variant + .variants + .iter() + .find(|v| v.name == variant_name) + .unwrap_or_else(|| panic!("Expected to find variant {}", variant_name)); + + let encoded = data.encode(); + assert_eq!(encoded[0], variant.index); + + // For variants with fields, check the encoded length matches expected field lengths + if !variant.fields.is_empty() { + let expected_len = match data { + Data::None => 0, + Data::Raw(bytes) => bytes.len() as u32, + Data::BlakeTwo256(_) + | Data::Sha256(_) + | Data::Keccak256(_) + | Data::ShaThree256(_) => 32, + Data::TimelockEncrypted { + encrypted, + reveal_round, + } => { + // Calculate length: encrypted (length prefixed) + reveal_round (u64) + let encrypted_len = encrypted.encode().len() as u32; // Includes length prefix + let reveal_round_len = reveal_round.encode().len() as u32; // Typically 8 bytes + encrypted_len + reveal_round_len + } + }; + assert_eq!( + encoded.len() as u32 - 1, // Subtract variant byte + expected_len, + "Encoded length mismatch for variant {}", + variant_name + ); + } else { + assert_eq!( + encoded.len() as u32 - 1, + 0, + "Expected no fields for {}", + variant_name + ); + } + } else { + panic!("Should be a variant type"); + } + }; + + let mut data = vec![ + Data::None, + Data::BlakeTwo256(Default::default()), + Data::Sha256(Default::default()), + Data::Keccak256(Default::default()), + Data::ShaThree256(Default::default()), + ]; + + // Add Raw instances for all possible sizes + for n in 0..128 { + data.push(Data::Raw(vec![0u8; n as usize].try_into().unwrap())); + } + + // Add a TimelockEncrypted instance + data.push(Data::TimelockEncrypted { + encrypted: vec![0u8; 64].try_into().unwrap(), + reveal_round: 12345, + }); + + for d in data.iter() { + check_type_info(d); + } } -} -pub struct TestCanCommit; -impl pallet_commitments::CanCommit for TestCanCommit { - fn can_commit(_netuid: u16, _who: &u64) -> bool { - true + #[test] + fn set_commitment_works() { + new_test_ext().execute_with(|| { + System::::set_block_number(1); + let info = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![]).unwrap(), + ..Default::default() + }); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(1), + 1, + info.clone() + )); + + let commitment = Pallet::::commitment_of(1, &1).unwrap(); + let initial_deposit: u64 = ::InitialDeposit::get(); + assert_eq!(commitment.deposit, initial_deposit); + assert_eq!(commitment.block, 1); + assert_eq!(Pallet::::last_commitment(1, &1), Some(1)); + }); } -} -impl pallet_commitments::Config for Test { - type RuntimeEvent = RuntimeEvent; - type Currency = Balances; - type WeightInfo = (); - type MaxFields = TestMaxFields; - type CanCommit = TestCanCommit; - type FieldDeposit = ConstU64<0>; - type InitialDeposit = ConstU64<0>; - type DefaultRateLimit = ConstU64<0>; -} + #[test] + #[should_panic(expected = "BoundedVec::try_from failed")] + fn set_commitment_too_many_fields_panics() { + new_test_ext().execute_with(|| { + let max_fields: u32 = ::MaxFields::get(); + let fields = vec![Data::None; (max_fields + 1) as usize]; -impl pallet_drand::Config for Test { - type RuntimeEvent = RuntimeEvent; - type WeightInfo = pallet_drand::weights::SubstrateWeight; - type AuthorityId = test_crypto::TestAuthId; - type Verifier = pallet_drand::verifier::QuicknetVerifier; - type UnsignedPriority = ConstU64<{ 1 << 20 }>; - type HttpFetchTimeout = ConstU64<1_000>; -} + // This line will panic when 'BoundedVec::try_from(...)' sees too many items. + let info = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(fields).expect("BoundedVec::try_from failed"), + ..Default::default() + }); -pub mod test_crypto { - use sp_core::sr25519::{Public as Sr25519Public, Signature as Sr25519Signature}; - use sp_runtime::{ - app_crypto::{app_crypto, sr25519}, - traits::IdentifyAccount, - }; + // We never get here, because the constructor panics above. + let _ = + Pallet::::set_commitment(frame_system::RawOrigin::Signed(1).into(), 1, info); + }); + } - pub const KEY_TYPE: sp_runtime::KeyTypeId = sp_runtime::KeyTypeId(*b"test"); + #[test] + fn set_commitment_rate_limit_exceeded() { + new_test_ext().execute_with(|| { + let rate_limit = ::DefaultRateLimit::get(); + System::::set_block_number(1); + let info = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![]).unwrap(), + ..Default::default() + }); - app_crypto!(sr25519, KEY_TYPE); + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(1), + 1, + info.clone() + )); - pub struct TestAuthId; + // Set block number to just before rate limit expires + System::::set_block_number(rate_limit); + assert_noop!( + Pallet::::set_commitment(RuntimeOrigin::signed(1), 1, info.clone()), + Error::::CommitmentSetRateLimitExceeded + ); - impl frame_system::offchain::AppCrypto for TestAuthId { - type RuntimeAppPublic = Public; - type GenericSignature = Sr25519Signature; - type GenericPublic = Sr25519Public; + // Set block number to after rate limit + System::::set_block_number(rate_limit + 1); + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(1), + 1, + info + )); + }); } - impl IdentifyAccount for Public { - type AccountId = u64; + #[test] + fn set_commitment_updates_deposit() { + new_test_ext().execute_with(|| { + System::::set_block_number(1); + let info1 = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![Default::default(); 2]).unwrap(), + ..Default::default() + }); + let info2 = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![Default::default(); 3]).unwrap(), + ..Default::default() + }); - fn into_account(self) -> u64 { - let mut bytes = [0u8; 32]; - bytes.copy_from_slice(self.as_ref()); - u64::from_le_bytes(bytes[..8].try_into().unwrap()) - } + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(1), + 1, + info1 + )); + let initial_deposit: u64 = ::InitialDeposit::get(); + let field_deposit: u64 = ::FieldDeposit::get(); + let expected_deposit1: u64 = initial_deposit + 2u64 * field_deposit; + assert_eq!( + Pallet::::commitment_of(1, &1).unwrap().deposit, + expected_deposit1 + ); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(1), + 1, + info2 + )); + let expected_deposit2: u64 = initial_deposit + 3u64 * field_deposit; + assert_eq!( + Pallet::::commitment_of(1, &1).unwrap().deposit, + expected_deposit2 + ); + }); } -} -impl frame_system::offchain::SigningTypes for Test { - type Public = test_crypto::Public; - type Signature = test_crypto::Signature; -} + #[test] + fn set_rate_limit_works() { + new_test_ext().execute_with(|| { + let default_rate_limit: u64 = ::DefaultRateLimit::get(); + assert_eq!(RateLimit::::get(), default_rate_limit); + + assert_ok!(Pallet::::set_rate_limit(RuntimeOrigin::root(), 200)); + assert_eq!(RateLimit::::get(), 200); -impl frame_system::offchain::CreateSignedTransaction> for Test { - fn create_transaction>( - call: RuntimeCall, - _public: Self::Public, - account: Self::AccountId, - _nonce: u32, - ) -> Option<( - RuntimeCall, - ::SignaturePayload, - )> { - // Create a dummy sr25519 signature from a raw byte array - let dummy_raw = [0u8; 64]; - let dummy_signature = sp_core::sr25519::Signature::from(dummy_raw); - let signature = test_crypto::Signature::from(dummy_signature); - Some((call, (account, signature, ()))) + assert_noop!( + Pallet::::set_rate_limit(RuntimeOrigin::signed(1), 300), + sp_runtime::DispatchError::BadOrigin + ); + }); } -} -impl frame_system::offchain::SendTransactionTypes for Test -where - RuntimeCall: From, -{ - type Extrinsic = UncheckedExtrinsic; - type OverarchingCall = RuntimeCall; -} + #[test] + fn event_emission_works() { + new_test_ext().execute_with(|| { + System::::set_block_number(1); + let info = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![]).unwrap(), + ..Default::default() + }); -pub fn new_test_ext() -> sp_io::TestExternalities { - let t = frame_system::GenesisConfig::::default() - .build_storage() - .unwrap(); - let mut ext = sp_io::TestExternalities::new(t); - ext.execute_with(|| System::set_block_number(1)); - ext + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(1), + 1, + info + )); + + let events = System::::events(); + assert!(events.iter().any(|e| matches!( + &e.event, + RuntimeEvent::Commitments(Event::Commitment { netuid: 1, who: 1 }) + ))); + }); + } } diff --git a/pallets/commitments/src/types.rs b/pallets/commitments/src/types.rs index 8a43394aa..c59c7212e 100644 --- a/pallets/commitments/src/types.rs +++ b/pallets/commitments/src/types.rs @@ -395,257 +395,3 @@ impl< }) } } - -#[cfg(test)] -#[allow(clippy::indexing_slicing, clippy::unwrap_used)] -mod tests { - use super::*; - use crate::{ - Config, Error, Event, Pallet, RateLimit, - tests::{RuntimeEvent, RuntimeOrigin, Test, new_test_ext}, - }; - use frame_support::{BoundedVec, assert_noop, assert_ok}; - use frame_system::Pallet as System; - - #[test] - fn manual_data_type_info() { - let mut registry = scale_info::Registry::new(); - let type_id = registry.register_type(&scale_info::meta_type::()); - let registry: scale_info::PortableRegistry = registry.into(); - let type_info = registry.resolve(type_id.id).unwrap(); - - let check_type_info = |data: &Data| { - let variant_name = match data { - Data::None => "None".to_string(), - Data::BlakeTwo256(_) => "BlakeTwo256".to_string(), - Data::Sha256(_) => "Sha256".to_string(), - Data::Keccak256(_) => "Keccak256".to_string(), - Data::ShaThree256(_) => "ShaThree256".to_string(), - Data::Raw(bytes) => format!("Raw{}", bytes.len()), - Data::TimelockEncrypted { .. } => "TimelockEncrypted".to_string(), - }; - if let scale_info::TypeDef::Variant(variant) = &type_info.type_def { - let variant = variant - .variants - .iter() - .find(|v| v.name == variant_name) - .unwrap_or_else(|| panic!("Expected to find variant {}", variant_name)); - - let encoded = data.encode(); - assert_eq!(encoded[0], variant.index); - - // For variants with fields, check the encoded length matches expected field lengths - if !variant.fields.is_empty() { - let expected_len = match data { - Data::None => 0, - Data::Raw(bytes) => bytes.len() as u32, - Data::BlakeTwo256(_) - | Data::Sha256(_) - | Data::Keccak256(_) - | Data::ShaThree256(_) => 32, - Data::TimelockEncrypted { - encrypted, - reveal_round, - } => { - // Calculate length: encrypted (length prefixed) + reveal_round (u64) - let encrypted_len = encrypted.encode().len() as u32; // Includes length prefix - let reveal_round_len = reveal_round.encode().len() as u32; // Typically 8 bytes - encrypted_len + reveal_round_len - } - }; - assert_eq!( - encoded.len() as u32 - 1, // Subtract variant byte - expected_len, - "Encoded length mismatch for variant {}", - variant_name - ); - } else { - assert_eq!( - encoded.len() as u32 - 1, - 0, - "Expected no fields for {}", - variant_name - ); - } - } else { - panic!("Should be a variant type"); - } - }; - - let mut data = vec![ - Data::None, - Data::BlakeTwo256(Default::default()), - Data::Sha256(Default::default()), - Data::Keccak256(Default::default()), - Data::ShaThree256(Default::default()), - ]; - - // Add Raw instances for all possible sizes - for n in 0..128 { - data.push(Data::Raw(vec![0u8; n as usize].try_into().unwrap())); - } - - // Add a TimelockEncrypted instance - data.push(Data::TimelockEncrypted { - encrypted: vec![0u8; 64].try_into().unwrap(), - reveal_round: 12345, - }); - - for d in data.iter() { - check_type_info(d); - } - } - - #[test] - fn set_commitment_works() { - new_test_ext().execute_with(|| { - System::::set_block_number(1); - let info = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![]).unwrap(), - ..Default::default() - }); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(1), - 1, - info.clone() - )); - - let commitment = Pallet::::commitment_of(1, &1).unwrap(); - let initial_deposit: u64 = ::InitialDeposit::get(); - assert_eq!(commitment.deposit, initial_deposit); - assert_eq!(commitment.block, 1); - assert_eq!(Pallet::::last_commitment(1, &1), Some(1)); - }); - } - - #[test] - #[should_panic(expected = "BoundedVec::try_from failed")] - fn set_commitment_too_many_fields_panics() { - new_test_ext().execute_with(|| { - let max_fields: u32 = ::MaxFields::get(); - let fields = vec![Data::None; (max_fields + 1) as usize]; - - // This line will panic when 'BoundedVec::try_from(...)' sees too many items. - let info = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(fields).expect("BoundedVec::try_from failed"), - ..Default::default() - }); - - // We never get here, because the constructor panics above. - let _ = - Pallet::::set_commitment(frame_system::RawOrigin::Signed(1).into(), 1, info); - }); - } - - #[test] - fn set_commitment_rate_limit_exceeded() { - new_test_ext().execute_with(|| { - let rate_limit = ::DefaultRateLimit::get(); - System::::set_block_number(1); - let info = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![]).unwrap(), - ..Default::default() - }); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(1), - 1, - info.clone() - )); - - // Set block number to just before rate limit expires - System::::set_block_number(rate_limit); - assert_noop!( - Pallet::::set_commitment(RuntimeOrigin::signed(1), 1, info.clone()), - Error::::CommitmentSetRateLimitExceeded - ); - - // Set block number to after rate limit - System::::set_block_number(rate_limit + 1); - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(1), - 1, - info - )); - }); - } - - #[test] - fn set_commitment_updates_deposit() { - new_test_ext().execute_with(|| { - System::::set_block_number(1); - let info1 = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![Default::default(); 2]).unwrap(), - ..Default::default() - }); - let info2 = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![Default::default(); 3]).unwrap(), - ..Default::default() - }); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(1), - 1, - info1 - )); - let initial_deposit: u64 = ::InitialDeposit::get(); - let field_deposit: u64 = ::FieldDeposit::get(); - let expected_deposit1: u64 = initial_deposit + 2u64 * field_deposit; - assert_eq!( - Pallet::::commitment_of(1, &1).unwrap().deposit, - expected_deposit1 - ); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(1), - 1, - info2 - )); - let expected_deposit2: u64 = initial_deposit + 3u64 * field_deposit; - assert_eq!( - Pallet::::commitment_of(1, &1).unwrap().deposit, - expected_deposit2 - ); - }); - } - - #[test] - fn set_rate_limit_works() { - new_test_ext().execute_with(|| { - let default_rate_limit: u64 = ::DefaultRateLimit::get(); - assert_eq!(RateLimit::::get(), default_rate_limit); - - assert_ok!(Pallet::::set_rate_limit(RuntimeOrigin::root(), 200)); - assert_eq!(RateLimit::::get(), 200); - - assert_noop!( - Pallet::::set_rate_limit(RuntimeOrigin::signed(1), 300), - sp_runtime::DispatchError::BadOrigin - ); - }); - } - - #[test] - fn event_emission_works() { - new_test_ext().execute_with(|| { - System::::set_block_number(1); - let info = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![]).unwrap(), - ..Default::default() - }); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(1), - 1, - info - )); - - let events = System::::events(); - assert!(events.iter().any(|e| matches!( - &e.event, - RuntimeEvent::Commitments(Event::Commitment { netuid: 1, who: 1 }) - ))); - }); - } -} From 423fe2c17c982d7f258d5027a4f6b1f7bf7ee718 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Mon, 3 Mar 2025 15:06:45 -0800 Subject: [PATCH 08/28] tlock commitments unit tests --- Cargo.lock | 3 + pallets/commitments/Cargo.toml | 4 + pallets/commitments/src/lib.rs | 179 +++++++++++------------- pallets/commitments/src/mock.rs | 76 +++++++++++ pallets/commitments/src/tests.rs | 224 ++++++++++++++++++++++++++++++- 5 files changed, 380 insertions(+), 106 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cf2492051..b65fb0308 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6093,11 +6093,14 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", + "hex", "log", "pallet-balances", "pallet-drand", "parity-scale-codec", + "rand_chacha", "scale-info", + "sha2 0.10.8", "sp-core", "sp-io", "sp-runtime", diff --git a/pallets/commitments/Cargo.toml b/pallets/commitments/Cargo.toml index f8cb5e2cf..c4507d44e 100644 --- a/pallets/commitments/Cargo.toml +++ b/pallets/commitments/Cargo.toml @@ -33,6 +33,10 @@ pallet-drand = { path = "../drand", default-features = false } tle = { workspace = true, default-features = false } ark-serialize = { workspace = true, default-features = false } w3f-bls = { workspace = true, default-features = false } +rand_chacha = { workspace = true } +hex = { workspace = true } +sha2 = { workspace = true } + log = { workspace = true } [dev-dependencies] diff --git a/pallets/commitments/src/lib.rs b/pallets/commitments/src/lib.rs index 77cc06ddb..745e06b7c 100644 --- a/pallets/commitments/src/lib.rs +++ b/pallets/commitments/src/lib.rs @@ -17,7 +17,6 @@ pub use weights::WeightInfo; use ark_serialize::CanonicalDeserialize; use frame_support::{BoundedVec, traits::Currency}; -use frame_system::pallet_prelude::BlockNumberFor; use sp_runtime::{Saturating, traits::Zero}; use sp_std::{boxed::Box, vec::Vec}; use tle::{ @@ -398,123 +397,101 @@ impl Pallet { .iter() .find(|data| matches!(data, Data::TimelockEncrypted { .. })) { - // Calculate reveal block - let reveal_block = Self::calculate_reveal_block(*reveal_round, registration.block)?; - - // Check if the current block has reached or exceeded the reveal block - if current_block >= reveal_block { - // Deserialize the encrypted commitment into a TLECiphertext - let reader = &mut &encrypted[..]; - let commit = TLECiphertext::::deserialize_compressed(reader) + // Check if the corresponding Drand round data exists + let pulse = match pallet_drand::Pulses::::get(*reveal_round) { + Some(p) => p, + None => continue, + }; + + // Prepare the signature bytes + let signature_bytes = pulse + .signature + .strip_prefix(b"0x") + .unwrap_or(&pulse.signature); + let sig_reader = &mut &signature_bytes[..]; + let sig = + ::SignatureGroup::deserialize_compressed(sig_reader) .map_err(|e| { - log::warn!("Failed to deserialize TLECiphertext for {:?}: {:?}", who, e) + log::warn!( + "Failed to deserialize drand signature for {:?}: {:?}", + who, + e + ) }) .ok(); - let commit = match commit { - Some(c) => c, - None => continue, - }; + let sig = match sig { + Some(s) => s, + None => continue, + }; - // Get the drand pulse for the reveal round - let pulse = match pallet_drand::Pulses::::get(*reveal_round) { - Some(p) => p, - None => { - log::warn!( - "Failed to reveal commit for subnet {} by {:?}: missing drand round {}", - netuid, - who, - reveal_round - ); - continue; - } - }; - - // Prepare the signature bytes - let signature_bytes = pulse - .signature - .strip_prefix(b"0x") - .unwrap_or(&pulse.signature); - let sig_reader = &mut &signature_bytes[..]; - let sig = ::SignatureGroup::deserialize_compressed( - sig_reader, - ) + // Attempt to deserialize the encrypted commitment + let reader = &mut &encrypted[..]; + let commit = TLECiphertext::::deserialize_compressed(reader) .map_err(|e| { - log::warn!( - "Failed to deserialize drand signature for {:?}: {:?}", - who, - e - ) + log::warn!("Failed to deserialize TLECiphertext for {:?}: {:?}", who, e) }) .ok(); - let sig = match sig { - Some(s) => s, - None => continue, - }; - - // Decrypt the timelock commitment - let decrypted_bytes: Vec = - tld::(commit, sig) - .map_err(|e| { - log::warn!("Failed to decrypt timelock for {:?}: {:?}", who, e) - }) - .ok() - .unwrap_or_default(); - - if decrypted_bytes.is_empty() { - continue; - } + let commit = match commit { + Some(c) => c, + None => continue, + }; - // Decode the decrypted bytes into CommitmentInfo (assuming it’s SCALE-encoded CommitmentInfo) - let mut reader = &decrypted_bytes[..]; - let revealed_info: CommitmentInfo = Decode::decode(&mut reader) + // Decrypt the timelock commitment + let decrypted_bytes: Vec = + tld::(commit, sig) .map_err(|e| { - log::warn!("Failed to decode decrypted data for {:?}: {:?}", who, e) + log::warn!("Failed to decrypt timelock for {:?}: {:?}", who, e) }) .ok() - .unwrap_or_else(|| CommitmentInfo { - fields: BoundedVec::default(), - }); - - // Create RevealedData for storage - let revealed_data = RevealedData { - info: revealed_info, - revealed_block: current_block, - deposit: registration.deposit, - }; - - // Store the revealed data in RevealedCommitments - >::insert(netuid, &who, revealed_data); - - // Remove the TimelockEncrypted field from the original commitment - let filtered_fields: Vec = registration.info.fields.into_iter() - .filter(|data| !matches!(data, Data::TimelockEncrypted { reveal_round: r, .. } if r == reveal_round)) - .collect(); - registration.info.fields = BoundedVec::try_from(filtered_fields) - .map_err(|_| "Failed to filter timelock fields")?; - - Self::deposit_event(Event::CommitmentRevealed { netuid, who }); + .unwrap_or_default(); + + if decrypted_bytes.is_empty() { + continue; } + + // Decode the decrypted bytes into CommitmentInfo + let mut reader = &decrypted_bytes[..]; + let revealed_info: CommitmentInfo = match Decode::decode(&mut reader) + { + Ok(info) => info, + Err(e) => { + log::warn!("Failed to decode decrypted data for {:?}: {:?}", who, e); + continue; + } + }; + + // Store the revealed data + let revealed_data = RevealedData { + info: revealed_info, + revealed_block: current_block, + deposit: registration.deposit, + }; + >::insert(netuid, &who, revealed_data); + + // Remove the TimelockEncrypted field from the original commitment + let filtered_fields: Vec = registration + .info + .fields + .into_iter() + .filter(|data| { + !matches!( + data, + Data::TimelockEncrypted { + reveal_round: r, .. + } if r == reveal_round + ) + }) + .collect(); + + registration.info.fields = BoundedVec::try_from(filtered_fields) + .map_err(|_| "Failed to filter timelock fields")?; + + Self::deposit_event(Event::CommitmentRevealed { netuid, who }); } } Ok(()) } - - fn calculate_reveal_block( - reveal_round: u64, - commit_block: BlockNumberFor, - ) -> Result, &'static str> { - let last_drand_round = pallet_drand::LastStoredRound::::get(); - let blocks_per_round = 12_u64.checked_div(3).unwrap_or(0); // 4 blocks per round (12s blocktime / 3s round) - let rounds_since_last = reveal_round.saturating_sub(last_drand_round); - let blocks_to_reveal = rounds_since_last.saturating_mul(blocks_per_round); - let reveal_block = commit_block.saturating_add( - blocks_to_reveal - .try_into() - .map_err(|_| "Block number conversion failed")?, - ); - Ok(reveal_block) - } } diff --git a/pallets/commitments/src/mock.rs b/pallets/commitments/src/mock.rs index b77c10548..6c3886896 100644 --- a/pallets/commitments/src/mock.rs +++ b/pallets/commitments/src/mock.rs @@ -181,3 +181,79 @@ pub fn new_test_ext() -> sp_io::TestExternalities { ext.execute_with(|| System::set_block_number(1)); ext } + +use super::*; +use crate::{EngineBLS, MAX_TIMELOCK_COMMITMENT_SIZE_BYTES, TinyBLS381}; +use ark_serialize::CanonicalSerialize; +use frame_support::BoundedVec; +use rand_chacha::{ChaCha20Rng, rand_core::SeedableRng}; +use sha2::Digest; +use tle::{ibe::fullident::Identity, stream_ciphers::AESGCMStreamCipherProvider, tlock::tle}; + +// Drand Quicknet public key and signature for round=1000: +pub const DRAND_QUICKNET_PUBKEY_HEX: &str = "83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6\ + a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809b\ + d274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a"; +pub const DRAND_QUICKNET_SIG_HEX: &str = "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39"; + +/// Inserts a Drand pulse for `round` with the given `signature_bytes`. +pub fn insert_drand_pulse(round: u64, signature_bytes: &[u8]) { + let sig_bounded: BoundedVec> = signature_bytes + .to_vec() + .try_into() + .expect("Signature within 144 bytes"); + + let randomness_bounded: BoundedVec> = vec![0u8; 32] + .try_into() + .expect("Randomness must be exactly 32 bytes"); + + pallet_drand::Pulses::::insert( + round, + pallet_drand::types::Pulse { + round, + randomness: randomness_bounded, + signature: sig_bounded, + }, + ); +} + +/// Produces a **real** ciphertext by TLE-encrypting `plaintext` for Drand Quicknet `round`. +/// +/// The returned `BoundedVec>` +/// will decrypt if you pass in the valid signature for the same round. +pub fn produce_ciphertext( + plaintext: &[u8], + round: u64, +) -> BoundedVec> { + // 1) Deserialize the known Drand Quicknet public key: + let pub_key_bytes = hex::decode(DRAND_QUICKNET_PUBKEY_HEX).expect("decode pubkey"); + let pub_key = + ::PublicKeyGroup::deserialize_compressed(&pub_key_bytes[..]) + .expect("bad pubkey bytes"); + + // 2) Prepare the identity for that round + // by hashing round.to_be_bytes() with SHA256: + let msg = { + let mut hasher = sha2::Sha256::new(); + hasher.update(round.to_be_bytes()); + hasher.finalize().to_vec() + }; + let identity = Identity::new(b"", vec![msg]); + + // 3) Actually encrypt + // (just an example ephemeral secret key & RNG seed) + let esk = [2u8; 32]; + let rng = ChaCha20Rng::seed_from_u64(0); + + let ct = tle::( + pub_key, esk, plaintext, identity, rng, + ) + .expect("Encryption failed in produce_real_ciphertext"); + + // 4) Serialize the ciphertext to BoundedVec + let mut ct_bytes = Vec::new(); + ct.serialize_compressed(&mut ct_bytes) + .expect("serialize TLECiphertext"); + + ct_bytes.try_into().expect("Ciphertext is within max size") +} diff --git a/pallets/commitments/src/tests.rs b/pallets/commitments/src/tests.rs index c6b8d93d6..fd1368324 100644 --- a/pallets/commitments/src/tests.rs +++ b/pallets/commitments/src/tests.rs @@ -1,6 +1,4 @@ -use crate::{CommitmentInfo, Data}; use codec::Encode; -use frame_support::traits::Get; use sp_std::prelude::*; #[cfg(test)] @@ -8,10 +6,13 @@ use sp_std::prelude::*; mod tests { use super::*; use crate::{ - Config, Error, Event, Pallet, RateLimit, - mock::{RuntimeEvent, RuntimeOrigin, Test, new_test_ext}, + CommitmentInfo, Config, Data, Error, Event, Pallet, RateLimit, RevealedCommitments, + mock::{ + DRAND_QUICKNET_SIG_HEX, RuntimeEvent, RuntimeOrigin, Test, insert_drand_pulse, + new_test_ext, produce_ciphertext, + }, }; - use frame_support::{BoundedVec, assert_noop, assert_ok}; + use frame_support::{BoundedVec, assert_noop, assert_ok, traits::Get}; use frame_system::Pallet as System; #[test] @@ -255,4 +256,217 @@ mod tests { ))); }); } + + #[test] + fn happy_path_timelock_commitments() { + new_test_ext().execute_with(|| { + let message_text = b"Hello timelock only!"; + let data_raw = Data::Raw( + message_text + .to_vec() + .try_into() + .expect("<= 128 bytes for Raw variant"), + ); + let fields_vec = vec![data_raw]; + let fields_bounded: BoundedVec::MaxFields> = + BoundedVec::try_from(fields_vec).expect("Too many fields"); + + let inner_info: CommitmentInfo<::MaxFields> = CommitmentInfo { + fields: fields_bounded, + }; + + let plaintext = inner_info.encode(); + + let reveal_round = 1000; + let encrypted = produce_ciphertext(&plaintext, reveal_round); + + let data = Data::TimelockEncrypted { + encrypted: encrypted.clone(), + reveal_round, + }; + + let fields_outer: BoundedVec::MaxFields> = + BoundedVec::try_from(vec![data]).expect("Too many fields"); + let info_outer = CommitmentInfo { + fields: fields_outer, + }; + + let who = 123; + let netuid = 42; + System::::set_block_number(1); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + Box::new(info_outer) + )); + + let drand_signature_bytes = hex::decode(DRAND_QUICKNET_SIG_HEX).unwrap(); + insert_drand_pulse(reveal_round, &drand_signature_bytes); + + System::::set_block_number(9999); + assert_ok!(Pallet::::reveal_timelocked_commitments(9999)); + + let revealed = + RevealedCommitments::::get(netuid, &who).expect("Should have revealed data"); + + let revealed_inner = &revealed.info; + assert_eq!(revealed_inner.fields.len(), 1); + match &revealed_inner.fields[0] { + Data::Raw(bounded_bytes) => { + assert_eq!( + bounded_bytes.as_slice(), + message_text, + "Decrypted text from on-chain storage must match the original message" + ); + } + other => panic!("Expected Data::Raw(...) in revealed, got {:?}", other), + } + }); + } + + #[test] + fn reveal_timelocked_commitment_missing_round_does_nothing() { + new_test_ext().execute_with(|| { + let who = 1; + let netuid = 2; + System::::set_block_number(5); + let ciphertext = produce_ciphertext(b"My plaintext", 1000); + let data = Data::TimelockEncrypted { + encrypted: ciphertext, + reveal_round: 1000, + }; + let fields: BoundedVec<_, ::MaxFields> = + BoundedVec::try_from(vec![data]).unwrap(); + let info = CommitmentInfo { fields }; + let origin = RuntimeOrigin::signed(who); + assert_ok!(Pallet::::set_commitment( + origin, + netuid, + Box::new(info) + )); + System::::set_block_number(100_000); + assert_ok!(Pallet::::reveal_timelocked_commitments(100_000)); + assert!(RevealedCommitments::::get(netuid, &who).is_none()); + }); + } + + #[test] + fn reveal_timelocked_commitment_cant_deserialize_ciphertext() { + new_test_ext().execute_with(|| { + let who = 42; + let netuid = 9; + System::::set_block_number(10); + let good_ct = produce_ciphertext(b"Some data", 1000); + let mut corrupted = good_ct.into_inner(); + if !corrupted.is_empty() { + corrupted[0] = 0xFF; + } + let corrupted_ct = BoundedVec::try_from(corrupted).unwrap(); + let data = Data::TimelockEncrypted { + encrypted: corrupted_ct, + reveal_round: 1000, + }; + let fields = BoundedVec::try_from(vec![data]).unwrap(); + let info = CommitmentInfo { fields }; + let origin = RuntimeOrigin::signed(who); + assert_ok!(Pallet::::set_commitment( + origin, + netuid, + Box::new(info) + )); + let sig_bytes = hex::decode(DRAND_QUICKNET_SIG_HEX).unwrap(); + insert_drand_pulse(1000, &sig_bytes); + System::::set_block_number(99999); + assert_ok!(Pallet::::reveal_timelocked_commitments(99999)); + assert!(RevealedCommitments::::get(netuid, &who).is_none()); + }); + } + + #[test] + fn reveal_timelocked_commitment_bad_signature_skips_decryption() { + new_test_ext().execute_with(|| { + let who = 10; + let netuid = 11; + System::::set_block_number(15); + let real_ct = produce_ciphertext(b"A valid plaintext", 1000); + let data = Data::TimelockEncrypted { + encrypted: real_ct, + reveal_round: 1000, + }; + let fields: BoundedVec<_, ::MaxFields> = + BoundedVec::try_from(vec![data]).unwrap(); + let info = CommitmentInfo { fields }; + let origin = RuntimeOrigin::signed(who); + assert_ok!(Pallet::::set_commitment( + origin, + netuid, + Box::new(info) + )); + let bad_signature = [0x33u8; 10]; + insert_drand_pulse(1000, &bad_signature); + System::::set_block_number(10_000); + assert_ok!(Pallet::::reveal_timelocked_commitments(10_000)); + assert!(RevealedCommitments::::get(netuid, &who).is_none()); + }); + } + + #[test] + fn reveal_timelocked_commitment_empty_decrypted_data_is_skipped() { + new_test_ext().execute_with(|| { + let who = 2; + let netuid = 3; + let commit_block = 100u64; + System::::set_block_number(commit_block); + let reveal_round = 1000; + let empty_ct = produce_ciphertext(&[], reveal_round); + let data = Data::TimelockEncrypted { + encrypted: empty_ct, + reveal_round, + }; + let fields = BoundedVec::try_from(vec![data]).unwrap(); + let info = CommitmentInfo { fields }; + let origin = RuntimeOrigin::signed(who); + assert_ok!(Pallet::::set_commitment( + origin, + netuid, + Box::new(info) + )); + let sig_bytes = hex::decode(DRAND_QUICKNET_SIG_HEX).unwrap(); + insert_drand_pulse(reveal_round, &sig_bytes); + System::::set_block_number(10_000); + assert_ok!(Pallet::::reveal_timelocked_commitments(10_000)); + assert!(RevealedCommitments::::get(netuid, &who).is_none()); + }); + } + + #[test] + fn reveal_timelocked_commitment_decode_failure_is_skipped() { + new_test_ext().execute_with(|| { + let who = 999; + let netuid = 8; + let commit_block = 42u64; + System::::set_block_number(commit_block); + let plaintext = vec![0xAA, 0xBB, 0xCC, 0xDD, 0xEE]; + let reveal_round = 1000; + let real_ct = produce_ciphertext(&plaintext, reveal_round); + let data = Data::TimelockEncrypted { + encrypted: real_ct, + reveal_round, + }; + let fields = BoundedVec::try_from(vec![data]).unwrap(); + let info = CommitmentInfo { fields }; + let origin = RuntimeOrigin::signed(who); + assert_ok!(Pallet::::set_commitment( + origin, + netuid, + Box::new(info) + )); + let sig_bytes = hex::decode(DRAND_QUICKNET_SIG_HEX.as_bytes()).unwrap(); + insert_drand_pulse(reveal_round, &sig_bytes); + System::::set_block_number(9999); + assert_ok!(Pallet::::reveal_timelocked_commitments(9999)); + assert!(RevealedCommitments::::get(netuid, &who).is_none()); + }); + } } From 3c8d3de212a160b9a24c2f83028b7216d1fefb2d Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Mon, 3 Mar 2025 15:18:35 -0800 Subject: [PATCH 09/28] zepter --- pallets/commitments/Cargo.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pallets/commitments/Cargo.toml b/pallets/commitments/Cargo.toml index c4507d44e..fb0091deb 100644 --- a/pallets/commitments/Cargo.toml +++ b/pallets/commitments/Cargo.toml @@ -62,7 +62,10 @@ std = [ "log/std", "pallet-drand/std", "tle/std", - "w3f-bls/std" + "w3f-bls/std", + "hex/std", + "rand_chacha/std", + "sha2/std" ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", From 4a2b2986e93f0b9b9b39b6f61fc618e5d61a00e5 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Mon, 3 Mar 2025 15:42:08 -0800 Subject: [PATCH 10/28] address lints --- pallets/admin-utils/src/tests/mock.rs | 1 + pallets/commitments/src/mock.rs | 4 +- pallets/commitments/src/tests.rs | 886 +++++++++++++------------- pallets/commitments/src/types.rs | 1 + pallets/subtensor/src/tests/mock.rs | 1 + runtime/src/lib.rs | 2 + 6 files changed, 452 insertions(+), 443 deletions(-) diff --git a/pallets/admin-utils/src/tests/mock.rs b/pallets/admin-utils/src/tests/mock.rs index aa7d8cd1d..47565d999 100644 --- a/pallets/admin-utils/src/tests/mock.rs +++ b/pallets/admin-utils/src/tests/mock.rs @@ -386,6 +386,7 @@ parameter_types! { pub const CommitmentRateLimit: BlockNumber = 100; } +#[subtensor_macros::freeze_struct("7c76bd954afbb54e")] #[derive(Clone, Eq, PartialEq, Encode, Decode, TypeInfo)] pub struct MaxCommitFields; impl Get for MaxCommitFields { diff --git a/pallets/commitments/src/mock.rs b/pallets/commitments/src/mock.rs index 6c3886896..5a4b3cace 100644 --- a/pallets/commitments/src/mock.rs +++ b/pallets/commitments/src/mock.rs @@ -137,7 +137,7 @@ pub mod test_crypto { fn into_account(self) -> u64 { let mut bytes = [0u8; 32]; bytes.copy_from_slice(self.as_ref()); - u64::from_le_bytes(bytes[..8].try_into().unwrap()) + u64::from_le_bytes(bytes[..8].try_into().expect("Expected to not panic")) } } } @@ -176,7 +176,7 @@ where pub fn new_test_ext() -> sp_io::TestExternalities { let t = frame_system::GenesisConfig::::default() .build_storage() - .unwrap(); + .expect("Expected to not panic"); let mut ext = sp_io::TestExternalities::new(t); ext.execute_with(|| System::set_block_number(1)); ext diff --git a/pallets/commitments/src/tests.rs b/pallets/commitments/src/tests.rs index fd1368324..c40c0b042 100644 --- a/pallets/commitments/src/tests.rs +++ b/pallets/commitments/src/tests.rs @@ -2,471 +2,475 @@ use codec::Encode; use sp_std::prelude::*; #[cfg(test)] -#[allow(clippy::indexing_slicing, clippy::unwrap_used)] -mod tests { - use super::*; - use crate::{ - CommitmentInfo, Config, Data, Error, Event, Pallet, RateLimit, RevealedCommitments, - mock::{ - DRAND_QUICKNET_SIG_HEX, RuntimeEvent, RuntimeOrigin, Test, insert_drand_pulse, - new_test_ext, produce_ciphertext, - }, - }; - use frame_support::{BoundedVec, assert_noop, assert_ok, traits::Get}; - use frame_system::Pallet as System; - - #[test] - fn manual_data_type_info() { - let mut registry = scale_info::Registry::new(); - let type_id = registry.register_type(&scale_info::meta_type::()); - let registry: scale_info::PortableRegistry = registry.into(); - let type_info = registry.resolve(type_id.id).unwrap(); - - let check_type_info = |data: &Data| { - let variant_name = match data { - Data::None => "None".to_string(), - Data::BlakeTwo256(_) => "BlakeTwo256".to_string(), - Data::Sha256(_) => "Sha256".to_string(), - Data::Keccak256(_) => "Keccak256".to_string(), - Data::ShaThree256(_) => "ShaThree256".to_string(), - Data::Raw(bytes) => format!("Raw{}", bytes.len()), - Data::TimelockEncrypted { .. } => "TimelockEncrypted".to_string(), - }; - if let scale_info::TypeDef::Variant(variant) = &type_info.type_def { - let variant = variant - .variants - .iter() - .find(|v| v.name == variant_name) - .unwrap_or_else(|| panic!("Expected to find variant {}", variant_name)); - - let encoded = data.encode(); - assert_eq!(encoded[0], variant.index); - - // For variants with fields, check the encoded length matches expected field lengths - if !variant.fields.is_empty() { - let expected_len = match data { - Data::None => 0, - Data::Raw(bytes) => bytes.len() as u32, - Data::BlakeTwo256(_) - | Data::Sha256(_) - | Data::Keccak256(_) - | Data::ShaThree256(_) => 32, - Data::TimelockEncrypted { - encrypted, - reveal_round, - } => { - // Calculate length: encrypted (length prefixed) + reveal_round (u64) - let encrypted_len = encrypted.encode().len() as u32; // Includes length prefix - let reveal_round_len = reveal_round.encode().len() as u32; // Typically 8 bytes - encrypted_len + reveal_round_len - } - }; - assert_eq!( - encoded.len() as u32 - 1, // Subtract variant byte - expected_len, - "Encoded length mismatch for variant {}", - variant_name - ); - } else { - assert_eq!( - encoded.len() as u32 - 1, - 0, - "Expected no fields for {}", - variant_name - ); - } +use crate::{ + CommitmentInfo, Config, Data, Error, Event, Pallet, RateLimit, RevealedCommitments, + mock::{ + DRAND_QUICKNET_SIG_HEX, RuntimeEvent, RuntimeOrigin, Test, insert_drand_pulse, + new_test_ext, produce_ciphertext, + }, +}; +use frame_support::{BoundedVec, assert_noop, assert_ok, traits::Get}; +use frame_system::Pallet as System; + +#[allow(clippy::indexing_slicing)] +#[test] +fn manual_data_type_info() { + let mut registry = scale_info::Registry::new(); + let type_id = registry.register_type(&scale_info::meta_type::()); + let registry: scale_info::PortableRegistry = registry.into(); + let type_info = registry.resolve(type_id.id).expect("Expected not to panic"); + + let check_type_info = |data: &Data| { + let variant_name = match data { + Data::None => "None".to_string(), + Data::BlakeTwo256(_) => "BlakeTwo256".to_string(), + Data::Sha256(_) => "Sha256".to_string(), + Data::Keccak256(_) => "Keccak256".to_string(), + Data::ShaThree256(_) => "ShaThree256".to_string(), + Data::Raw(bytes) => format!("Raw{}", bytes.len()), + Data::TimelockEncrypted { .. } => "TimelockEncrypted".to_string(), + }; + if let scale_info::TypeDef::Variant(variant) = &type_info.type_def { + let variant = variant + .variants + .iter() + .find(|v| v.name == variant_name) + .unwrap_or_else(|| panic!("Expected to find variant {}", variant_name)); + + let encoded = data.encode(); + assert_eq!(encoded[0], variant.index); + + // For variants with fields, check the encoded length matches expected field lengths + if !variant.fields.is_empty() { + let expected_len = match data { + Data::None => 0, + Data::Raw(bytes) => bytes.len() as u32, + Data::BlakeTwo256(_) + | Data::Sha256(_) + | Data::Keccak256(_) + | Data::ShaThree256(_) => 32, + Data::TimelockEncrypted { + encrypted, + reveal_round, + } => { + // Calculate length: encrypted (length prefixed) + reveal_round (u64) + let encrypted_len = encrypted.encode().len() as u32; // Includes length prefix + let reveal_round_len = reveal_round.encode().len() as u32; // Typically 8 bytes + encrypted_len + reveal_round_len + } + }; + assert_eq!( + encoded.len() as u32 - 1, // Subtract variant byte + expected_len, + "Encoded length mismatch for variant {}", + variant_name + ); } else { - panic!("Should be a variant type"); + assert_eq!( + encoded.len() as u32 - 1, + 0, + "Expected no fields for {}", + variant_name + ); } - }; - - let mut data = vec![ - Data::None, - Data::BlakeTwo256(Default::default()), - Data::Sha256(Default::default()), - Data::Keccak256(Default::default()), - Data::ShaThree256(Default::default()), - ]; - - // Add Raw instances for all possible sizes - for n in 0..128 { - data.push(Data::Raw(vec![0u8; n as usize].try_into().unwrap())); + } else { + panic!("Should be a variant type"); } + }; - // Add a TimelockEncrypted instance - data.push(Data::TimelockEncrypted { - encrypted: vec![0u8; 64].try_into().unwrap(), - reveal_round: 12345, - }); + let mut data = vec![ + Data::None, + Data::BlakeTwo256(Default::default()), + Data::Sha256(Default::default()), + Data::Keccak256(Default::default()), + Data::ShaThree256(Default::default()), + ]; + + // Add Raw instances for all possible sizes + for n in 0..128 { + data.push(Data::Raw( + vec![0u8; n as usize] + .try_into() + .expect("Expected not to panic"), + )); + } - for d in data.iter() { - check_type_info(d); - } + // Add a TimelockEncrypted instance + data.push(Data::TimelockEncrypted { + encrypted: vec![0u8; 64].try_into().expect("Expected not to panic"), + reveal_round: 12345, + }); + + for d in data.iter() { + check_type_info(d); } +} - #[test] - fn set_commitment_works() { - new_test_ext().execute_with(|| { - System::::set_block_number(1); - let info = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![]).unwrap(), - ..Default::default() - }); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(1), - 1, - info.clone() - )); - - let commitment = Pallet::::commitment_of(1, &1).unwrap(); - let initial_deposit: u64 = ::InitialDeposit::get(); - assert_eq!(commitment.deposit, initial_deposit); - assert_eq!(commitment.block, 1); - assert_eq!(Pallet::::last_commitment(1, &1), Some(1)); +#[test] +fn set_commitment_works() { + new_test_ext().execute_with(|| { + System::::set_block_number(1); + let info = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![]).expect("Expected not to panic"), }); - } - #[test] - #[should_panic(expected = "BoundedVec::try_from failed")] - fn set_commitment_too_many_fields_panics() { - new_test_ext().execute_with(|| { - let max_fields: u32 = ::MaxFields::get(); - let fields = vec![Data::None; (max_fields + 1) as usize]; - - // This line will panic when 'BoundedVec::try_from(...)' sees too many items. - let info = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(fields).expect("BoundedVec::try_from failed"), - ..Default::default() - }); - - // We never get here, because the constructor panics above. - let _ = - Pallet::::set_commitment(frame_system::RawOrigin::Signed(1).into(), 1, info); + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(1), + 1, + info.clone() + )); + + let commitment = Pallet::::commitment_of(1, 1).expect("Expected not to panic"); + let initial_deposit: u64 = ::InitialDeposit::get(); + assert_eq!(commitment.deposit, initial_deposit); + assert_eq!(commitment.block, 1); + assert_eq!(Pallet::::last_commitment(1, 1), Some(1)); + }); +} + +#[test] +#[should_panic(expected = "BoundedVec::try_from failed")] +fn set_commitment_too_many_fields_panics() { + new_test_ext().execute_with(|| { + let max_fields: u32 = ::MaxFields::get(); + let fields = vec![Data::None; (max_fields + 1) as usize]; + + // This line will panic when 'BoundedVec::try_from(...)' sees too many items. + let info = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(fields).expect("BoundedVec::try_from failed"), }); - } - #[test] - fn set_commitment_rate_limit_exceeded() { - new_test_ext().execute_with(|| { - let rate_limit = ::DefaultRateLimit::get(); - System::::set_block_number(1); - let info = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![]).unwrap(), - ..Default::default() - }); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(1), - 1, - info.clone() - )); - - // Set block number to just before rate limit expires - System::::set_block_number(rate_limit); - assert_noop!( - Pallet::::set_commitment(RuntimeOrigin::signed(1), 1, info.clone()), - Error::::CommitmentSetRateLimitExceeded - ); - - // Set block number to after rate limit - System::::set_block_number(rate_limit + 1); - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(1), - 1, - info - )); + // We never get here, because the constructor panics above. + let _ = Pallet::::set_commitment(frame_system::RawOrigin::Signed(1).into(), 1, info); + }); +} + +#[test] +fn set_commitment_rate_limit_exceeded() { + new_test_ext().execute_with(|| { + let rate_limit = ::DefaultRateLimit::get(); + System::::set_block_number(1); + let info = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![]).expect("Expected not to panic"), }); - } - #[test] - fn set_commitment_updates_deposit() { - new_test_ext().execute_with(|| { - System::::set_block_number(1); - let info1 = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![Default::default(); 2]).unwrap(), - ..Default::default() - }); - let info2 = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![Default::default(); 3]).unwrap(), - ..Default::default() - }); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(1), - 1, - info1 - )); - let initial_deposit: u64 = ::InitialDeposit::get(); - let field_deposit: u64 = ::FieldDeposit::get(); - let expected_deposit1: u64 = initial_deposit + 2u64 * field_deposit; - assert_eq!( - Pallet::::commitment_of(1, &1).unwrap().deposit, - expected_deposit1 - ); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(1), - 1, - info2 - )); - let expected_deposit2: u64 = initial_deposit + 3u64 * field_deposit; - assert_eq!( - Pallet::::commitment_of(1, &1).unwrap().deposit, - expected_deposit2 - ); + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(1), + 1, + info.clone() + )); + + // Set block number to just before rate limit expires + System::::set_block_number(rate_limit); + assert_noop!( + Pallet::::set_commitment(RuntimeOrigin::signed(1), 1, info.clone()), + Error::::CommitmentSetRateLimitExceeded + ); + + // Set block number to after rate limit + System::::set_block_number(rate_limit + 1); + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(1), + 1, + info + )); + }); +} + +#[test] +fn set_commitment_updates_deposit() { + new_test_ext().execute_with(|| { + System::::set_block_number(1); + let info1 = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![Default::default(); 2]) + .expect("Expected not to panic"), + }); + let info2 = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![Default::default(); 3]) + .expect("Expected not to panic"), }); - } - #[test] - fn set_rate_limit_works() { - new_test_ext().execute_with(|| { - let default_rate_limit: u64 = ::DefaultRateLimit::get(); - assert_eq!(RateLimit::::get(), default_rate_limit); + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(1), + 1, + info1 + )); + let initial_deposit: u64 = ::InitialDeposit::get(); + let field_deposit: u64 = ::FieldDeposit::get(); + let expected_deposit1: u64 = initial_deposit + 2u64 * field_deposit; + assert_eq!( + Pallet::::commitment_of(1, 1) + .expect("Expected not to panic") + .deposit, + expected_deposit1 + ); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(1), + 1, + info2 + )); + let expected_deposit2: u64 = initial_deposit + 3u64 * field_deposit; + assert_eq!( + Pallet::::commitment_of(1, 1) + .expect("Expected not to panic") + .deposit, + expected_deposit2 + ); + }); +} - assert_ok!(Pallet::::set_rate_limit(RuntimeOrigin::root(), 200)); - assert_eq!(RateLimit::::get(), 200); +#[test] +fn set_rate_limit_works() { + new_test_ext().execute_with(|| { + let default_rate_limit: u64 = ::DefaultRateLimit::get(); + assert_eq!(RateLimit::::get(), default_rate_limit); - assert_noop!( - Pallet::::set_rate_limit(RuntimeOrigin::signed(1), 300), - sp_runtime::DispatchError::BadOrigin - ); - }); - } + assert_ok!(Pallet::::set_rate_limit(RuntimeOrigin::root(), 200)); + assert_eq!(RateLimit::::get(), 200); - #[test] - fn event_emission_works() { - new_test_ext().execute_with(|| { - System::::set_block_number(1); - let info = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![]).unwrap(), - ..Default::default() - }); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(1), - 1, - info - )); - - let events = System::::events(); - assert!(events.iter().any(|e| matches!( - &e.event, - RuntimeEvent::Commitments(Event::Commitment { netuid: 1, who: 1 }) - ))); - }); - } + assert_noop!( + Pallet::::set_rate_limit(RuntimeOrigin::signed(1), 300), + sp_runtime::DispatchError::BadOrigin + ); + }); +} - #[test] - fn happy_path_timelock_commitments() { - new_test_ext().execute_with(|| { - let message_text = b"Hello timelock only!"; - let data_raw = Data::Raw( - message_text - .to_vec() - .try_into() - .expect("<= 128 bytes for Raw variant"), - ); - let fields_vec = vec![data_raw]; - let fields_bounded: BoundedVec::MaxFields> = - BoundedVec::try_from(fields_vec).expect("Too many fields"); - - let inner_info: CommitmentInfo<::MaxFields> = CommitmentInfo { - fields: fields_bounded, - }; - - let plaintext = inner_info.encode(); - - let reveal_round = 1000; - let encrypted = produce_ciphertext(&plaintext, reveal_round); - - let data = Data::TimelockEncrypted { - encrypted: encrypted.clone(), - reveal_round, - }; - - let fields_outer: BoundedVec::MaxFields> = - BoundedVec::try_from(vec![data]).expect("Too many fields"); - let info_outer = CommitmentInfo { - fields: fields_outer, - }; - - let who = 123; - let netuid = 42; - System::::set_block_number(1); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(who), - netuid, - Box::new(info_outer) - )); - - let drand_signature_bytes = hex::decode(DRAND_QUICKNET_SIG_HEX).unwrap(); - insert_drand_pulse(reveal_round, &drand_signature_bytes); - - System::::set_block_number(9999); - assert_ok!(Pallet::::reveal_timelocked_commitments(9999)); - - let revealed = - RevealedCommitments::::get(netuid, &who).expect("Should have revealed data"); - - let revealed_inner = &revealed.info; - assert_eq!(revealed_inner.fields.len(), 1); - match &revealed_inner.fields[0] { - Data::Raw(bounded_bytes) => { - assert_eq!( - bounded_bytes.as_slice(), - message_text, - "Decrypted text from on-chain storage must match the original message" - ); - } - other => panic!("Expected Data::Raw(...) in revealed, got {:?}", other), - } +#[test] +fn event_emission_works() { + new_test_ext().execute_with(|| { + System::::set_block_number(1); + let info = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![]).expect("Expected not to panic"), }); - } - #[test] - fn reveal_timelocked_commitment_missing_round_does_nothing() { - new_test_ext().execute_with(|| { - let who = 1; - let netuid = 2; - System::::set_block_number(5); - let ciphertext = produce_ciphertext(b"My plaintext", 1000); - let data = Data::TimelockEncrypted { - encrypted: ciphertext, - reveal_round: 1000, - }; - let fields: BoundedVec<_, ::MaxFields> = - BoundedVec::try_from(vec![data]).unwrap(); - let info = CommitmentInfo { fields }; - let origin = RuntimeOrigin::signed(who); - assert_ok!(Pallet::::set_commitment( - origin, - netuid, - Box::new(info) - )); - System::::set_block_number(100_000); - assert_ok!(Pallet::::reveal_timelocked_commitments(100_000)); - assert!(RevealedCommitments::::get(netuid, &who).is_none()); - }); - } + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(1), + 1, + info + )); + + let events = System::::events(); + assert!(events.iter().any(|e| matches!( + &e.event, + RuntimeEvent::Commitments(Event::Commitment { netuid: 1, who: 1 }) + ))); + }); +} + +#[allow(clippy::indexing_slicing)] +#[test] +fn happy_path_timelock_commitments() { + new_test_ext().execute_with(|| { + let message_text = b"Hello timelock only!"; + let data_raw = Data::Raw( + message_text + .to_vec() + .try_into() + .expect("<= 128 bytes for Raw variant"), + ); + let fields_vec = vec![data_raw]; + let fields_bounded: BoundedVec::MaxFields> = + BoundedVec::try_from(fields_vec).expect("Too many fields"); + + let inner_info: CommitmentInfo<::MaxFields> = CommitmentInfo { + fields: fields_bounded, + }; + + let plaintext = inner_info.encode(); + + let reveal_round = 1000; + let encrypted = produce_ciphertext(&plaintext, reveal_round); + + let data = Data::TimelockEncrypted { + encrypted: encrypted.clone(), + reveal_round, + }; + + let fields_outer: BoundedVec::MaxFields> = + BoundedVec::try_from(vec![data]).expect("Too many fields"); + let info_outer = CommitmentInfo { + fields: fields_outer, + }; - #[test] - fn reveal_timelocked_commitment_cant_deserialize_ciphertext() { - new_test_ext().execute_with(|| { - let who = 42; - let netuid = 9; - System::::set_block_number(10); - let good_ct = produce_ciphertext(b"Some data", 1000); - let mut corrupted = good_ct.into_inner(); - if !corrupted.is_empty() { - corrupted[0] = 0xFF; + let who = 123; + let netuid = 42; + System::::set_block_number(1); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + Box::new(info_outer) + )); + + let drand_signature_bytes = + hex::decode(DRAND_QUICKNET_SIG_HEX).expect("Expected not to panic"); + insert_drand_pulse(reveal_round, &drand_signature_bytes); + + System::::set_block_number(9999); + assert_ok!(Pallet::::reveal_timelocked_commitments(9999)); + + let revealed = + RevealedCommitments::::get(netuid, who).expect("Should have revealed data"); + + let revealed_inner = &revealed.info; + assert_eq!(revealed_inner.fields.len(), 1); + match &revealed_inner.fields[0] { + Data::Raw(bounded_bytes) => { + assert_eq!( + bounded_bytes.as_slice(), + message_text, + "Decrypted text from on-chain storage must match the original message" + ); } - let corrupted_ct = BoundedVec::try_from(corrupted).unwrap(); - let data = Data::TimelockEncrypted { - encrypted: corrupted_ct, - reveal_round: 1000, - }; - let fields = BoundedVec::try_from(vec![data]).unwrap(); - let info = CommitmentInfo { fields }; - let origin = RuntimeOrigin::signed(who); - assert_ok!(Pallet::::set_commitment( - origin, - netuid, - Box::new(info) - )); - let sig_bytes = hex::decode(DRAND_QUICKNET_SIG_HEX).unwrap(); - insert_drand_pulse(1000, &sig_bytes); - System::::set_block_number(99999); - assert_ok!(Pallet::::reveal_timelocked_commitments(99999)); - assert!(RevealedCommitments::::get(netuid, &who).is_none()); - }); - } + other => panic!("Expected Data::Raw(...) in revealed, got {:?}", other), + } + }); +} - #[test] - fn reveal_timelocked_commitment_bad_signature_skips_decryption() { - new_test_ext().execute_with(|| { - let who = 10; - let netuid = 11; - System::::set_block_number(15); - let real_ct = produce_ciphertext(b"A valid plaintext", 1000); - let data = Data::TimelockEncrypted { - encrypted: real_ct, - reveal_round: 1000, - }; - let fields: BoundedVec<_, ::MaxFields> = - BoundedVec::try_from(vec![data]).unwrap(); - let info = CommitmentInfo { fields }; - let origin = RuntimeOrigin::signed(who); - assert_ok!(Pallet::::set_commitment( - origin, - netuid, - Box::new(info) - )); - let bad_signature = [0x33u8; 10]; - insert_drand_pulse(1000, &bad_signature); - System::::set_block_number(10_000); - assert_ok!(Pallet::::reveal_timelocked_commitments(10_000)); - assert!(RevealedCommitments::::get(netuid, &who).is_none()); - }); - } +#[test] +fn reveal_timelocked_commitment_missing_round_does_nothing() { + new_test_ext().execute_with(|| { + let who = 1; + let netuid = 2; + System::::set_block_number(5); + let ciphertext = produce_ciphertext(b"My plaintext", 1000); + let data = Data::TimelockEncrypted { + encrypted: ciphertext, + reveal_round: 1000, + }; + let fields: BoundedVec<_, ::MaxFields> = + BoundedVec::try_from(vec![data]).expect("Expected not to panic"); + let info = CommitmentInfo { fields }; + let origin = RuntimeOrigin::signed(who); + assert_ok!(Pallet::::set_commitment( + origin, + netuid, + Box::new(info) + )); + System::::set_block_number(100_000); + assert_ok!(Pallet::::reveal_timelocked_commitments(100_000)); + assert!(RevealedCommitments::::get(netuid, who).is_none()); + }); +} - #[test] - fn reveal_timelocked_commitment_empty_decrypted_data_is_skipped() { - new_test_ext().execute_with(|| { - let who = 2; - let netuid = 3; - let commit_block = 100u64; - System::::set_block_number(commit_block); - let reveal_round = 1000; - let empty_ct = produce_ciphertext(&[], reveal_round); - let data = Data::TimelockEncrypted { - encrypted: empty_ct, - reveal_round, - }; - let fields = BoundedVec::try_from(vec![data]).unwrap(); - let info = CommitmentInfo { fields }; - let origin = RuntimeOrigin::signed(who); - assert_ok!(Pallet::::set_commitment( - origin, - netuid, - Box::new(info) - )); - let sig_bytes = hex::decode(DRAND_QUICKNET_SIG_HEX).unwrap(); - insert_drand_pulse(reveal_round, &sig_bytes); - System::::set_block_number(10_000); - assert_ok!(Pallet::::reveal_timelocked_commitments(10_000)); - assert!(RevealedCommitments::::get(netuid, &who).is_none()); - }); - } +#[allow(clippy::indexing_slicing)] +#[test] +fn reveal_timelocked_commitment_cant_deserialize_ciphertext() { + new_test_ext().execute_with(|| { + let who = 42; + let netuid = 9; + System::::set_block_number(10); + let good_ct = produce_ciphertext(b"Some data", 1000); + let mut corrupted = good_ct.into_inner(); + if !corrupted.is_empty() { + corrupted[0] = 0xFF; + } + let corrupted_ct = BoundedVec::try_from(corrupted).expect("Expected not to panic"); + let data = Data::TimelockEncrypted { + encrypted: corrupted_ct, + reveal_round: 1000, + }; + let fields = BoundedVec::try_from(vec![data]).expect("Expected not to panic"); + let info = CommitmentInfo { fields }; + let origin = RuntimeOrigin::signed(who); + assert_ok!(Pallet::::set_commitment( + origin, + netuid, + Box::new(info) + )); + let sig_bytes = hex::decode(DRAND_QUICKNET_SIG_HEX).expect("Expected not to panic"); + insert_drand_pulse(1000, &sig_bytes); + System::::set_block_number(99999); + assert_ok!(Pallet::::reveal_timelocked_commitments(99999)); + assert!(RevealedCommitments::::get(netuid, who).is_none()); + }); +} - #[test] - fn reveal_timelocked_commitment_decode_failure_is_skipped() { - new_test_ext().execute_with(|| { - let who = 999; - let netuid = 8; - let commit_block = 42u64; - System::::set_block_number(commit_block); - let plaintext = vec![0xAA, 0xBB, 0xCC, 0xDD, 0xEE]; - let reveal_round = 1000; - let real_ct = produce_ciphertext(&plaintext, reveal_round); - let data = Data::TimelockEncrypted { - encrypted: real_ct, - reveal_round, - }; - let fields = BoundedVec::try_from(vec![data]).unwrap(); - let info = CommitmentInfo { fields }; - let origin = RuntimeOrigin::signed(who); - assert_ok!(Pallet::::set_commitment( - origin, - netuid, - Box::new(info) - )); - let sig_bytes = hex::decode(DRAND_QUICKNET_SIG_HEX.as_bytes()).unwrap(); - insert_drand_pulse(reveal_round, &sig_bytes); - System::::set_block_number(9999); - assert_ok!(Pallet::::reveal_timelocked_commitments(9999)); - assert!(RevealedCommitments::::get(netuid, &who).is_none()); - }); - } +#[test] +fn reveal_timelocked_commitment_bad_signature_skips_decryption() { + new_test_ext().execute_with(|| { + let who = 10; + let netuid = 11; + System::::set_block_number(15); + let real_ct = produce_ciphertext(b"A valid plaintext", 1000); + let data = Data::TimelockEncrypted { + encrypted: real_ct, + reveal_round: 1000, + }; + let fields: BoundedVec<_, ::MaxFields> = + BoundedVec::try_from(vec![data]).expect("Expected not to panic"); + let info = CommitmentInfo { fields }; + let origin = RuntimeOrigin::signed(who); + assert_ok!(Pallet::::set_commitment( + origin, + netuid, + Box::new(info) + )); + let bad_signature = [0x33u8; 10]; + insert_drand_pulse(1000, &bad_signature); + System::::set_block_number(10_000); + assert_ok!(Pallet::::reveal_timelocked_commitments(10_000)); + assert!(RevealedCommitments::::get(netuid, who).is_none()); + }); +} + +#[test] +fn reveal_timelocked_commitment_empty_decrypted_data_is_skipped() { + new_test_ext().execute_with(|| { + let who = 2; + let netuid = 3; + let commit_block = 100u64; + System::::set_block_number(commit_block); + let reveal_round = 1000; + let empty_ct = produce_ciphertext(&[], reveal_round); + let data = Data::TimelockEncrypted { + encrypted: empty_ct, + reveal_round, + }; + let fields = BoundedVec::try_from(vec![data]).expect("Expected not to panic"); + let info = CommitmentInfo { fields }; + let origin = RuntimeOrigin::signed(who); + assert_ok!(Pallet::::set_commitment( + origin, + netuid, + Box::new(info) + )); + let sig_bytes = hex::decode(DRAND_QUICKNET_SIG_HEX).expect("Expected not to panic"); + insert_drand_pulse(reveal_round, &sig_bytes); + System::::set_block_number(10_000); + assert_ok!(Pallet::::reveal_timelocked_commitments(10_000)); + assert!(RevealedCommitments::::get(netuid, who).is_none()); + }); +} + +#[test] +fn reveal_timelocked_commitment_decode_failure_is_skipped() { + new_test_ext().execute_with(|| { + let who = 999; + let netuid = 8; + let commit_block = 42u64; + System::::set_block_number(commit_block); + let plaintext = vec![0xAA, 0xBB, 0xCC, 0xDD, 0xEE]; + let reveal_round = 1000; + let real_ct = produce_ciphertext(&plaintext, reveal_round); + let data = Data::TimelockEncrypted { + encrypted: real_ct, + reveal_round, + }; + let fields = BoundedVec::try_from(vec![data]).expect("Expected not to panic"); + let info = CommitmentInfo { fields }; + let origin = RuntimeOrigin::signed(who); + assert_ok!(Pallet::::set_commitment( + origin, + netuid, + Box::new(info) + )); + let sig_bytes = + hex::decode(DRAND_QUICKNET_SIG_HEX.as_bytes()).expect("Expected not to panic"); + insert_drand_pulse(reveal_round, &sig_bytes); + System::::set_block_number(9999); + assert_ok!(Pallet::::reveal_timelocked_commitments(9999)); + assert!(RevealedCommitments::::get(netuid, who).is_none()); + }); } diff --git a/pallets/commitments/src/types.rs b/pallets/commitments/src/types.rs index c59c7212e..400c10244 100644 --- a/pallets/commitments/src/types.rs +++ b/pallets/commitments/src/types.rs @@ -338,6 +338,7 @@ pub struct CommitmentInfo> { pub const MAX_TIMELOCK_COMMITMENT_SIZE_BYTES: u32 = 1024; /// Contains the decrypted data of a revealed commitment. +#[freeze_struct("bf575857b57f9bef")] #[derive(Clone, Eq, PartialEq, Encode, Decode, TypeInfo, Debug)] pub struct RevealedData, BlockNumber> { pub info: CommitmentInfo, diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index bdaca2269..4e767b7c8 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -541,6 +541,7 @@ impl frame_system::offchain::CreateSignedTransaction> f } } +#[freeze_struct("7c76bd954afbb54e")] #[derive(Clone, Eq, PartialEq, Encode, Decode, TypeInfo)] pub struct MaxCommitFields; impl Get for MaxCommitFields { diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 8c2fbd43b..09a551ce3 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -996,6 +996,7 @@ parameter_types! { pub const CommitmentRateLimit: BlockNumber = 100; // Allow commitment every 100 blocks } +#[subtensor_macros::freeze_struct("7c76bd954afbb54e")] #[derive(Clone, Eq, PartialEq, Encode, Decode, TypeInfo)] pub struct MaxCommitFields; impl Get for MaxCommitFields { @@ -1004,6 +1005,7 @@ impl Get for MaxCommitFields { } } +#[subtensor_macros::freeze_struct("c39297f5eb97ee82")] pub struct AllowCommitments; impl CanCommit for AllowCommitments { #[cfg(not(feature = "runtime-benchmarks"))] From 087288981e79800a3a0d7605c7872293eb85bdc7 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Tue, 4 Mar 2025 08:38:27 -0800 Subject: [PATCH 11/28] add import --- runtime/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index fa0b930ad..2953299ed 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -89,6 +89,8 @@ pub use sp_runtime::{Perbill, Permill}; use core::marker::PhantomData; +use scale_info::TypeInfo; + // Frontier use fp_rpc::TransactionStatus; use pallet_ethereum::{Call::transact, PostLogContent, Transaction as EthereumTransaction}; From 7e76fb1ed226d95aecd1f4dc90b1365ab3b560b0 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Tue, 4 Mar 2025 12:32:05 -0800 Subject: [PATCH 12/28] handle multiple fields --- pallets/commitments/src/lib.rs | 219 ++++++++++++++++++------------- pallets/commitments/src/tests.rs | 194 +++++++++++++++++++++++++++ 2 files changed, 323 insertions(+), 90 deletions(-) diff --git a/pallets/commitments/src/lib.rs b/pallets/commitments/src/lib.rs index 745e06b7c..e03fce646 100644 --- a/pallets/commitments/src/lib.rs +++ b/pallets/commitments/src/lib.rs @@ -25,6 +25,7 @@ use tle::{ tlock::{TLECiphertext, tld}, }; use w3f_bls::EngineBLS; +use scale_info::prelude::collections::BTreeSet; type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; @@ -386,109 +387,147 @@ impl Pallet { .map_err(|_| "Failed to convert u64 to BlockNumberFor")?; for (netuid, who, mut registration) in >::iter() { - if let Some(Data::TimelockEncrypted { - encrypted, - reveal_round, - .. - }) = registration - .info - .fields - .clone() - .iter() - .find(|data| matches!(data, Data::TimelockEncrypted { .. })) - { - // Check if the corresponding Drand round data exists - let pulse = match pallet_drand::Pulses::::get(*reveal_round) { - Some(p) => p, - None => continue, - }; - - // Prepare the signature bytes - let signature_bytes = pulse - .signature - .strip_prefix(b"0x") - .unwrap_or(&pulse.signature); - let sig_reader = &mut &signature_bytes[..]; - let sig = - ::SignatureGroup::deserialize_compressed(sig_reader) - .map_err(|e| { - log::warn!( - "Failed to deserialize drand signature for {:?}: {:?}", - who, - e + let original_fields = registration.info.fields.clone(); + let mut remain_fields = Vec::new(); + let mut revealed_fields = Vec::new(); + + for data in original_fields { + match data { + Data::TimelockEncrypted { + encrypted, + reveal_round, + } => { + let pulse = match pallet_drand::Pulses::::get(reveal_round) { + Some(p) => p, + None => { + remain_fields.push(Data::TimelockEncrypted { + encrypted, + reveal_round, + }); + continue; + } + }; + + let signature_bytes = pulse + .signature + .strip_prefix(b"0x") + .unwrap_or(&pulse.signature); + + let sig_reader = &mut &signature_bytes[..]; + let sig = + ::SignatureGroup::deserialize_compressed( + sig_reader, ) - }) - .ok(); - - let sig = match sig { - Some(s) => s, - None => continue, - }; + .map_err(|e| { + log::warn!( + "Failed to deserialize drand signature for {:?}: {:?}", + who, + e + ) + }) + .ok(); + + let Some(sig) = sig else { + remain_fields.push(Data::TimelockEncrypted { + encrypted, + reveal_round, + }); + continue; + }; + + let reader = &mut &encrypted[..]; + let commit = TLECiphertext::::deserialize_compressed(reader) + .map_err(|e| { + log::warn!( + "Failed to deserialize TLECiphertext for {:?}: {:?}", + who, + e + ) + }) + .ok(); + + let Some(commit) = commit else { + remain_fields.push(Data::TimelockEncrypted { + encrypted, + reveal_round, + }); + continue; + }; + + let decrypted_bytes: Vec = + tld::(commit, sig) + .map_err(|e| { + log::warn!("Failed to decrypt timelock for {:?}: {:?}", who, e) + }) + .ok() + .unwrap_or_default(); + + if decrypted_bytes.is_empty() { + remain_fields.push(Data::TimelockEncrypted { + encrypted, + reveal_round, + }); + continue; + } + + let mut reader = &decrypted_bytes[..]; + let revealed_info: CommitmentInfo = + match Decode::decode(&mut reader) { + Ok(info) => info, + Err(e) => { + log::warn!( + "Failed to decode decrypted data for {:?}: {:?}", + who, + e + ); + remain_fields.push(Data::TimelockEncrypted { + encrypted, + reveal_round, + }); + continue; + } + }; + + revealed_fields.push(revealed_info); + } - // Attempt to deserialize the encrypted commitment - let reader = &mut &encrypted[..]; - let commit = TLECiphertext::::deserialize_compressed(reader) - .map_err(|e| { - log::warn!("Failed to deserialize TLECiphertext for {:?}: {:?}", who, e) - }) - .ok(); - - let commit = match commit { - Some(c) => c, - None => continue, - }; + other => remain_fields.push(other), + } + } - // Decrypt the timelock commitment - let decrypted_bytes: Vec = - tld::(commit, sig) - .map_err(|e| { - log::warn!("Failed to decrypt timelock for {:?}: {:?}", who, e) - }) - .ok() - .unwrap_or_default(); - - if decrypted_bytes.is_empty() { - continue; + if !revealed_fields.is_empty() { + let mut all_revealed_data = Vec::new(); + for info in revealed_fields { + all_revealed_data.extend(info.fields.into_inner()); } - // Decode the decrypted bytes into CommitmentInfo - let mut reader = &decrypted_bytes[..]; - let revealed_info: CommitmentInfo = match Decode::decode(&mut reader) - { - Ok(info) => info, - Err(e) => { - log::warn!("Failed to decode decrypted data for {:?}: {:?}", who, e); - continue; - } + let bounded_revealed = BoundedVec::try_from(all_revealed_data) + .map_err(|_| "Could not build BoundedVec for revealed fields")?; + + let combined_revealed_info = CommitmentInfo { + fields: bounded_revealed, }; - // Store the revealed data let revealed_data = RevealedData { - info: revealed_info, + info: combined_revealed_info, revealed_block: current_block, deposit: registration.deposit, }; >::insert(netuid, &who, revealed_data); - // Remove the TimelockEncrypted field from the original commitment - let filtered_fields: Vec = registration - .info - .fields - .into_iter() - .filter(|data| { - !matches!( - data, - Data::TimelockEncrypted { - reveal_round: r, .. - } if r == reveal_round - ) - }) - .collect(); - - registration.info.fields = BoundedVec::try_from(filtered_fields) - .map_err(|_| "Failed to filter timelock fields")?; - - Self::deposit_event(Event::CommitmentRevealed { netuid, who }); + let who_clone = who.clone(); + Self::deposit_event(Event::CommitmentRevealed { + netuid, + who: who_clone, + }); + } + + registration.info.fields = BoundedVec::try_from(remain_fields) + .map_err(|_| "Failed to build BoundedVec for remain_fields")?; + + match registration.info.fields.is_empty() { + true => >::remove(netuid, &who), + false => >::insert(netuid, who, registration), } } diff --git a/pallets/commitments/src/tests.rs b/pallets/commitments/src/tests.rs index c40c0b042..4f308747f 100644 --- a/pallets/commitments/src/tests.rs +++ b/pallets/commitments/src/tests.rs @@ -474,3 +474,197 @@ fn reveal_timelocked_commitment_decode_failure_is_skipped() { assert!(RevealedCommitments::::get(netuid, who).is_none()); }); } + +#[test] +fn reveal_timelocked_commitment_single_field_entry_is_removed_after_reveal() { + new_test_ext().execute_with(|| { + let message_text = b"Single field timelock test!"; + let data_raw = Data::Raw( + message_text + .to_vec() + .try_into() + .expect("Message must be <=128 bytes for Raw variant"), + ); + + let fields_bounded: BoundedVec::MaxFields> = + BoundedVec::try_from(vec![data_raw]).expect("BoundedVec creation must not fail"); + + let inner_info: CommitmentInfo<::MaxFields> = CommitmentInfo { + fields: fields_bounded, + }; + + let plaintext = inner_info.encode(); + let reveal_round = 1000; + let encrypted = produce_ciphertext(&plaintext, reveal_round); + + let timelock_data = Data::TimelockEncrypted { + encrypted, + reveal_round, + }; + let fields_outer: BoundedVec::MaxFields> = + BoundedVec::try_from(vec![timelock_data]).expect("Too many fields"); + let info_outer: CommitmentInfo<::MaxFields> = CommitmentInfo { + fields: fields_outer, + }; + + let who = 555; + let netuid = 777; + System::::set_block_number(1); + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + Box::new(info_outer) + )); + + let drand_signature_bytes = hex::decode(DRAND_QUICKNET_SIG_HEX) + .expect("Must decode DRAND_QUICKNET_SIG_HEX successfully"); + insert_drand_pulse(reveal_round, &drand_signature_bytes); + + System::::set_block_number(9999); + assert_ok!(Pallet::::reveal_timelocked_commitments(9999)); + + let revealed = + RevealedCommitments::::get(netuid, who).expect("Expected to find revealed data"); + assert_eq!( + revealed.info.fields.len(), + 1, + "Should have exactly 1 revealed field" + ); + + assert!( + crate::CommitmentOf::::get(netuid, who).is_none(), + "Expected CommitmentOf entry to be removed after reveal" + ); + }); +} + +#[allow(clippy::indexing_slicing)] +#[test] +fn reveal_timelocked_multiple_fields_only_correct_ones_removed() { + new_test_ext().execute_with(|| { + let round_1000 = 1000; + + // 2) Build two CommitmentInfos, one for each timelock + let msg_1 = b"Hello from TLE #1"; + let inner_1_fields: BoundedVec::MaxFields> = + BoundedVec::try_from(vec![Data::Raw( + msg_1.to_vec().try_into().expect("expected not to panic"), + )]) + .expect("BoundedVec of size 1"); + let inner_info_1 = CommitmentInfo { + fields: inner_1_fields, + }; + let encoded_1 = inner_info_1.encode(); + let ciphertext_1 = produce_ciphertext(&encoded_1, round_1000); + let timelock_1 = Data::TimelockEncrypted { + encrypted: ciphertext_1, + reveal_round: round_1000, + }; + + let msg_2 = b"Hello from TLE #2"; + let inner_2_fields: BoundedVec::MaxFields> = + BoundedVec::try_from(vec![Data::Raw( + msg_2.to_vec().try_into().expect("expected not to panic"), + )]) + .expect("BoundedVec of size 1"); + let inner_info_2 = CommitmentInfo { + fields: inner_2_fields, + }; + let encoded_2 = inner_info_2.encode(); + let ciphertext_2 = produce_ciphertext(&encoded_2, round_1000); + let timelock_2 = Data::TimelockEncrypted { + encrypted: ciphertext_2, + reveal_round: round_1000, + }; + + // 3) One plain Data::Raw field (non-timelocked) + let raw_bytes = b"Plain non-timelocked data"; + let data_raw = Data::Raw( + raw_bytes + .to_vec() + .try_into() + .expect("expected not to panic"), + ); + + // 4) Outer commitment: 3 fields total => [Raw, TLE #1, TLE #2] + let outer_fields = BoundedVec::try_from(vec![ + data_raw.clone(), + timelock_1.clone(), + timelock_2.clone(), + ]) + .expect("T::MaxFields >= 3 in the test config, or at least 3 here"); + let outer_info = CommitmentInfo { + fields: outer_fields, + }; + + // 5) Insert the commitment + let who = 123; + let netuid = 999; + System::::set_block_number(1); + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + Box::new(outer_info) + )); + let initial = Pallet::::commitment_of(netuid, who).expect("Must exist"); + assert_eq!(initial.info.fields.len(), 3, "3 fields inserted"); + + // 6) Insert Drand signature for round=1000 + let drand_sig_1000 = hex::decode(DRAND_QUICKNET_SIG_HEX).expect("decode DRAND sig"); + insert_drand_pulse(round_1000, &drand_sig_1000); + + // 7) Reveal once + System::::set_block_number(50); + assert_ok!(Pallet::::reveal_timelocked_commitments(50)); + + // => The pallet code has removed *both* TLE #1 and TLE #2 in this single call! + let after_reveal = Pallet::::commitment_of(netuid, who) + .expect("Should still exist with leftover fields"); + // Only the raw, non-timelocked field remains + assert_eq!( + after_reveal.info.fields.len(), + 1, + "Both timelocks referencing round=1000 got removed at once" + ); + assert_eq!( + after_reveal.info.fields[0], data_raw, + "Only the raw field is left" + ); + + // 8) Check revealed data + let revealed_data = RevealedCommitments::::get(netuid, who) + .expect("Expected revealed data for TLE #1 and #2"); + + assert_eq!( + revealed_data.info.fields.len(), + 2, + "We revealed both TLE #1 and TLE #2 in the same pass" + ); + let mut found_msg1 = false; + let mut found_msg2 = false; + for item in &revealed_data.info.fields { + if let Data::Raw(bytes) = item { + if bytes.as_slice() == msg_1 { + found_msg1 = true; + } else if bytes.as_slice() == msg_2 { + found_msg2 = true; + } + } + } + assert!( + found_msg1 && found_msg2, + "Should see both TLE #1 and TLE #2 in the revealed data" + ); + + // 9) A second reveal call now does nothing, because no timelocks remain + System::::set_block_number(51); + assert_ok!(Pallet::::reveal_timelocked_commitments(51)); + + let after_second = Pallet::::commitment_of(netuid, who).expect("Still must exist"); + assert_eq!( + after_second.info.fields.len(), + 1, + "No new fields were removed, because no timelocks remain" + ); + }); +} From 937368a565f22ddc1c4f460d1fa8ae81baf712df Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Tue, 4 Mar 2025 13:22:04 -0800 Subject: [PATCH 13/28] only iterate over timelocked commitments in reveal --- pallets/commitments/src/lib.rs | 59 ++++++-- pallets/commitments/src/tests.rs | 242 ++++++++++++++++++++++++++++++- 2 files changed, 290 insertions(+), 11 deletions(-) diff --git a/pallets/commitments/src/lib.rs b/pallets/commitments/src/lib.rs index e03fce646..c7d66146c 100644 --- a/pallets/commitments/src/lib.rs +++ b/pallets/commitments/src/lib.rs @@ -17,6 +17,7 @@ pub use weights::WeightInfo; use ark_serialize::CanonicalDeserialize; use frame_support::{BoundedVec, traits::Currency}; +use scale_info::prelude::collections::BTreeSet; use sp_runtime::{Saturating, traits::Zero}; use sp_std::{boxed::Box, vec::Vec}; use tle::{ @@ -25,7 +26,6 @@ use tle::{ tlock::{TLECiphertext, tld}, }; use w3f_bls::EngineBLS; -use scale_info::prelude::collections::BTreeSet; type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; @@ -120,6 +120,12 @@ pub mod pallet { #[pallet::storage] pub type RateLimit = StorageValue<_, BlockNumberFor, ValueQuery, DefaultRateLimit>; + /// Tracks all CommitmentOf that have at least one timelocked field. + #[pallet::storage] + #[pallet::getter(fn timelocked_index)] + pub type TimelockedIndex = + StorageValue<_, BTreeSet<(u16, T::AccountId)>, ValueQuery>; + /// Identity data by account #[pallet::storage] #[pallet::getter(fn commitment_of)] @@ -225,11 +231,22 @@ pub mod pallet { { Self::deposit_event(Event::TimelockCommitment { netuid, - who, + who: who.clone(), reveal_round: *reveal_round, }); + + TimelockedIndex::::mutate(|index| { + index.insert((netuid, who.clone())); + }); } else { - Self::deposit_event(Event::Commitment { netuid, who }); + Self::deposit_event(Event::Commitment { + netuid, + who: who.clone(), + }); + + TimelockedIndex::::mutate(|index| { + index.remove(&(netuid, who.clone())); + }); } Ok(()) @@ -386,7 +403,15 @@ impl Pallet { .try_into() .map_err(|_| "Failed to convert u64 to BlockNumberFor")?; - for (netuid, who, mut registration) in >::iter() { + let index = TimelockedIndex::::get(); + for (netuid, who) in index.clone() { + let Some(mut registration) = >::get(netuid, &who) else { + TimelockedIndex::::mutate(|idx| { + idx.remove(&(netuid, who.clone())); + }); + continue; + }; + let original_fields = registration.info.fields.clone(); let mut remain_fields = Vec::new(); let mut revealed_fields = Vec::new(); @@ -412,7 +437,6 @@ impl Pallet { .signature .strip_prefix(b"0x") .unwrap_or(&pulse.signature); - let sig_reader = &mut &signature_bytes[..]; let sig = ::SignatureGroup::deserialize_compressed( @@ -514,11 +538,9 @@ impl Pallet { deposit: registration.deposit, }; >::insert(netuid, &who, revealed_data); - - let who_clone = who.clone(); Self::deposit_event(Event::CommitmentRevealed { netuid, - who: who_clone, + who: who.clone(), }); } @@ -526,8 +548,25 @@ impl Pallet { .map_err(|_| "Failed to build BoundedVec for remain_fields")?; match registration.info.fields.is_empty() { - true => >::remove(netuid, &who), - false => >::insert(netuid, who, registration), + true => { + >::remove(netuid, &who); + TimelockedIndex::::mutate(|idx| { + idx.remove(&(netuid, who.clone())); + }); + } + false => { + >::insert(netuid, &who, ®istration); + let has_timelock = registration + .info + .fields + .iter() + .any(|f| matches!(f, Data::TimelockEncrypted { .. })); + if !has_timelock { + TimelockedIndex::::mutate(|idx| { + idx.remove(&(netuid, who.clone())); + }); + } + } } } diff --git a/pallets/commitments/src/tests.rs b/pallets/commitments/src/tests.rs index 4f308747f..c226c0eee 100644 --- a/pallets/commitments/src/tests.rs +++ b/pallets/commitments/src/tests.rs @@ -3,7 +3,8 @@ use sp_std::prelude::*; #[cfg(test)] use crate::{ - CommitmentInfo, Config, Data, Error, Event, Pallet, RateLimit, RevealedCommitments, + CommitmentInfo, CommitmentOf, Config, Data, Error, Event, Pallet, RateLimit, + RevealedCommitments, TimelockedIndex, mock::{ DRAND_QUICKNET_SIG_HEX, RuntimeEvent, RuntimeOrigin, Test, insert_drand_pulse, new_test_ext, produce_ciphertext, @@ -668,3 +669,242 @@ fn reveal_timelocked_multiple_fields_only_correct_ones_removed() { ); }); } + +#[test] +fn test_index_lifecycle_no_timelocks_updates_in_out() { + new_test_ext().execute_with(|| { + let netuid = 100; + let who = 999; + + // + // A) Create a commitment with **no** timelocks => shouldn't be in index + // + let no_tl_fields: BoundedVec::MaxFields> = + BoundedVec::try_from(vec![]).expect("Empty is ok"); + let info_no_tl = CommitmentInfo { + fields: no_tl_fields, + }; + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + Box::new(info_no_tl) + )); + assert!( + !TimelockedIndex::::get().contains(&(netuid, who)), + "User with no timelocks must not appear in index" + ); + + // + // B) Update the commitment to have a timelock => enters index + // + let tl_fields: BoundedVec<_, ::MaxFields> = + BoundedVec::try_from(vec![Data::TimelockEncrypted { + encrypted: Default::default(), + reveal_round: 1234, + }]) + .expect("Expected success"); + let info_with_tl = CommitmentInfo { fields: tl_fields }; + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + Box::new(info_with_tl) + )); + assert!( + TimelockedIndex::::get().contains(&(netuid, who)), + "User must appear in index after adding a timelock" + ); + + // + // C) Remove the timelock => leaves index + // + let back_to_no_tl: BoundedVec<_, ::MaxFields> = + BoundedVec::try_from(vec![]).expect("Expected success"); + let info_remove_tl = CommitmentInfo { + fields: back_to_no_tl, + }; + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + Box::new(info_remove_tl) + )); + + assert!( + !TimelockedIndex::::get().contains(&(netuid, who)), + "User must be removed from index after losing all timelocks" + ); + }); +} + +#[test] +fn two_timelocks_partial_then_full_reveal() { + new_test_ext().execute_with(|| { + let netuid_a = 1; + let who_a = 10; + let round_1000 = 1000; + let round_2000 = 2000; + + let drand_sig_1000 = hex::decode(DRAND_QUICKNET_SIG_HEX).expect("Expected success"); + insert_drand_pulse(round_1000, &drand_sig_1000); + + let drand_sig_2000_hex = + "b6cb8f482a0b15d45936a4c4ea08e98a087e71787caee3f4d07a8a9843b1bc5423c6b3c22f446488b3137eaca799c77e"; + + // + // First Timelock => round=1000 + // + let msg_a1 = b"UserA timelock #1 (round=1000)"; + let inner_1_fields: BoundedVec::MaxFields> = BoundedVec::try_from( + vec![Data::Raw(msg_a1.to_vec().try_into().expect("Expected success"))], + ) + .expect("MaxFields >= 1"); + let inner_info_1: CommitmentInfo<::MaxFields> = CommitmentInfo { + fields: inner_1_fields, + }; + let encoded_1 = inner_info_1.encode(); + let ciphertext_1 = produce_ciphertext(&encoded_1, round_1000); + let tle_a1 = Data::TimelockEncrypted { + encrypted: ciphertext_1, + reveal_round: round_1000, + }; + + // + // Second Timelock => round=2000 + // + let msg_a2 = b"UserA timelock #2 (round=2000)"; + let inner_2_fields: BoundedVec::MaxFields> = BoundedVec::try_from( + vec![Data::Raw(msg_a2.to_vec().try_into().expect("Expected success"))], + ) + .expect("MaxFields >= 1"); + let inner_info_2: CommitmentInfo<::MaxFields> = CommitmentInfo { + fields: inner_2_fields, + }; + let encoded_2 = inner_info_2.encode(); + let ciphertext_2 = produce_ciphertext(&encoded_2, round_2000); + let tle_a2 = Data::TimelockEncrypted { + encrypted: ciphertext_2, + reveal_round: round_2000, + }; + + // + // Insert outer commitment with both timelocks + // + let fields_a: BoundedVec::MaxFields> = + BoundedVec::try_from(vec![tle_a1, tle_a2]).expect("2 fields, must be <= MaxFields"); + let info_a: CommitmentInfo<::MaxFields> = CommitmentInfo { fields: fields_a }; + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who_a), + netuid_a, + Box::new(info_a) + )); + assert!( + TimelockedIndex::::get().contains(&(netuid_a, who_a)), + "User A must be in index with 2 timelocks" + ); + + System::::set_block_number(10); + assert_ok!(Pallet::::reveal_timelocked_commitments(10)); + + let leftover_a1 = CommitmentOf::::get(netuid_a, who_a).expect("still there"); + assert_eq!( + leftover_a1.info.fields.len(), + 1, + "Only the round=1000 timelock removed; round=2000 remains" + ); + assert!( + TimelockedIndex::::get().contains(&(netuid_a, who_a)), + "Still in index with leftover timelock" + ); + + // + // Insert signature for round=2000 => final reveal => leftover=none => removed + // + let drand_sig_2000 = hex::decode(drand_sig_2000_hex).expect("Expected success"); + insert_drand_pulse(round_2000, &drand_sig_2000); + + System::::set_block_number(11); + assert_ok!(Pallet::::reveal_timelocked_commitments(11)); + + let leftover_a2 = CommitmentOf::::get(netuid_a, who_a); + assert!( + leftover_a2.is_none(), + "All timelocks removed => none leftover" + ); + assert!( + !TimelockedIndex::::get().contains(&(netuid_a, who_a)), + "User A removed from index after final reveal" + ); + }); +} + +#[test] +fn single_timelock_reveal_later_round() { + new_test_ext().execute_with(|| { + let netuid_b = 2; + let who_b = 20; + let round_2000 = 2000; + + let drand_sig_2000_hex = + "b6cb8f482a0b15d45936a4c4ea08e98a087e71787caee3f4d07a8a9843b1bc5423c6b3c22f446488b3137eaca799c77e"; + let drand_sig_2000 = hex::decode(drand_sig_2000_hex).expect("Expected success"); + insert_drand_pulse(round_2000, &drand_sig_2000); + + let msg_b = b"UserB single timelock (round=2000)"; + + let inner_b_fields: BoundedVec::MaxFields> = + BoundedVec::try_from(vec![Data::Raw(msg_b.to_vec().try_into().expect("Expected success"))]) + .expect("MaxFields >= 1"); + let inner_info_b: CommitmentInfo<::MaxFields> = CommitmentInfo { + fields: inner_b_fields, + }; + let encoded_b = inner_info_b.encode(); + let ciphertext_b = produce_ciphertext(&encoded_b, round_2000); + let tle_b = Data::TimelockEncrypted { + encrypted: ciphertext_b, + reveal_round: round_2000, + }; + + let fields_b: BoundedVec::MaxFields> = + BoundedVec::try_from(vec![tle_b]).expect("1 field"); + let info_b: CommitmentInfo<::MaxFields> = CommitmentInfo { fields: fields_b }; + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who_b), + netuid_b, + Box::new(info_b) + )); + assert!( + TimelockedIndex::::get().contains(&(netuid_b, who_b)), + "User B in index" + ); + + // Remove the round=2000 signature so first reveal does nothing + pallet_drand::Pulses::::remove(round_2000); + + System::::set_block_number(20); + assert_ok!(Pallet::::reveal_timelocked_commitments(20)); + + let leftover_b1 = CommitmentOf::::get(netuid_b, who_b).expect("still there"); + assert_eq!( + leftover_b1.info.fields.len(), + 1, + "No signature => timelock remains" + ); + assert!( + TimelockedIndex::::get().contains(&(netuid_b, who_b)), + "Still in index with leftover timelock" + ); + + insert_drand_pulse(round_2000, &drand_sig_2000); + + System::::set_block_number(21); + assert_ok!(Pallet::::reveal_timelocked_commitments(21)); + + let leftover_b2 = CommitmentOf::::get(netuid_b, who_b); + assert!(leftover_b2.is_none(), "Timelock removed => leftover=none"); + assert!( + !TimelockedIndex::::get().contains(&(netuid_b, who_b)), + "User B removed from index after final reveal" + ); + }); +} From 99b5a28b36a0e765a734b3202e80e9fd917079ca Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Thu, 6 Mar 2025 10:06:17 -0800 Subject: [PATCH 14/28] remove block_number param --- pallets/commitments/src/lib.rs | 7 ++---- pallets/commitments/src/tests.rs | 26 ++++++++++---------- pallets/subtensor/src/coinbase/block_step.rs | 2 +- pallets/subtensor/src/utils/misc.rs | 6 ++--- 4 files changed, 18 insertions(+), 23 deletions(-) diff --git a/pallets/commitments/src/lib.rs b/pallets/commitments/src/lib.rs index c7d66146c..850fe23e3 100644 --- a/pallets/commitments/src/lib.rs +++ b/pallets/commitments/src/lib.rs @@ -398,11 +398,8 @@ where } impl Pallet { - pub fn reveal_timelocked_commitments(current_block: u64) -> DispatchResult { - let current_block = current_block - .try_into() - .map_err(|_| "Failed to convert u64 to BlockNumberFor")?; - + pub fn reveal_timelocked_commitments() -> DispatchResult { + let current_block = >::block_number(); let index = TimelockedIndex::::get(); for (netuid, who) in index.clone() { let Some(mut registration) = >::get(netuid, &who) else { diff --git a/pallets/commitments/src/tests.rs b/pallets/commitments/src/tests.rs index c226c0eee..bcb775f9c 100644 --- a/pallets/commitments/src/tests.rs +++ b/pallets/commitments/src/tests.rs @@ -309,7 +309,7 @@ fn happy_path_timelock_commitments() { insert_drand_pulse(reveal_round, &drand_signature_bytes); System::::set_block_number(9999); - assert_ok!(Pallet::::reveal_timelocked_commitments(9999)); + assert_ok!(Pallet::::reveal_timelocked_commitments()); let revealed = RevealedCommitments::::get(netuid, who).expect("Should have revealed data"); @@ -350,7 +350,7 @@ fn reveal_timelocked_commitment_missing_round_does_nothing() { Box::new(info) )); System::::set_block_number(100_000); - assert_ok!(Pallet::::reveal_timelocked_commitments(100_000)); + assert_ok!(Pallet::::reveal_timelocked_commitments()); assert!(RevealedCommitments::::get(netuid, who).is_none()); }); } @@ -383,7 +383,7 @@ fn reveal_timelocked_commitment_cant_deserialize_ciphertext() { let sig_bytes = hex::decode(DRAND_QUICKNET_SIG_HEX).expect("Expected not to panic"); insert_drand_pulse(1000, &sig_bytes); System::::set_block_number(99999); - assert_ok!(Pallet::::reveal_timelocked_commitments(99999)); + assert_ok!(Pallet::::reveal_timelocked_commitments()); assert!(RevealedCommitments::::get(netuid, who).is_none()); }); } @@ -411,7 +411,7 @@ fn reveal_timelocked_commitment_bad_signature_skips_decryption() { let bad_signature = [0x33u8; 10]; insert_drand_pulse(1000, &bad_signature); System::::set_block_number(10_000); - assert_ok!(Pallet::::reveal_timelocked_commitments(10_000)); + assert_ok!(Pallet::::reveal_timelocked_commitments()); assert!(RevealedCommitments::::get(netuid, who).is_none()); }); } @@ -440,7 +440,7 @@ fn reveal_timelocked_commitment_empty_decrypted_data_is_skipped() { let sig_bytes = hex::decode(DRAND_QUICKNET_SIG_HEX).expect("Expected not to panic"); insert_drand_pulse(reveal_round, &sig_bytes); System::::set_block_number(10_000); - assert_ok!(Pallet::::reveal_timelocked_commitments(10_000)); + assert_ok!(Pallet::::reveal_timelocked_commitments()); assert!(RevealedCommitments::::get(netuid, who).is_none()); }); } @@ -471,7 +471,7 @@ fn reveal_timelocked_commitment_decode_failure_is_skipped() { hex::decode(DRAND_QUICKNET_SIG_HEX.as_bytes()).expect("Expected not to panic"); insert_drand_pulse(reveal_round, &sig_bytes); System::::set_block_number(9999); - assert_ok!(Pallet::::reveal_timelocked_commitments(9999)); + assert_ok!(Pallet::::reveal_timelocked_commitments()); assert!(RevealedCommitments::::get(netuid, who).is_none()); }); } @@ -522,7 +522,7 @@ fn reveal_timelocked_commitment_single_field_entry_is_removed_after_reveal() { insert_drand_pulse(reveal_round, &drand_signature_bytes); System::::set_block_number(9999); - assert_ok!(Pallet::::reveal_timelocked_commitments(9999)); + assert_ok!(Pallet::::reveal_timelocked_commitments()); let revealed = RevealedCommitments::::get(netuid, who).expect("Expected to find revealed data"); @@ -616,7 +616,7 @@ fn reveal_timelocked_multiple_fields_only_correct_ones_removed() { // 7) Reveal once System::::set_block_number(50); - assert_ok!(Pallet::::reveal_timelocked_commitments(50)); + assert_ok!(Pallet::::reveal_timelocked_commitments()); // => The pallet code has removed *both* TLE #1 and TLE #2 in this single call! let after_reveal = Pallet::::commitment_of(netuid, who) @@ -659,7 +659,7 @@ fn reveal_timelocked_multiple_fields_only_correct_ones_removed() { // 9) A second reveal call now does nothing, because no timelocks remain System::::set_block_number(51); - assert_ok!(Pallet::::reveal_timelocked_commitments(51)); + assert_ok!(Pallet::::reveal_timelocked_commitments()); let after_second = Pallet::::commitment_of(netuid, who).expect("Still must exist"); assert_eq!( @@ -803,7 +803,7 @@ fn two_timelocks_partial_then_full_reveal() { ); System::::set_block_number(10); - assert_ok!(Pallet::::reveal_timelocked_commitments(10)); + assert_ok!(Pallet::::reveal_timelocked_commitments()); let leftover_a1 = CommitmentOf::::get(netuid_a, who_a).expect("still there"); assert_eq!( @@ -823,7 +823,7 @@ fn two_timelocks_partial_then_full_reveal() { insert_drand_pulse(round_2000, &drand_sig_2000); System::::set_block_number(11); - assert_ok!(Pallet::::reveal_timelocked_commitments(11)); + assert_ok!(Pallet::::reveal_timelocked_commitments()); let leftover_a2 = CommitmentOf::::get(netuid_a, who_a); assert!( @@ -882,7 +882,7 @@ fn single_timelock_reveal_later_round() { pallet_drand::Pulses::::remove(round_2000); System::::set_block_number(20); - assert_ok!(Pallet::::reveal_timelocked_commitments(20)); + assert_ok!(Pallet::::reveal_timelocked_commitments()); let leftover_b1 = CommitmentOf::::get(netuid_b, who_b).expect("still there"); assert_eq!( @@ -898,7 +898,7 @@ fn single_timelock_reveal_later_round() { insert_drand_pulse(round_2000, &drand_sig_2000); System::::set_block_number(21); - assert_ok!(Pallet::::reveal_timelocked_commitments(21)); + assert_ok!(Pallet::::reveal_timelocked_commitments()); let leftover_b2 = CommitmentOf::::get(netuid_b, who_b); assert!(leftover_b2.is_none(), "Timelock removed => leftover=none"); diff --git a/pallets/subtensor/src/coinbase/block_step.rs b/pallets/subtensor/src/coinbase/block_step.rs index 6943a0265..6b650b761 100644 --- a/pallets/subtensor/src/coinbase/block_step.rs +++ b/pallets/subtensor/src/coinbase/block_step.rs @@ -20,7 +20,7 @@ impl Pallet { Self::try_set_pending_children(block_number); // --- 5. Unveil all matured timelocked entries - if let Err(e) = Self::reveal_timelocked_commitments(block_number) { + if let Err(e) = Self::reveal_timelocked_commitments() { log::debug!( "Failed to unveil matured commitments on block {} due to error: {:?}", block_number, diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index 1983f1a16..3f6924faa 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -769,9 +769,7 @@ impl Pallet { Self::deposit_event(Event::SubnetOwnerHotkeySet(netuid, hotkey.clone())); } - pub fn reveal_timelocked_commitments(block_number: u64) -> DispatchResult { - pallet_commitments::Pallet::::reveal_timelocked_commitments( - block_number, - ) + pub fn reveal_timelocked_commitments() -> DispatchResult { + pallet_commitments::Pallet::::reveal_timelocked_commitments() } } From 20bf5026dd0d6b1f2cb5e914c40bab15341a6f17 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Wed, 12 Mar 2025 09:49:52 -0700 Subject: [PATCH 15/28] fix merge --- pallets/subtensor/src/tests/mock.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index 78cfaaa15..588fcab4d 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -409,6 +409,7 @@ impl crate::Config for Test { type InitialDissolveNetworkScheduleDuration = InitialDissolveNetworkScheduleDuration; type InitialTaoWeight = InitialTaoWeight; type CommitmentRuntime = Test; + type InitialEmaPriceHalvingPeriod = InitialEmaPriceHalvingPeriod; } parameter_types! { @@ -416,7 +417,6 @@ parameter_types! { pub const CommitmentInitialDeposit: Balance = 0; pub const CommitmentFieldDeposit: Balance = 0; pub const CommitmentRateLimit: BlockNumber = 100; - type InitialEmaPriceHalvingPeriod = InitialEmaPriceHalvingPeriod; } pub struct OriginPrivilegeCmp; From 4b237eae1dacd46588c3252d7457a999ad86e3d0 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Thu, 13 Mar 2025 11:03:48 -0700 Subject: [PATCH 16/28] use space based rate limit per subnet tempo --- Cargo.lock | 2 +- pallets/admin-utils/src/tests/mock.rs | 38 +------- pallets/commitments/Cargo.toml | 11 ++- pallets/commitments/src/lib.rs | 98 ++++++++++++++++++-- pallets/commitments/src/mock.rs | 8 ++ pallets/commitments/src/types.rs | 10 ++ pallets/subtensor/Cargo.toml | 11 +-- pallets/subtensor/src/coinbase/block_step.rs | 9 -- pallets/subtensor/src/macros/config.rs | 3 - pallets/subtensor/src/tests/mock.rs | 36 ------- pallets/subtensor/src/utils/misc.rs | 4 - runtime/src/lib.rs | 15 ++- 12 files changed, 133 insertions(+), 112 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3cb5acc97..57227ec93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6099,6 +6099,7 @@ dependencies = [ "log", "pallet-balances", "pallet-drand", + "pallet-subtensor", "parity-scale-codec", "rand_chacha", "scale-info", @@ -6461,7 +6462,6 @@ dependencies = [ "num-traits", "pallet-balances", "pallet-collective", - "pallet-commitments", "pallet-drand", "pallet-membership", "pallet-preimage", diff --git a/pallets/admin-utils/src/tests/mock.rs b/pallets/admin-utils/src/tests/mock.rs index 858570a16..fc0d01619 100644 --- a/pallets/admin-utils/src/tests/mock.rs +++ b/pallets/admin-utils/src/tests/mock.rs @@ -7,11 +7,10 @@ use frame_support::{ }; use frame_system as system; use frame_system::{EnsureNever, EnsureRoot, limits}; -use scale_info::TypeInfo; use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_consensus_grandpa::AuthorityList as GrandpaAuthorityList; use sp_core::U256; -use sp_core::{ConstU64, Decode, Encode, Get, H256}; +use sp_core::{ConstU64, H256}; use sp_runtime::{ BuildStorage, KeyTypeId, Perbill, testing::TestXt, @@ -33,7 +32,6 @@ frame_support::construct_runtime!( Drand: pallet_drand::{Pallet, Call, Storage, Event} = 6, Grandpa: pallet_grandpa = 7, EVMChainId: pallet_evm_chain_id = 8, - Commitments: pallet_commitments::{Pallet, Call, Storage, Event} = 9, } ); @@ -200,7 +198,6 @@ impl pallet_subtensor::Config for Test { type InitialColdkeySwapScheduleDuration = InitialColdkeySwapScheduleDuration; type InitialDissolveNetworkScheduleDuration = InitialDissolveNetworkScheduleDuration; type InitialTaoWeight = InitialTaoWeight; - type CommitmentRuntime = Test; type InitialEmaPriceHalvingPeriod = InitialEmaPriceHalvingPeriod; } @@ -378,39 +375,6 @@ where type OverarchingCall = RuntimeCall; } -parameter_types! { - pub const MaxCommitFieldsInner: u32 = 1; - pub const CommitmentInitialDeposit: Balance = 0; - pub const CommitmentFieldDeposit: Balance = 0; - pub const CommitmentRateLimit: BlockNumber = 100; -} - -#[subtensor_macros::freeze_struct("7c76bd954afbb54e")] -#[derive(Clone, Eq, PartialEq, Encode, Decode, TypeInfo)] -pub struct MaxCommitFields; -impl Get for MaxCommitFields { - fn get() -> u32 { - MaxCommitFieldsInner::get() - } -} - -pub struct AllowCommitments; -impl pallet_commitments::CanCommit for AllowCommitments { - fn can_commit(_netuid: u16, _address: &AccountId) -> bool { - true - } -} - -impl pallet_commitments::Config for Test { - type RuntimeEvent = RuntimeEvent; - type Currency = Balances; - type WeightInfo = pallet_commitments::weights::SubstrateWeight; - type CanCommit = AllowCommitments; - type MaxFields = MaxCommitFields; - type InitialDeposit = CommitmentInitialDeposit; - type FieldDeposit = CommitmentFieldDeposit; - type DefaultRateLimit = CommitmentRateLimit; -} // Build genesis storage according to the mock runtime. pub fn new_test_ext() -> sp_io::TestExternalities { sp_tracing::try_init_simple(); diff --git a/pallets/commitments/Cargo.toml b/pallets/commitments/Cargo.toml index fb0091deb..7b2f49ace 100644 --- a/pallets/commitments/Cargo.toml +++ b/pallets/commitments/Cargo.toml @@ -39,6 +39,8 @@ sha2 = { workspace = true } log = { workspace = true } +pallet-subtensor = { path = "../subtensor", default-features = false } + [dev-dependencies] sp-core = { workspace = true } sp-io = { workspace = true } @@ -65,7 +67,8 @@ std = [ "w3f-bls/std", "hex/std", "rand_chacha/std", - "sha2/std" + "sha2/std", + "pallet-subtensor/std" ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", @@ -73,12 +76,14 @@ runtime-benchmarks = [ "frame-system/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "pallet-balances/runtime-benchmarks", - "pallet-drand/runtime-benchmarks" + "pallet-drand/runtime-benchmarks", + "pallet-subtensor/runtime-benchmarks" ] try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", "pallet-balances/try-runtime", "sp-runtime/try-runtime", - "pallet-drand/try-runtime" + "pallet-drand/try-runtime", + "pallet-subtensor/try-runtime" ] diff --git a/pallets/commitments/src/lib.rs b/pallets/commitments/src/lib.rs index 850fe23e3..8fa467f7c 100644 --- a/pallets/commitments/src/lib.rs +++ b/pallets/commitments/src/lib.rs @@ -70,6 +70,15 @@ pub mod pallet { /// The rate limit for commitments #[pallet::constant] type DefaultRateLimit: Get>; + + /// Used to retreive the given subnet's tempo + type TempoInterface: GetTempoInterface; + } + + /// Used to retreive the given subnet's tempo + pub trait GetTempoInterface { + /// Used to retreive the given subnet's tempo + fn get_tempo_for_netuid(netuid: u16) -> u16; } #[pallet::event] @@ -108,6 +117,8 @@ pub mod pallet { AccountNotAllowedCommit, /// Account is trying to commit data too fast, rate limit exceeded CommitmentSetRateLimitExceeded, + /// Space Limit Exceeded for the current interval + SpaceLimitExceeded, } #[pallet::type_value] @@ -162,15 +173,39 @@ pub mod pallet { OptionQuery, >; + /// Maps (netuid, who) -> usage (how many “bytes” they've committed) + /// in the RateLimit window + #[pallet::storage] + #[pallet::getter(fn used_space_of)] + pub type UsedSpaceOf = StorageDoubleMap< + _, + Identity, + u16, + Twox64Concat, + T::AccountId, + UsageTracker>, + OptionQuery, + >; + + #[pallet::type_value] + /// The default Maximum Space + pub fn DefaultMaxSpace() -> u32 { + 3100 + } + + #[pallet::storage] + #[pallet::getter(fn max_space_per_user_per_rate_limit)] + pub type MaxSpace = StorageValue<_, u32, ValueQuery, DefaultMaxSpace>; + #[pallet::call] impl Pallet { /// Set the commitment for a given netuid #[pallet::call_index(0)] #[pallet::weight(( - ::WeightInfo::set_commitment(), - DispatchClass::Operational, - Pays::No - ))] + ::WeightInfo::set_commitment(), + DispatchClass::Operational, + Pays::No + ))] pub fn set_commitment( origin: OriginFor, netuid: u16, @@ -189,14 +224,26 @@ pub mod pallet { ); let cur_block = >::block_number(); - if let Some(last_commit) = >::get(netuid, &who) { - ensure!( - cur_block >= last_commit.saturating_add(RateLimit::::get()), - Error::::CommitmentSetRateLimitExceeded - ); + + let required_space = info.using_encoded(|b| b.len()) as u64; + + let mut usage = UsedSpaceOf::::get(netuid, &who).unwrap_or_default(); + let tempo_length = T::TempoInterface::get_tempo_for_netuid(netuid); + + if cur_block.saturating_sub(usage.last_reset_block) >= tempo_length.into() { + usage.last_reset_block = cur_block; + usage.used_space = 0; } - let fd = >::from(extra_fields).saturating_mul(T::FieldDeposit::get()); + let max_allowed = MaxSpace::::get() as u64; + ensure!( + usage.used_space + required_space <= max_allowed, + Error::::SpaceLimitExceeded + ); + + usage.used_space += required_space; + UsedSpaceOf::::insert(netuid, &who, usage); + let mut id = match >::get(netuid, &who) { Some(mut id) => { id.info = *info.clone(); @@ -211,6 +258,7 @@ pub mod pallet { }; let old_deposit = id.deposit; + let fd = >::from(extra_fields).saturating_mul(T::FieldDeposit::get()); id.deposit = T::InitialDeposit::get().saturating_add(fd); if id.deposit > old_deposit { T::Currency::reserve(&who, id.deposit.saturating_sub(old_deposit))?; @@ -264,6 +312,36 @@ pub mod pallet { RateLimit::::set(rate_limit_blocks.into()); Ok(()) } + + /// Sudo-set MaxSpace + #[pallet::call_index(2)] + #[pallet::weight(( + ::WeightInfo::set_rate_limit(), + DispatchClass::Operational, + Pays::No + ))] + pub fn set_max_space_per_user_per_rate_limit( + origin: OriginFor, + new_limit: u32, + ) -> DispatchResult { + ensure_root(origin)?; + MaxSpace::::set(new_limit); + Ok(()) + } + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(n: BlockNumberFor) -> Weight { + if let Err(e) = Self::reveal_timelocked_commitments() { + log::debug!( + "Failed to unveil matured commitments on block {:?}: {:?}", + n, + e + ); + } + Weight::from_parts(0, 0) + } } } diff --git a/pallets/commitments/src/mock.rs b/pallets/commitments/src/mock.rs index 5a4b3cace..88885a073 100644 --- a/pallets/commitments/src/mock.rs +++ b/pallets/commitments/src/mock.rs @@ -101,6 +101,14 @@ impl pallet_commitments::Config for Test { type FieldDeposit = ConstU64<0>; type InitialDeposit = ConstU64<0>; type DefaultRateLimit = ConstU64<0>; + type TempoInterface = MockTempoInterface; +} + +pub struct MockTempoInterface; +impl pallet_commitments::GetTempoInterface for MockTempoInterface { + fn get_tempo_for_netuid(_netuid: u16) -> u16 { + 360 + } } impl pallet_drand::Config for Test { diff --git a/pallets/commitments/src/types.rs b/pallets/commitments/src/types.rs index 400c10244..735d085a9 100644 --- a/pallets/commitments/src/types.rs +++ b/pallets/commitments/src/types.rs @@ -346,6 +346,16 @@ pub struct RevealedData, BlockNumber> { pub deposit: Balance, } +/// Tracks how much “space” each (netuid, who) has used within the current RateLimit block-window. +#[freeze_struct("c73c7815f7c51556")] +#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, TypeInfo)] +pub struct UsageTracker { + /// Last Reset block + pub last_reset_block: BlockNumber, + /// Space used + pub used_space: u64, +} + /// Information concerning the identity of the controller of an account. /// /// NOTE: This is stored separately primarily to facilitate the addition of extra fields in a diff --git a/pallets/subtensor/Cargo.toml b/pallets/subtensor/Cargo.toml index 42b468b1d..f240245c4 100644 --- a/pallets/subtensor/Cargo.toml +++ b/pallets/subtensor/Cargo.toml @@ -56,8 +56,6 @@ w3f-bls = { workspace = true, default-features = false } sha2 = { workspace = true } rand_chacha = { workspace = true } -pallet-commitments = { default-features = false, path = "../commitments" } - [dev-dependencies] pallet-balances = { workspace = true, features = ["std"] } pallet-scheduler = { workspace = true } @@ -109,8 +107,7 @@ std = [ "rand_chacha/std", "safe-math/std", "sha2/std", - "share-pool/std", - "pallet-commitments/std" + "share-pool/std" ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", @@ -123,8 +120,7 @@ runtime-benchmarks = [ "pallet-collective/runtime-benchmarks", "pallet-preimage/runtime-benchmarks", "pallet-scheduler/runtime-benchmarks", - "pallet-drand/runtime-benchmarks", - "pallet-commitments/runtime-benchmarks" + "pallet-drand/runtime-benchmarks" ] try-runtime = [ "frame-support/try-runtime", @@ -137,8 +133,7 @@ try-runtime = [ "pallet-utility/try-runtime", "sp-runtime/try-runtime", "pallet-collective/try-runtime", - "pallet-drand/try-runtime", - "pallet-commitments/try-runtime" + "pallet-drand/try-runtime" ] pow-faucet = [] fast-blocks = [] diff --git a/pallets/subtensor/src/coinbase/block_step.rs b/pallets/subtensor/src/coinbase/block_step.rs index 6b650b761..2eb7ec2fb 100644 --- a/pallets/subtensor/src/coinbase/block_step.rs +++ b/pallets/subtensor/src/coinbase/block_step.rs @@ -19,15 +19,6 @@ impl Pallet { // --- 4. Set pending children on the epoch; but only after the coinbase has been run. Self::try_set_pending_children(block_number); - // --- 5. Unveil all matured timelocked entries - if let Err(e) = Self::reveal_timelocked_commitments() { - log::debug!( - "Failed to unveil matured commitments on block {} due to error: {:?}", - block_number, - e - ); - } - // Return ok. Ok(()) } diff --git a/pallets/subtensor/src/macros/config.rs b/pallets/subtensor/src/macros/config.rs index 17d5b9345..af448c877 100644 --- a/pallets/subtensor/src/macros/config.rs +++ b/pallets/subtensor/src/macros/config.rs @@ -47,9 +47,6 @@ mod config { /// the preimage to store the call data. type Preimages: QueryPreimage + StorePreimage; - /// The commitment pallet's runtime - type CommitmentRuntime: pallet_commitments::Config; - /// ================================= /// ==== Initial Value Constants ==== /// ================================= diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index 588fcab4d..0d979a612 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -38,7 +38,6 @@ frame_support::construct_runtime!( Scheduler: pallet_scheduler::{Pallet, Call, Storage, Event} = 9, Preimage: pallet_preimage::{Pallet, Call, Storage, Event} = 10, Drand: pallet_drand::{Pallet, Call, Storage, Event} = 11, - Commitments: pallet_commitments::{Pallet, Call, Storage, Event} = 12, } ); @@ -408,17 +407,9 @@ impl crate::Config for Test { type InitialColdkeySwapScheduleDuration = InitialColdkeySwapScheduleDuration; type InitialDissolveNetworkScheduleDuration = InitialDissolveNetworkScheduleDuration; type InitialTaoWeight = InitialTaoWeight; - type CommitmentRuntime = Test; type InitialEmaPriceHalvingPeriod = InitialEmaPriceHalvingPeriod; } -parameter_types! { - pub const MaxCommitFieldsInner: u32 = 1; - pub const CommitmentInitialDeposit: Balance = 0; - pub const CommitmentFieldDeposit: Balance = 0; - pub const CommitmentRateLimit: BlockNumber = 100; -} - pub struct OriginPrivilegeCmp; impl PrivilegeCmp for OriginPrivilegeCmp { @@ -540,33 +531,6 @@ impl frame_system::offchain::CreateSignedTransaction> f } } -#[freeze_struct("7c76bd954afbb54e")] -#[derive(Clone, Eq, PartialEq, Encode, Decode, TypeInfo)] -pub struct MaxCommitFields; -impl Get for MaxCommitFields { - fn get() -> u32 { - MaxCommitFieldsInner::get() - } -} - -pub struct AllowCommitments; -impl pallet_commitments::CanCommit for AllowCommitments { - fn can_commit(_netuid: u16, _address: &AccountId) -> bool { - true - } -} - -impl pallet_commitments::Config for Test { - type RuntimeEvent = RuntimeEvent; - type Currency = Balances; - type WeightInfo = pallet_commitments::weights::SubstrateWeight; - type CanCommit = AllowCommitments; - type MaxFields = MaxCommitFields; - type InitialDeposit = CommitmentInitialDeposit; - type FieldDeposit = CommitmentFieldDeposit; - type DefaultRateLimit = CommitmentRateLimit; -} - #[allow(dead_code)] // Build genesis storage according to the mock runtime. pub fn new_test_ext(block_number: BlockNumber) -> sp_io::TestExternalities { diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index d8481a002..19d07248d 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -734,10 +734,6 @@ impl Pallet { Self::deposit_event(Event::SubnetOwnerHotkeySet(netuid, hotkey.clone())); } - pub fn reveal_timelocked_commitments() -> DispatchResult { - pallet_commitments::Pallet::::reveal_timelocked_commitments() - } - // Get the uid of the Owner Hotkey for a subnet. pub fn get_owner_uid(netuid: u16) -> Option { match SubnetOwnerHotkey::::try_get(netuid) { diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index f8bc81f38..db151817d 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -960,6 +960,20 @@ impl pallet_commitments::Config for Runtime { type InitialDeposit = CommitmentInitialDeposit; type FieldDeposit = CommitmentFieldDeposit; type DefaultRateLimit = CommitmentRateLimit; + type TempoInterface = TempoInterface; +} + +pub struct TempoInterface; +impl pallet_commitments::GetTempoInterface for TempoInterface { + fn get_tempo_for_netuid(_netuid: u16) -> u16 { + 360 + } +} + +impl pallet_commitments::GetTempoInterface for Runtime { + fn get_tempo_for_netuid(netuid: u16) -> u16 { + pallet_subtensor::Tempo::::get(netuid) + } } #[cfg(not(feature = "fast-blocks"))] @@ -1093,7 +1107,6 @@ impl pallet_subtensor::Config for Runtime { type Preimages = Preimage; type InitialColdkeySwapScheduleDuration = InitialColdkeySwapScheduleDuration; type InitialDissolveNetworkScheduleDuration = InitialDissolveNetworkScheduleDuration; - type CommitmentRuntime = Runtime; type InitialEmaPriceHalvingPeriod = InitialEmaPriceHalvingPeriod; } From 5e70dc1fc770e41841f0448d9373d1e6d1d1aadb Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Thu, 13 Mar 2025 11:08:55 -0700 Subject: [PATCH 17/28] clippy --- pallets/commitments/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pallets/commitments/src/lib.rs b/pallets/commitments/src/lib.rs index 8fa467f7c..9a5f4fa75 100644 --- a/pallets/commitments/src/lib.rs +++ b/pallets/commitments/src/lib.rs @@ -237,11 +237,12 @@ pub mod pallet { let max_allowed = MaxSpace::::get() as u64; ensure!( - usage.used_space + required_space <= max_allowed, + usage.used_space.saturating_add(required_space) <= max_allowed, Error::::SpaceLimitExceeded ); - usage.used_space += required_space; + usage.used_space = usage.used_space.saturating_add(required_space); + UsedSpaceOf::::insert(netuid, &who, usage); let mut id = match >::get(netuid, &who) { From b441d87b59b3e6ab5940cf0834593b32e4d44842 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Thu, 13 Mar 2025 11:52:56 -0700 Subject: [PATCH 18/28] fix `required_space` --- pallets/commitments/src/lib.rs | 6 +++++- pallets/commitments/src/types.rs | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/pallets/commitments/src/lib.rs b/pallets/commitments/src/lib.rs index 9a5f4fa75..81fd0ed64 100644 --- a/pallets/commitments/src/lib.rs +++ b/pallets/commitments/src/lib.rs @@ -225,7 +225,11 @@ pub mod pallet { let cur_block = >::block_number(); - let required_space = info.using_encoded(|b| b.len()) as u64; + let required_space: u64 = info + .fields + .iter() + .map(|field| field.len_for_rate_limit()) + .sum(); let mut usage = UsedSpaceOf::::get(netuid, &who).unwrap_or_default(); let tempo_length = T::TempoInterface::get_tempo_for_netuid(netuid); diff --git a/pallets/commitments/src/types.rs b/pallets/commitments/src/types.rs index 735d085a9..e90b8ade3 100644 --- a/pallets/commitments/src/types.rs +++ b/pallets/commitments/src/types.rs @@ -69,6 +69,18 @@ impl Data { pub fn is_timelock_encrypted(&self) -> bool { matches!(self, Data::TimelockEncrypted { .. }) } + + pub fn len_for_rate_limit(&self) -> u64 { + match self { + Data::None => 0, + Data::Raw(bytes) => bytes.len() as u64, + Data::BlakeTwo256(arr) + | Data::Sha256(arr) + | Data::Keccak256(arr) + | Data::ShaThree256(arr) => arr.len() as u64, + Data::TimelockEncrypted { encrypted, .. } => encrypted.len() as u64, + } + } } impl Decode for Data { From c94ea1fe8f1927c14cbbb71b1cea039268a11676 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Thu, 13 Mar 2025 11:53:05 -0700 Subject: [PATCH 19/28] add tests --- pallets/commitments/src/tests.rs | 282 +++++++++++++++++++++++++++---- 1 file changed, 250 insertions(+), 32 deletions(-) diff --git a/pallets/commitments/src/tests.rs b/pallets/commitments/src/tests.rs index bcb775f9c..971fa66ad 100644 --- a/pallets/commitments/src/tests.rs +++ b/pallets/commitments/src/tests.rs @@ -3,7 +3,7 @@ use sp_std::prelude::*; #[cfg(test)] use crate::{ - CommitmentInfo, CommitmentOf, Config, Data, Error, Event, Pallet, RateLimit, + CommitmentInfo, CommitmentOf, Config, Data, Error, Event, MaxSpace, Pallet, RateLimit, RevealedCommitments, TimelockedIndex, mock::{ DRAND_QUICKNET_SIG_HEX, RuntimeEvent, RuntimeOrigin, Test, insert_drand_pulse, @@ -146,37 +146,38 @@ fn set_commitment_too_many_fields_panics() { }); } -#[test] -fn set_commitment_rate_limit_exceeded() { - new_test_ext().execute_with(|| { - let rate_limit = ::DefaultRateLimit::get(); - System::::set_block_number(1); - let info = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![]).expect("Expected not to panic"), - }); - - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(1), - 1, - info.clone() - )); - - // Set block number to just before rate limit expires - System::::set_block_number(rate_limit); - assert_noop!( - Pallet::::set_commitment(RuntimeOrigin::signed(1), 1, info.clone()), - Error::::CommitmentSetRateLimitExceeded - ); - - // Set block number to after rate limit - System::::set_block_number(rate_limit + 1); - assert_ok!(Pallet::::set_commitment( - RuntimeOrigin::signed(1), - 1, - info - )); - }); -} +// DEPRECATED +// #[test] +// fn set_commitment_rate_limit_exceeded() { +// new_test_ext().execute_with(|| { +// let rate_limit = ::DefaultRateLimit::get(); +// System::::set_block_number(1); +// let info = Box::new(CommitmentInfo { +// fields: BoundedVec::try_from(vec![]).expect("Expected not to panic"), +// }); + +// assert_ok!(Pallet::::set_commitment( +// RuntimeOrigin::signed(1), +// 1, +// info.clone() +// )); + +// // Set block number to just before rate limit expires +// System::::set_block_number(rate_limit); +// assert_noop!( +// Pallet::::set_commitment(RuntimeOrigin::signed(1), 1, info.clone()), +// Error::::CommitmentSetRateLimitExceeded +// ); + +// // Set block number to after rate limit +// System::::set_block_number(rate_limit + 1); +// assert_ok!(Pallet::::set_commitment( +// RuntimeOrigin::signed(1), +// 1, +// info +// )); +// }); +// } #[test] fn set_commitment_updates_deposit() { @@ -908,3 +909,220 @@ fn single_timelock_reveal_later_round() { ); }); } + +#[test] +fn tempo_based_space_limit_accumulates_in_same_window() { + new_test_ext().execute_with(|| { + let netuid = 1; + let who = 100; + let space_limit = 50; + MaxSpace::::set(space_limit); + System::::set_block_number(0); + + // A single commitment that uses some space, e.g. 30 bytes: + let data = vec![0u8; 30]; + let info = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![Data::Raw( + data.try_into().expect("Data up to 128 bytes OK"), + )]) + .expect("1 field is <= MaxFields"), + }); + + // 2) First call => usage=0 => usage=30 after. OK. + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + info.clone(), + )); + + // 3) Second call => tries another 30 bytes in the SAME block => total=60 => exceeds 50 => should fail. + assert_noop!( + Pallet::::set_commitment(RuntimeOrigin::signed(who), netuid, info.clone()), + Error::::SpaceLimitExceeded + ); + }); +} + +#[test] +fn tempo_based_space_limit_resets_after_tempo() { + new_test_ext().execute_with(|| { + let netuid = 2; + let who = 101; + + MaxSpace::::set(40); + System::::set_block_number(1); + + let commit_small = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![Data::Raw(vec![0u8; 20].try_into().unwrap())]) + .unwrap(), + }); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + commit_small.clone() + )); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + commit_small.clone() + )); + + assert_noop!( + Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + commit_small.clone() + ), + Error::::SpaceLimitExceeded + ); + + System::::set_block_number(200); + + assert_noop!( + Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + commit_small.clone() + ), + Error::::SpaceLimitExceeded + ); + + System::::set_block_number(400); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + commit_small + )); + }); +} + +#[test] +fn tempo_based_space_limit_does_not_affect_different_netuid() { + new_test_ext().execute_with(|| { + let netuid_a = 10; + let netuid_b = 20; + let who = 111; + let space_limit = 50; + MaxSpace::::set(space_limit); + + let commit_large = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![Data::Raw(vec![0u8; 40].try_into().unwrap())]) + .unwrap(), + }); + let commit_small = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![Data::Raw(vec![0u8; 20].try_into().unwrap())]) + .unwrap(), + }); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid_a, + commit_large.clone() + )); + + assert_noop!( + Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid_a, + commit_small.clone() + ), + Error::::SpaceLimitExceeded + ); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid_b, + commit_large + )); + + assert_noop!( + Pallet::::set_commitment(RuntimeOrigin::signed(who), netuid_b, commit_small), + Error::::SpaceLimitExceeded + ); + }); +} + +#[test] +fn tempo_based_space_limit_does_not_affect_different_user() { + new_test_ext().execute_with(|| { + let netuid = 10; + let user1 = 123; + let user2 = 456; + let space_limit = 50; + MaxSpace::::set(space_limit); + + let commit_large = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![Data::Raw(vec![0u8; 40].try_into().unwrap())]) + .unwrap(), + }); + let commit_small = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![Data::Raw(vec![0u8; 20].try_into().unwrap())]) + .unwrap(), + }); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(user1), + netuid, + commit_large.clone() + )); + + assert_noop!( + Pallet::::set_commitment( + RuntimeOrigin::signed(user1), + netuid, + commit_small.clone() + ), + Error::::SpaceLimitExceeded + ); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(user2), + netuid, + commit_large + )); + + assert_noop!( + Pallet::::set_commitment(RuntimeOrigin::signed(user2), netuid, commit_small), + Error::::SpaceLimitExceeded + ); + }); +} + +#[test] +fn tempo_based_space_limit_sudo_set_max_space() { + new_test_ext().execute_with(|| { + let netuid = 3; + let who = 15; + MaxSpace::::set(30); + + System::::set_block_number(1); + let commit_25 = Box::new(CommitmentInfo { + fields: BoundedVec::try_from(vec![Data::Raw(vec![0u8; 25].try_into().unwrap())]) + .unwrap(), + }); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + commit_25.clone() + )); + assert_noop!( + Pallet::::set_commitment(RuntimeOrigin::signed(who), netuid, commit_25.clone()), + Error::::SpaceLimitExceeded + ); + + assert_ok!(Pallet::::set_max_space_per_user_per_rate_limit( + RuntimeOrigin::root(), + 100 + )); + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + commit_25 + )); + }); +} From 501baa2eaaf11cf706663cdfe215901d4fb81628 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Thu, 13 Mar 2025 11:56:03 -0700 Subject: [PATCH 20/28] clippy --- pallets/commitments/src/tests.rs | 36 +++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/pallets/commitments/src/tests.rs b/pallets/commitments/src/tests.rs index 971fa66ad..9460b2c69 100644 --- a/pallets/commitments/src/tests.rs +++ b/pallets/commitments/src/tests.rs @@ -953,8 +953,10 @@ fn tempo_based_space_limit_resets_after_tempo() { System::::set_block_number(1); let commit_small = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![Data::Raw(vec![0u8; 20].try_into().unwrap())]) - .unwrap(), + fields: BoundedVec::try_from(vec![Data::Raw( + vec![0u8; 20].try_into().expect("expected ok"), + )]) + .expect("expected ok"), }); assert_ok!(Pallet::::set_commitment( @@ -1009,12 +1011,16 @@ fn tempo_based_space_limit_does_not_affect_different_netuid() { MaxSpace::::set(space_limit); let commit_large = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![Data::Raw(vec![0u8; 40].try_into().unwrap())]) - .unwrap(), + fields: BoundedVec::try_from(vec![Data::Raw( + vec![0u8; 40].try_into().expect("expected ok"), + )]) + .expect("expected ok"), }); let commit_small = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![Data::Raw(vec![0u8; 20].try_into().unwrap())]) - .unwrap(), + fields: BoundedVec::try_from(vec![Data::Raw( + vec![0u8; 20].try_into().expect("expected ok"), + )]) + .expect("expected ok"), }); assert_ok!(Pallet::::set_commitment( @@ -1055,12 +1061,16 @@ fn tempo_based_space_limit_does_not_affect_different_user() { MaxSpace::::set(space_limit); let commit_large = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![Data::Raw(vec![0u8; 40].try_into().unwrap())]) - .unwrap(), + fields: BoundedVec::try_from(vec![Data::Raw( + vec![0u8; 40].try_into().expect("expected ok"), + )]) + .expect("expected ok"), }); let commit_small = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![Data::Raw(vec![0u8; 20].try_into().unwrap())]) - .unwrap(), + fields: BoundedVec::try_from(vec![Data::Raw( + vec![0u8; 20].try_into().expect("expected ok"), + )]) + .expect("expected ok"), }); assert_ok!(Pallet::::set_commitment( @@ -1100,8 +1110,10 @@ fn tempo_based_space_limit_sudo_set_max_space() { System::::set_block_number(1); let commit_25 = Box::new(CommitmentInfo { - fields: BoundedVec::try_from(vec![Data::Raw(vec![0u8; 25].try_into().unwrap())]) - .unwrap(), + fields: BoundedVec::try_from(vec![Data::Raw( + vec![0u8; 25].try_into().expect("expected ok"), + )]) + .expect("expected ok"), }); assert_ok!(Pallet::::set_commitment( From 2376741b05312c2f55cf84a4930d7ddbeb6846f6 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Thu, 13 Mar 2025 11:59:17 -0700 Subject: [PATCH 21/28] revert unneeded --- Cargo.lock | 1 - pallets/admin-utils/Cargo.toml | 10 +++------- pallets/drand/src/lib.rs | 2 +- pallets/subtensor/src/coinbase/block_step.rs | 1 - 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 57227ec93..51a10fdf0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5991,7 +5991,6 @@ dependencies = [ "frame-system", "log", "pallet-balances", - "pallet-commitments", "pallet-drand", "pallet-evm-chain-id", "pallet-grandpa", diff --git a/pallets/admin-utils/Cargo.toml b/pallets/admin-utils/Cargo.toml index d8a413977..587f9daa3 100644 --- a/pallets/admin-utils/Cargo.toml +++ b/pallets/admin-utils/Cargo.toml @@ -32,7 +32,6 @@ substrate-fixed = { workspace = true } pallet-evm-chain-id = { workspace = true } pallet-drand = { workspace = true, default-features = false } sp-consensus-grandpa = { workspace = true } -pallet-commitments = { default-features = false, path = "../commitments" } [dev-dependencies] sp-core = { workspace = true } @@ -67,8 +66,7 @@ std = [ "sp-std/std", "sp-tracing/std", "sp-weights/std", - "substrate-fixed/std", - "pallet-commitments/std" + "substrate-fixed/std" ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", @@ -79,8 +77,7 @@ runtime-benchmarks = [ "pallet-grandpa/runtime-benchmarks", "pallet-scheduler/runtime-benchmarks", "pallet-subtensor/runtime-benchmarks", - "sp-runtime/runtime-benchmarks", - "pallet-commitments/runtime-benchmarks" + "sp-runtime/runtime-benchmarks" ] try-runtime = [ "frame-support/try-runtime", @@ -91,6 +88,5 @@ try-runtime = [ "pallet-grandpa/try-runtime", "pallet-scheduler/try-runtime", "pallet-subtensor/try-runtime", - "sp-runtime/try-runtime", - "pallet-commitments/try-runtime" + "sp-runtime/try-runtime" ] diff --git a/pallets/drand/src/lib.rs b/pallets/drand/src/lib.rs index d9da28821..40bf7ccb9 100644 --- a/pallets/drand/src/lib.rs +++ b/pallets/drand/src/lib.rs @@ -219,7 +219,7 @@ pub mod pallet { pub type Pulses = StorageMap<_, Blake2_128Concat, RoundNumber, Pulse, OptionQuery>; #[pallet::storage] - pub type LastStoredRound = StorageValue<_, RoundNumber, ValueQuery>; + pub(super) type LastStoredRound = StorageValue<_, RoundNumber, ValueQuery>; /// Defines the block when next unsigned transaction will be accepted. /// diff --git a/pallets/subtensor/src/coinbase/block_step.rs b/pallets/subtensor/src/coinbase/block_step.rs index 2eb7ec2fb..669f8e09d 100644 --- a/pallets/subtensor/src/coinbase/block_step.rs +++ b/pallets/subtensor/src/coinbase/block_step.rs @@ -18,7 +18,6 @@ impl Pallet { Self::run_coinbase(block_emission); // --- 4. Set pending children on the epoch; but only after the coinbase has been run. Self::try_set_pending_children(block_number); - // Return ok. Ok(()) } From a0b33c7dcfc1e04e351de1a9c2cefa9c2706fe75 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Thu, 13 Mar 2025 12:00:46 -0700 Subject: [PATCH 22/28] revert unneeded change --- pallets/admin-utils/Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pallets/admin-utils/Cargo.toml b/pallets/admin-utils/Cargo.toml index 587f9daa3..b3c1410cc 100644 --- a/pallets/admin-utils/Cargo.toml +++ b/pallets/admin-utils/Cargo.toml @@ -66,7 +66,7 @@ std = [ "sp-std/std", "sp-tracing/std", "sp-weights/std", - "substrate-fixed/std" + "substrate-fixed/std", ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", @@ -77,7 +77,7 @@ runtime-benchmarks = [ "pallet-grandpa/runtime-benchmarks", "pallet-scheduler/runtime-benchmarks", "pallet-subtensor/runtime-benchmarks", - "sp-runtime/runtime-benchmarks" + "sp-runtime/runtime-benchmarks", ] try-runtime = [ "frame-support/try-runtime", @@ -88,5 +88,5 @@ try-runtime = [ "pallet-grandpa/try-runtime", "pallet-scheduler/try-runtime", "pallet-subtensor/try-runtime", - "sp-runtime/try-runtime" + "sp-runtime/try-runtime", ] From c1236c6f061e7ad1c598eb76375e743d99474f30 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Fri, 14 Mar 2025 09:54:11 -0700 Subject: [PATCH 23/28] rename set_max_space --- pallets/commitments/src/lib.rs | 5 +---- pallets/commitments/src/tests.rs | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/pallets/commitments/src/lib.rs b/pallets/commitments/src/lib.rs index 81fd0ed64..246073134 100644 --- a/pallets/commitments/src/lib.rs +++ b/pallets/commitments/src/lib.rs @@ -325,10 +325,7 @@ pub mod pallet { DispatchClass::Operational, Pays::No ))] - pub fn set_max_space_per_user_per_rate_limit( - origin: OriginFor, - new_limit: u32, - ) -> DispatchResult { + pub fn set_max_space(origin: OriginFor, new_limit: u32) -> DispatchResult { ensure_root(origin)?; MaxSpace::::set(new_limit); Ok(()) diff --git a/pallets/commitments/src/tests.rs b/pallets/commitments/src/tests.rs index 9460b2c69..f07b4ed68 100644 --- a/pallets/commitments/src/tests.rs +++ b/pallets/commitments/src/tests.rs @@ -1126,10 +1126,7 @@ fn tempo_based_space_limit_sudo_set_max_space() { Error::::SpaceLimitExceeded ); - assert_ok!(Pallet::::set_max_space_per_user_per_rate_limit( - RuntimeOrigin::root(), - 100 - )); + assert_ok!(Pallet::::set_max_space(RuntimeOrigin::root(), 100)); assert_ok!(Pallet::::set_commitment( RuntimeOrigin::signed(who), From 2b9a90f4de685157f3113d0241b801e0425e4803 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Fri, 14 Mar 2025 10:24:40 -0700 Subject: [PATCH 24/28] add test for reveal in blockstep --- pallets/commitments/src/tests.rs | 83 ++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/pallets/commitments/src/tests.rs b/pallets/commitments/src/tests.rs index f07b4ed68..0ce611122 100644 --- a/pallets/commitments/src/tests.rs +++ b/pallets/commitments/src/tests.rs @@ -12,6 +12,7 @@ use crate::{ }; use frame_support::{BoundedVec, assert_noop, assert_ok, traits::Get}; use frame_system::Pallet as System; +use frame_support::pallet_prelude::Hooks; #[allow(clippy::indexing_slicing)] #[test] @@ -1135,3 +1136,85 @@ fn tempo_based_space_limit_sudo_set_max_space() { )); }); } + +#[allow(clippy::indexing_slicing)] +#[test] +fn on_initialize_reveals_matured_timelocks() { + new_test_ext().execute_with(|| { + let who = 42; + let netuid = 7; + let reveal_round = 1000; + + let message_text = b"Timelock test via on_initialize"; + + let inner_fields: BoundedVec::MaxFields> = BoundedVec::try_from(vec![Data::Raw( + message_text + .to_vec() + .try_into() + .expect("<= 128 bytes is OK for Data::Raw"), + )]) + .expect("Should not exceed MaxFields"); + + let inner_info: CommitmentInfo<::MaxFields> = CommitmentInfo { + fields: inner_fields, + }; + + let plaintext = inner_info.encode(); + let encrypted = produce_ciphertext(&plaintext, reveal_round); + + let outer_fields = BoundedVec::try_from(vec![Data::TimelockEncrypted { + encrypted, + reveal_round, + }]) + .expect("One field is well under MaxFields"); + let info_outer = CommitmentInfo { fields: outer_fields }; + + System::::set_block_number(1); + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + Box::new(info_outer) + )); + + + assert!(CommitmentOf::::get(netuid, who).is_some()); + assert!( + TimelockedIndex::::get().contains(&(netuid, who)), + "Should appear in TimelockedIndex since it contains a timelock" + ); + + let drand_sig_hex = hex::decode(DRAND_QUICKNET_SIG_HEX) + .expect("Decoding DRAND_QUICKNET_SIG_HEX must not fail"); + insert_drand_pulse(reveal_round, &drand_sig_hex); + + assert!(RevealedCommitments::::get(netuid, who).is_none()); + + System::::set_block_number(2); + as Hooks>::on_initialize(2); + + let revealed_opt = RevealedCommitments::::get(netuid, who); + assert!( + revealed_opt.is_some(), + "Expected that the timelock got revealed at block #2" + ); + + let leftover = CommitmentOf::::get(netuid, who); + assert!( + leftover.is_none(), + "After revealing the only timelock, the entire commitment is removed." + ); + + assert!( + !TimelockedIndex::::get().contains(&(netuid, who)), + "No longer in TimelockedIndex after reveal." + ); + + let revealed_data = revealed_opt.expect("expected to not panic"); + assert_eq!(revealed_data.info.fields.len(), 1); + if let Data::Raw(bound_bytes) = &revealed_data.info.fields[0] { + assert_eq!(bound_bytes.as_slice(), message_text); + } else { + panic!("Expected a Data::Raw variant in revealed data."); + } + }); +} From a245655aec62a5de02e1fb6063cece1411abbfb3 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Fri, 14 Mar 2025 10:27:54 -0700 Subject: [PATCH 25/28] fmt --- pallets/commitments/src/tests.rs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/pallets/commitments/src/tests.rs b/pallets/commitments/src/tests.rs index 0ce611122..9de87a8b5 100644 --- a/pallets/commitments/src/tests.rs +++ b/pallets/commitments/src/tests.rs @@ -10,9 +10,9 @@ use crate::{ new_test_ext, produce_ciphertext, }, }; +use frame_support::pallet_prelude::Hooks; use frame_support::{BoundedVec, assert_noop, assert_ok, traits::Get}; use frame_system::Pallet as System; -use frame_support::pallet_prelude::Hooks; #[allow(clippy::indexing_slicing)] #[test] @@ -1147,17 +1147,18 @@ fn on_initialize_reveals_matured_timelocks() { let message_text = b"Timelock test via on_initialize"; - let inner_fields: BoundedVec::MaxFields> = BoundedVec::try_from(vec![Data::Raw( - message_text - .to_vec() - .try_into() - .expect("<= 128 bytes is OK for Data::Raw"), - )]) - .expect("Should not exceed MaxFields"); - + let inner_fields: BoundedVec::MaxFields> = + BoundedVec::try_from(vec![Data::Raw( + message_text + .to_vec() + .try_into() + .expect("<= 128 bytes is OK for Data::Raw"), + )]) + .expect("Should not exceed MaxFields"); + let inner_info: CommitmentInfo<::MaxFields> = CommitmentInfo { fields: inner_fields, - }; + }; let plaintext = inner_info.encode(); let encrypted = produce_ciphertext(&plaintext, reveal_round); @@ -1167,7 +1168,9 @@ fn on_initialize_reveals_matured_timelocks() { reveal_round, }]) .expect("One field is well under MaxFields"); - let info_outer = CommitmentInfo { fields: outer_fields }; + let info_outer = CommitmentInfo { + fields: outer_fields, + }; System::::set_block_number(1); assert_ok!(Pallet::::set_commitment( @@ -1176,7 +1179,6 @@ fn on_initialize_reveals_matured_timelocks() { Box::new(info_outer) )); - assert!(CommitmentOf::::get(netuid, who).is_some()); assert!( TimelockedIndex::::get().contains(&(netuid, who)), From b0eb8bb698382ab6b07715fd82f6f79e12091b14 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Fri, 14 Mar 2025 12:36:42 -0700 Subject: [PATCH 26/28] commitments data limit follows tempo exactly --- pallets/commitments/src/lib.rs | 23 +++++++++-------------- pallets/commitments/src/mock.rs | 4 ++-- pallets/commitments/src/tests.rs | 4 +++- pallets/commitments/src/types.rs | 8 ++++---- runtime/src/lib.rs | 8 ++++---- 5 files changed, 22 insertions(+), 25 deletions(-) diff --git a/pallets/commitments/src/lib.rs b/pallets/commitments/src/lib.rs index 246073134..a136a2a7e 100644 --- a/pallets/commitments/src/lib.rs +++ b/pallets/commitments/src/lib.rs @@ -18,6 +18,7 @@ pub use weights::WeightInfo; use ark_serialize::CanonicalDeserialize; use frame_support::{BoundedVec, traits::Currency}; use scale_info::prelude::collections::BTreeSet; +use sp_runtime::SaturatedConversion; use sp_runtime::{Saturating, traits::Zero}; use sp_std::{boxed::Box, vec::Vec}; use tle::{ @@ -77,8 +78,8 @@ pub mod pallet { /// Used to retreive the given subnet's tempo pub trait GetTempoInterface { - /// Used to retreive the given subnet's tempo - fn get_tempo_for_netuid(netuid: u16) -> u16; + /// Used to retreive the epoch index for the given subnet. + fn get_epoch_index(netuid: u16, cur_block: u64) -> u64; } #[pallet::event] @@ -177,15 +178,8 @@ pub mod pallet { /// in the RateLimit window #[pallet::storage] #[pallet::getter(fn used_space_of)] - pub type UsedSpaceOf = StorageDoubleMap< - _, - Identity, - u16, - Twox64Concat, - T::AccountId, - UsageTracker>, - OptionQuery, - >; + pub type UsedSpaceOf = + StorageDoubleMap<_, Identity, u16, Twox64Concat, T::AccountId, UsageTracker, OptionQuery>; #[pallet::type_value] /// The default Maximum Space @@ -232,10 +226,11 @@ pub mod pallet { .sum(); let mut usage = UsedSpaceOf::::get(netuid, &who).unwrap_or_default(); - let tempo_length = T::TempoInterface::get_tempo_for_netuid(netuid); + let cur_block_u64 = cur_block.saturated_into::(); + let current_epoch = T::TempoInterface::get_epoch_index(netuid, cur_block_u64); - if cur_block.saturating_sub(usage.last_reset_block) >= tempo_length.into() { - usage.last_reset_block = cur_block; + if usage.last_epoch != current_epoch { + usage.last_epoch = current_epoch; usage.used_space = 0; } diff --git a/pallets/commitments/src/mock.rs b/pallets/commitments/src/mock.rs index 88885a073..8b99faf06 100644 --- a/pallets/commitments/src/mock.rs +++ b/pallets/commitments/src/mock.rs @@ -106,8 +106,8 @@ impl pallet_commitments::Config for Test { pub struct MockTempoInterface; impl pallet_commitments::GetTempoInterface for MockTempoInterface { - fn get_tempo_for_netuid(_netuid: u16) -> u16 { - 360 + fn get_epoch_index(netuid: u16, cur_block: u64) -> u64 { + SubtensorModule::get_epoch_index(netuid, cur_block) } } diff --git a/pallets/commitments/src/tests.rs b/pallets/commitments/src/tests.rs index 9de87a8b5..00a661a61 100644 --- a/pallets/commitments/src/tests.rs +++ b/pallets/commitments/src/tests.rs @@ -950,6 +950,8 @@ fn tempo_based_space_limit_resets_after_tempo() { let netuid = 2; let who = 101; + //TODO SPIIGOT: make this line work + //pallet_subtensor::Tempo::::insert(netuid, 360); MaxSpace::::set(40); System::::set_block_number(1); @@ -992,7 +994,7 @@ fn tempo_based_space_limit_resets_after_tempo() { Error::::SpaceLimitExceeded ); - System::::set_block_number(400); + System::::set_block_number(363); assert_ok!(Pallet::::set_commitment( RuntimeOrigin::signed(who), diff --git a/pallets/commitments/src/types.rs b/pallets/commitments/src/types.rs index e90b8ade3..0f1d2302a 100644 --- a/pallets/commitments/src/types.rs +++ b/pallets/commitments/src/types.rs @@ -359,11 +359,11 @@ pub struct RevealedData, BlockNumber> { } /// Tracks how much “space” each (netuid, who) has used within the current RateLimit block-window. -#[freeze_struct("c73c7815f7c51556")] +#[freeze_struct("1f23fb50f96326e4")] #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, TypeInfo)] -pub struct UsageTracker { - /// Last Reset block - pub last_reset_block: BlockNumber, +pub struct UsageTracker { + /// Last epoch block + pub last_epoch: u64, /// Space used pub used_space: u64, } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index db151817d..8434e2faf 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -965,14 +965,14 @@ impl pallet_commitments::Config for Runtime { pub struct TempoInterface; impl pallet_commitments::GetTempoInterface for TempoInterface { - fn get_tempo_for_netuid(_netuid: u16) -> u16 { - 360 + fn get_epoch_index(netuid: u16, cur_block: u64) -> u64 { + SubtensorModule::get_epoch_index(netuid, cur_block) } } impl pallet_commitments::GetTempoInterface for Runtime { - fn get_tempo_for_netuid(netuid: u16) -> u16 { - pallet_subtensor::Tempo::::get(netuid) + fn get_epoch_index(netuid: u16, cur_block: u64) -> u64 { + SubtensorModule::get_epoch_index(netuid, cur_block) } } From c1015daf3dc38d6a2e8519023eaaacff56a8020d Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Fri, 14 Mar 2025 13:06:40 -0700 Subject: [PATCH 27/28] fix mock --- pallets/commitments/src/mock.rs | 7 ++++++- pallets/commitments/src/tests.rs | 4 +--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pallets/commitments/src/mock.rs b/pallets/commitments/src/mock.rs index 8b99faf06..cc2482ff8 100644 --- a/pallets/commitments/src/mock.rs +++ b/pallets/commitments/src/mock.rs @@ -107,7 +107,12 @@ impl pallet_commitments::Config for Test { pub struct MockTempoInterface; impl pallet_commitments::GetTempoInterface for MockTempoInterface { fn get_epoch_index(netuid: u16, cur_block: u64) -> u64 { - SubtensorModule::get_epoch_index(netuid, cur_block) + let tempo = 360; // TODO: configure SubtensorModule in this mock + let tempo_plus_one: u64 = tempo.saturating_add(1); + let netuid_plus_one: u64 = (netuid as u64).saturating_add(1); + let block_with_offset: u64 = cur_block.saturating_add(netuid_plus_one); + + block_with_offset.checked_div(tempo_plus_one).unwrap_or(0) } } diff --git a/pallets/commitments/src/tests.rs b/pallets/commitments/src/tests.rs index 00a661a61..e6bf38c44 100644 --- a/pallets/commitments/src/tests.rs +++ b/pallets/commitments/src/tests.rs @@ -950,8 +950,6 @@ fn tempo_based_space_limit_resets_after_tempo() { let netuid = 2; let who = 101; - //TODO SPIIGOT: make this line work - //pallet_subtensor::Tempo::::insert(netuid, 360); MaxSpace::::set(40); System::::set_block_number(1); @@ -994,7 +992,7 @@ fn tempo_based_space_limit_resets_after_tempo() { Error::::SpaceLimitExceeded ); - System::::set_block_number(363); + System::::set_block_number(360); assert_ok!(Pallet::::set_commitment( RuntimeOrigin::signed(who), From 187ed948b06523eb0aaa0fa693ec7bde5572ec63 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Fri, 21 Mar 2025 09:53:52 -0700 Subject: [PATCH 28/28] mark deprecated items --- pallets/commitments/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pallets/commitments/src/lib.rs b/pallets/commitments/src/lib.rs index a136a2a7e..e838f81a3 100644 --- a/pallets/commitments/src/lib.rs +++ b/pallets/commitments/src/lib.rs @@ -123,12 +123,12 @@ pub mod pallet { } #[pallet::type_value] - /// Default value for commitment rate limit. + /// *DEPRECATED* Default value for commitment rate limit. pub fn DefaultRateLimit() -> BlockNumberFor { T::DefaultRateLimit::get() } - /// The rate limit for commitments + /// *DEPRECATED* The rate limit for commitments #[pallet::storage] pub type RateLimit = StorageValue<_, BlockNumberFor, ValueQuery, DefaultRateLimit>;