diff --git a/pallets/admin-utils/src/benchmarking.rs b/pallets/admin-utils/src/benchmarking.rs index 917d9008ba..d05e64beb3 100644 --- a/pallets/admin-utils/src/benchmarking.rs +++ b/pallets/admin-utils/src/benchmarking.rs @@ -335,5 +335,16 @@ mod benchmarks { _(RawOrigin::Root, 1u16.into()/*netuid*/, true/*enabled*/)/*set_commit_reveal_weights_enabled*/; } + #[benchmark] + fn sudo_set_commit_reveal_version() { + pallet_subtensor::Pallet::::init_new_network( + 1u16.into(), /*netuid*/ + 1u16, /*sudo_tempo*/ + ); + + #[extrinsic_call] + _(RawOrigin::Root, 5u16/*version*/)/*sudo_set_commit_reveal_version()*/; + } + //impl_benchmark_test_suite!(AdminUtils, crate::mock::new_test_ext(), crate::mock::Test); } diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index ba5e155b7c..3784b7793d 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -405,7 +405,7 @@ pub mod pallet { /// It is only callable by the root account or subnet owner. /// The extrinsic will call the Subtensor pallet to set the adjustment beta. #[pallet::call_index(12)] - #[pallet::weight(Weight::from_parts(19_240_000, 0) + #[pallet::weight(Weight::from_parts(14_850_000, 0) .saturating_add(::DbWeight::get().reads(1_u64)) .saturating_add(::DbWeight::get().writes(1_u64)))] pub fn sudo_set_max_weight_limit( @@ -480,7 +480,7 @@ pub mod pallet { /// It is only callable by the root account. /// The extrinsic will call the Subtensor pallet to set the maximum allowed UIDs for a subnet. #[pallet::call_index(15)] - #[pallet::weight(Weight::from_parts(23_820_000, 0) + #[pallet::weight(Weight::from_parts(18_770_000, 0) .saturating_add(::DbWeight::get().reads(2_u64)) .saturating_add(::DbWeight::get().writes(1_u64)))] pub fn sudo_set_max_allowed_uids( @@ -578,7 +578,7 @@ pub mod pallet { /// The extrinsic will call the Subtensor pallet to set the network registration allowed. #[pallet::call_index(19)] #[pallet::weight(( - Weight::from_parts(8_696_000, 0) + Weight::from_parts(6_722_000, 0) .saturating_add(::DbWeight::get().reads(0)) .saturating_add(::DbWeight::get().writes(1)), DispatchClass::Operational, @@ -1102,7 +1102,7 @@ pub mod pallet { /// It is only callable by the root account or subnet owner. /// The extrinsic will call the Subtensor pallet to set the value. #[pallet::call_index(49)] - #[pallet::weight(Weight::from_parts(19_480_000, 0) + #[pallet::weight(Weight::from_parts(14_780_000, 0) .saturating_add(::DbWeight::get().reads(1_u64)) .saturating_add(::DbWeight::get().writes(1_u64)))] pub fn sudo_set_commit_reveal_weights_enabled( @@ -1655,6 +1655,18 @@ pub mod pallet { ); Ok(()) } + + /// Sets the commit-reveal weights version for all subnets + #[pallet::call_index(71)] + #[pallet::weight((0, DispatchClass::Operational, Pays::No))] + pub fn sudo_set_commit_reveal_version( + origin: OriginFor, + version: u16, + ) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::Pallet::::set_commit_reveal_weights_version(version); + Ok(()) + } } } diff --git a/pallets/admin-utils/src/tests/mod.rs b/pallets/admin-utils/src/tests/mod.rs index f6495524ee..a55d132bfe 100644 --- a/pallets/admin-utils/src/tests/mod.rs +++ b/pallets/admin-utils/src/tests/mod.rs @@ -1930,3 +1930,24 @@ fn test_sudo_set_yuma3_enabled() { assert_eq!(SubtensorModule::get_yuma3_enabled(netuid), !to_be_set); }); } + +#[test] +fn test_sudo_set_commit_reveal_version() { + new_test_ext().execute_with(|| { + add_network(NetUid::from(1), 10); + + let to_be_set: u16 = 5; + let init_value: u16 = SubtensorModule::get_commit_reveal_weights_version(); + + assert_ok!(AdminUtils::sudo_set_commit_reveal_version( + <::RuntimeOrigin>::root(), + to_be_set + )); + + assert!(init_value != to_be_set); + assert_eq!( + SubtensorModule::get_commit_reveal_weights_version(), + to_be_set + ); + }); +} diff --git a/pallets/drand/src/lib.rs b/pallets/drand/src/lib.rs index a9a2a9c3f6..0dfb708e44 100644 --- a/pallets/drand/src/lib.rs +++ b/pallets/drand/src/lib.rs @@ -58,6 +58,7 @@ use sp_runtime::{ }; pub mod bls12_381; +pub mod migrations; pub mod types; pub mod utils; pub mod verifier; @@ -91,6 +92,8 @@ pub const QUICKNET_CHAIN_HASH: &str = const CHAIN_HASH: &str = QUICKNET_CHAIN_HASH; pub const MAX_PULSES_TO_FETCH: u64 = 50; +pub const MAX_KEPT_PULSES: u64 = 216_000; // 1 week +pub const MAX_REMOVED_PULSES: u64 = 100; /// Defines application identifier for crypto keys of this module. /// @@ -212,12 +215,24 @@ pub mod pallet { } } + /// Define a maximum length for the migration key + type MigrationKeyMaxLen = ConstU32<128>; + + /// Storage for migration run status + #[pallet::storage] + pub type HasMigrationRun = + StorageMap<_, Identity, BoundedVec, bool, ValueQuery>; + /// map round number to pulse #[pallet::storage] pub type Pulses = StorageMap<_, Blake2_128Concat, RoundNumber, Pulse, OptionQuery>; #[pallet::storage] - pub(super) type LastStoredRound = StorageValue<_, RoundNumber, ValueQuery>; + pub type LastStoredRound = StorageValue<_, RoundNumber, ValueQuery>; + + /// oldest stored round + #[pallet::storage] + pub type OldestStoredRound = StorageValue<_, RoundNumber, ValueQuery>; /// Defines the block when next unsigned transaction will be accepted. /// @@ -261,6 +276,14 @@ pub mod pallet { log::debug!("Drand: Failed to fetch pulse from drand. {e:?}"); } } + fn on_runtime_upgrade() -> frame_support::weights::Weight { + /* let weight = */ + frame_support::weights::Weight::from_parts(0, 0) /*;*/ + + //weight = weight.saturating_add(migrations::migrate_prune_old_pulses::()); + + //weight + } } #[pallet::validate_unsigned] @@ -308,8 +331,8 @@ pub mod pallet { /// Verify and write a pulse from the beacon into the runtime #[pallet::call_index(0)] #[pallet::weight(Weight::from_parts(5_708_000_000, 0) - .saturating_add(T::DbWeight::get().reads(2_u64)) - .saturating_add(T::DbWeight::get().writes(3_u64)))] + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)))] pub fn write_pulse( origin: OriginFor, pulses_payload: PulsesPayload>, @@ -321,6 +344,10 @@ pub mod pallet { let mut last_stored_round = LastStoredRound::::get(); let mut new_rounds = Vec::new(); + let oldest_stored_round = OldestStoredRound::::get(); + let is_first_storage = last_stored_round == 0 && oldest_stored_round == 0; + let mut first_new_round: Option = None; + for pulse in &pulses_payload.pulses { let is_verified = T::Verifier::verify(config.clone(), pulse.clone()) .map_err(|_| Error::::PulseVerificationError)?; @@ -339,12 +366,25 @@ pub mod pallet { // Collect the new round new_rounds.push(pulse.round); + + // Set the first new round if this is the initial storage + if is_first_storage && first_new_round.is_none() { + first_new_round = Some(pulse.round); + } } } // Update LastStoredRound storage LastStoredRound::::put(last_stored_round); + // Set OldestStoredRound if this was the first storage + if let Some(first_round) = first_new_round { + OldestStoredRound::::put(first_round); + } + + // Prune old pulses + Self::prune_old_pulses(last_stored_round); + // Update the next unsigned block number let current_block = frame_system::Pallet::::block_number(); >::put(current_block); @@ -628,6 +668,24 @@ impl Pallet { .propagate(true) .build() } + + fn prune_old_pulses(last_stored_round: RoundNumber) { + let mut oldest = OldestStoredRound::::get(); + if oldest == 0 { + return; + } + + let mut removed: u64 = 0; + while last_stored_round.saturating_sub(oldest) + 1 > MAX_KEPT_PULSES + && removed < MAX_REMOVED_PULSES + { + Pulses::::remove(oldest); + oldest = oldest.saturating_add(1); + removed = removed.saturating_add(1); + } + + OldestStoredRound::::put(oldest); + } } /// construct a message (e.g. signed by drand) diff --git a/pallets/drand/src/migrations/migrate_prune_old_pulses.rs b/pallets/drand/src/migrations/migrate_prune_old_pulses.rs new file mode 100644 index 0000000000..0bdfaeb159 --- /dev/null +++ b/pallets/drand/src/migrations/migrate_prune_old_pulses.rs @@ -0,0 +1,63 @@ +use crate::*; +use frame_support::{traits::Get, weights::Weight}; +use log; + +pub fn migrate_prune_old_pulses() -> Weight { + let migration_name = BoundedVec::truncate_from(b"migrate_prune_old_pulses".to_vec()); + + // Initialize the weight with one read operation. + let mut weight = T::DbWeight::get().reads(1); + + // Check if the migration has already run + if HasMigrationRun::::get(&migration_name) { + log::info!( + "Migration '{:?}' has already run. Skipping.", + String::from_utf8_lossy(&migration_name) + ); + return weight; + } + log::info!( + "Running migration '{}'", + String::from_utf8_lossy(&migration_name) + ); + + // Collect all round numbers + let mut rounds: Vec = Pulses::::iter_keys().collect(); + weight = weight.saturating_add(T::DbWeight::get().reads(rounds.len() as u64)); + + if rounds.is_empty() { + OldestStoredRound::::put(0u64); + LastStoredRound::::put(0u64); + weight = weight.saturating_add(T::DbWeight::get().writes(2)); + } else { + rounds.sort(); + let num_pulses = rounds.len() as u64; + + let mut new_oldest = rounds[0]; + if num_pulses > MAX_KEPT_PULSES { + let num_to_delete = num_pulses - MAX_KEPT_PULSES; + new_oldest = rounds[num_to_delete as usize]; + + for &round in &rounds[0..num_to_delete as usize] { + Pulses::::remove(round); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + } + } + + OldestStoredRound::::put(new_oldest); + LastStoredRound::::put(*rounds.last().unwrap()); + weight = weight.saturating_add(T::DbWeight::get().writes(2)); + } + + // Mark the migration as completed + HasMigrationRun::::insert(&migration_name, true); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + log::info!( + "Migration '{:?}' completed.", + String::from_utf8_lossy(&migration_name) + ); + + // Return the migration weight. + weight +} diff --git a/pallets/drand/src/migrations/mod.rs b/pallets/drand/src/migrations/mod.rs new file mode 100644 index 0000000000..6853d46b7a --- /dev/null +++ b/pallets/drand/src/migrations/mod.rs @@ -0,0 +1,2 @@ +pub mod migrate_prune_old_pulses; +pub use migrate_prune_old_pulses::*; diff --git a/pallets/drand/src/tests.rs b/pallets/drand/src/tests.rs index 5cfdcb5485..405c9b8339 100644 --- a/pallets/drand/src/tests.rs +++ b/pallets/drand/src/tests.rs @@ -16,14 +16,17 @@ use crate::{ BeaconConfig, BeaconConfigurationPayload, BeaconInfoResponse, Call, DrandResponseBody, - ENDPOINTS, Error, Pulse, Pulses, PulsesPayload, QUICKNET_CHAIN_HASH, mock::*, + ENDPOINTS, Error, HasMigrationRun, LastStoredRound, MAX_KEPT_PULSES, OldestStoredRound, Pulse, + Pulses, PulsesPayload, QUICKNET_CHAIN_HASH, migrations::migrate_prune_old_pulses, mock::*, }; use codec::Encode; use frame_support::{ - assert_noop, assert_ok, + BoundedVec, assert_noop, assert_ok, pallet_prelude::{InvalidTransaction, TransactionSource}, + weights::RuntimeDbWeight, }; use frame_system::RawOrigin; +use sp_core::Get; use sp_runtime::{ offchain::{ OffchainWorkerExt, @@ -549,3 +552,156 @@ fn test_invalid_json_then_success() { assert_eq!(actual, expected_pulse); }); } + +#[test] +fn test_pulses_are_correctly_pruned() { + new_test_ext().execute_with(|| { + let pulse = Pulse::default(); + let last_round: u64 = MAX_KEPT_PULSES + 2; + let oldest_round: u64 = 1; + let prune_count: u64 = 2; + let new_oldest: u64 = oldest_round + prune_count; + let middle_round: u64 = MAX_KEPT_PULSES / 2; + + // Set storage bounds + OldestStoredRound::::put(oldest_round); + LastStoredRound::::put(last_round); + + // Insert pulses at boundaries + // These should be pruned + Pulses::::insert(1, pulse.clone()); + Pulses::::insert(2, pulse.clone()); + + // This should remain (new oldest) + Pulses::::insert(new_oldest, pulse.clone()); + + // Middle and last should remain + Pulses::::insert(middle_round, pulse.clone()); + Pulses::::insert(last_round, pulse.clone()); + + // Trigger prune + Drand::prune_old_pulses(last_round); + + // Assert new oldest + assert_eq!(OldestStoredRound::::get(), new_oldest); + + // Assert pruned correctly + assert!(!Pulses::::contains_key(1), "Round 1 should be pruned"); + assert!(!Pulses::::contains_key(2), "Round 2 should be pruned"); + + // Assert not pruned incorrectly + assert!( + Pulses::::contains_key(new_oldest), + "New oldest round should remain" + ); + assert!( + Pulses::::contains_key(middle_round), + "Middle round should remain" + ); + assert!( + Pulses::::contains_key(last_round), + "Last round should remain" + ); + }); +} + +#[test] +fn test_migrate_prune_old_pulses() { + new_test_ext().execute_with(|| { + let migration_name = BoundedVec::truncate_from(b"migrate_prune_old_pulses".to_vec()); + let pulse = Pulse::default(); + + assert_eq!(Pulses::::iter().count(), 0); + assert!(!HasMigrationRun::::get(&migration_name)); + assert_eq!(OldestStoredRound::::get(), 0); + assert_eq!(LastStoredRound::::get(), 0); + + // Test with more pulses than MAX_KEPT_PULSES + let excess: u64 = 9; + let total: u64 = MAX_KEPT_PULSES + excess; + for i in 1..=total { + Pulses::::insert(i, pulse.clone()); + } + + let weight_large = migrate_prune_old_pulses::(); + + let expected_oldest = excess + 1; + assert_eq!(OldestStoredRound::::get(), expected_oldest); + assert_eq!(LastStoredRound::::get(), total); + + for i in 1..=excess { + assert!(!Pulses::::contains_key(i)); + } + for i in expected_oldest..=total { + assert!(Pulses::::contains_key(i)); + } + + let db_weight: RuntimeDbWeight = ::DbWeight::get(); + let num_pulses = total; + let num_to_delete = num_pulses - MAX_KEPT_PULSES; + let expected_weight = db_weight.reads(1 + num_pulses) + db_weight.writes(num_to_delete + 3); + assert_eq!(weight_large, expected_weight); + }); +} + +#[test] +fn test_prune_maximum_of_100_pulses_per_call() { + new_test_ext().execute_with(|| { + // ------------------------------------------------------------ + // 1. Arrange – create a storage layout that exceeds MAX_KEPT_PULSES + // ------------------------------------------------------------ + const EXTRA: u64 = 250; + let oldest_round: u64 = 1; + let last_round: u64 = oldest_round + MAX_KEPT_PULSES + EXTRA; + + OldestStoredRound::::put(oldest_round); + LastStoredRound::::put(last_round); + let pulse = Pulse::default(); + + // Insert the first 150 rounds so we can check they disappear / stay + for r in oldest_round..=oldest_round + 150 { + Pulses::::insert(r, pulse.clone()); + } + let mid_round = oldest_round + 150; + Pulses::::insert(last_round, pulse.clone()); + + // ------------------------------------------------------------ + // 2. Act – run the pruning function once + // ------------------------------------------------------------ + Drand::prune_old_pulses(last_round); + + // ------------------------------------------------------------ + // 3. Assert – only the *first* 100 pulses were removed + // ------------------------------------------------------------ + let expected_new_oldest = oldest_round + 100; // 101 + + // ‣ Storage bound updated correctly + assert_eq!( + OldestStoredRound::::get(), + expected_new_oldest, + "OldestStoredRound should advance by exactly 100" + ); + + // ‣ Rounds 1‑100 are gone + for r in oldest_round..expected_new_oldest { + assert!( + !Pulses::::contains_key(r), + "Round {r} should have been pruned" + ); + } + + // ‣ Round 101 (new oldest) and later rounds remain + assert!( + Pulses::::contains_key(expected_new_oldest), + "Round {expected_new_oldest} should remain after pruning" + ); + assert!( + Pulses::::contains_key(mid_round), + "Mid-range round should remain after pruning" + ); + assert!( + Pulses::::contains_key(last_round), + "LastStoredRound should remain after pruning" + ); + }); +} diff --git a/pallets/subtensor/src/benchmarks.rs b/pallets/subtensor/src/benchmarks.rs index b239adb70e..a86a46ad32 100644 --- a/pallets/subtensor/src/benchmarks.rs +++ b/pallets/subtensor/src/benchmarks.rs @@ -1598,4 +1598,37 @@ mod pallet_benchmarks { assert_eq!(TokenSymbol::::get(netuid), new_symbol); } + + #[benchmark] + fn commit_timelocked_weights() { + let hotkey: T::AccountId = whitelisted_caller(); + let netuid = NetUid::from(1); + let vec_commit: Vec = vec![0; MAX_CRV3_COMMIT_SIZE_BYTES as usize]; + let commit: BoundedVec<_, _> = vec_commit.try_into().unwrap(); + let round: u64 = 0; + + Subtensor::::init_new_network(netuid, 1); + Subtensor::::set_network_pow_registration_allowed(netuid, true); + SubtokenEnabled::::insert(netuid, true); + + let reg_fee = Subtensor::::get_burn_as_u64(netuid); + Subtensor::::add_balance_to_coldkey_account(&hotkey, reg_fee.saturating_mul(2)); + + assert_ok!(Subtensor::::burned_register( + RawOrigin::Signed(hotkey.clone()).into(), + netuid, + hotkey.clone() + )); + + Subtensor::::set_commit_reveal_weights_enabled(netuid, true); + + #[extrinsic_call] + _( + RawOrigin::Signed(hotkey.clone()), + netuid, + commit.clone(), + round, + Subtensor::::get_commit_reveal_weights_version(), + ); + } } diff --git a/pallets/subtensor/src/coinbase/reveal_commits.rs b/pallets/subtensor/src/coinbase/reveal_commits.rs index 8e8929b993..e4b2ed3f43 100644 --- a/pallets/subtensor/src/coinbase/reveal_commits.rs +++ b/pallets/subtensor/src/coinbase/reveal_commits.rs @@ -1,13 +1,14 @@ use super::*; use ark_serialize::CanonicalDeserialize; use codec::Decode; -use frame_support::dispatch; -use frame_support::traits::OriginTrait; +use frame_support::{dispatch, traits::OriginTrait}; +use scale_info::prelude::collections::VecDeque; use subtensor_runtime_common::NetUid; -use tle::curves::drand::TinyBLS381; -use tle::stream_ciphers::AESGCMStreamCipherProvider; -use tle::tlock::TLECiphertext; -use tle::tlock::tld; +use tle::{ + curves::drand::TinyBLS381, + stream_ciphers::AESGCMStreamCipherProvider, + tlock::{TLECiphertext, tld}, +}; use w3f_bls::EngineBLS; /// Contains all necessary information to set weights. @@ -16,8 +17,18 @@ use w3f_bls::EngineBLS; /// encrypted, compressed, serialized, and submitted to the `commit_crv3_weights` /// extrinsic. #[derive(Encode, Decode)] -#[freeze_struct("46e75a8326ba3665")] +#[freeze_struct("b6833b5029be4127")] pub struct WeightsTlockPayload { + pub hotkey: Vec, + pub uids: Vec, + pub values: Vec, + pub version_key: u64, +} + +/// For the old structure +#[derive(Encode, Decode)] +#[freeze_struct("304e55f41267caa")] +pub struct LegacyWeightsTlockPayload { pub uids: Vec, pub values: Vec, pub version_key: u64, @@ -26,12 +37,12 @@ pub struct WeightsTlockPayload { impl Pallet { /// The `reveal_crv3_commits` function is run at the very beginning of epoch `n`, pub fn reveal_crv3_commits(netuid: NetUid) -> dispatch::DispatchResult { + let reveal_period = Self::get_reveal_period(netuid); let cur_block = Self::get_current_block_as_u64(); let cur_epoch = Self::get_epoch_index(netuid, cur_block); // Weights revealed must have been committed during epoch `cur_epoch - reveal_period`. - let reveal_epoch = - cur_epoch.saturating_sub(Self::get_reveal_period(netuid).saturating_sub(1)); + let reveal_epoch = cur_epoch.saturating_sub(reveal_period); // Clean expired commits for (epoch, _) in CRV3WeightCommitsV2::::iter_prefix(netuid) { @@ -40,36 +51,43 @@ impl Pallet { } } - // No commits to reveal until at least epoch 2. - if cur_epoch < 2 { + // No commits to reveal until at least epoch reveal_period. + if cur_epoch < reveal_period { log::warn!("Failed to reveal commit for subnet {netuid} Too early"); return Ok(()); } let mut entries = CRV3WeightCommitsV2::::take(netuid, reveal_epoch); + let mut unrevealed = VecDeque::new(); - // Keep popping item off the end of the queue until we sucessfully reveal a commit. - while let Some((who, _commit_block, serialized_compresssed_commit, round_number)) = + // Keep popping items off the front of the queue until we successfully reveal a commit. + while let Some((who, commit_block, serialized_compresssed_commit, round_number)) = entries.pop_front() { - let reader = &mut &serialized_compresssed_commit[..]; - let commit = match TLECiphertext::::deserialize_compressed(reader) { - Ok(c) => c, - Err(e) => { + // Try to get the round number from pallet_drand. + let pulse = match pallet_drand::Pulses::::get(round_number) { + Some(p) => p, + None => { + // Round number used was not found on the chain. Skip this commit. log::warn!( - "Failed to reveal commit for subnet {netuid} submitted by {who:?} due to error deserializing the commit: {e:?}" + "Failed to reveal commit for subnet {netuid} submitted by {who:?} on block {commit_block} due to missing round number {round_number}; will retry every block in reveal epoch." ); + unrevealed.push_back(( + who, + commit_block, + serialized_compresssed_commit, + round_number, + )); continue; } }; - // Try to get the round number from pallet_drand. - let pulse = match pallet_drand::Pulses::::get(round_number) { - Some(p) => p, - None => { - // Round number used was not found on the chain. Skip this commit. + let reader = &mut &serialized_compresssed_commit[..]; + let commit = match TLECiphertext::::deserialize_compressed(reader) { + Ok(c) => c, + Err(e) => { log::warn!( - "Failed to reveal commit for subnet {netuid} submitted by {who:?} due to missing round number {round_number} at time of reveal." + "Failed to reveal commit for subnet {netuid} submitted by {who:?} due to error deserializing the commit: {e:?}" ); continue; } @@ -86,7 +104,7 @@ impl Pallet { ) { Ok(s) => s, Err(e) => { - log::error!( + log::warn!( "Failed to reveal commit for subnet {netuid} submitted by {who:?} due to error deserializing signature from drand pallet: {e:?}" ); continue; @@ -105,32 +123,67 @@ impl Pallet { } }; - // Decrypt the bytes into WeightsPayload - let mut reader = &decrypted_bytes[..]; - let payload: WeightsTlockPayload = match Decode::decode(&mut reader) { - Ok(w) => w, - Err(e) => { - log::warn!( - "Failed to reveal commit for subnet {netuid} submitted by {who:?} due to error deserializing WeightsPayload: {e:?}" - ); - continue; + // ------------------------------------------------------------------ + // Try to decode payload with the new and legacy formats. + // ------------------------------------------------------------------ + let (uids, values, version_key) = { + let mut reader_new = &decrypted_bytes[..]; + if let Ok(payload) = WeightsTlockPayload::decode(&mut reader_new) { + // Verify hotkey matches committer + let mut hk_reader = &payload.hotkey[..]; + match T::AccountId::decode(&mut hk_reader) { + Ok(decoded_hotkey) if decoded_hotkey == who => { + (payload.uids, payload.values, payload.version_key) + } + Ok(_) => { + log::warn!( + "Failed to reveal commit for subnet {netuid} submitted by {who:?} due to hotkey mismatch in payload" + ); + continue; + } + Err(e) => { + log::warn!( + "Failed to reveal commit for subnet {netuid} submitted by {who:?} due to error deserializing hotkey: {e:?}" + ); + continue; + } + } + } else { + // Fallback to legacy payload + let mut reader_legacy = &decrypted_bytes[..]; + match LegacyWeightsTlockPayload::decode(&mut reader_legacy) { + Ok(legacy) => (legacy.uids, legacy.values, legacy.version_key), + Err(e) => { + log::warn!( + "Failed to reveal commit for subnet {netuid} submitted by {who:?} due to error deserializing both payload formats: {e:?}" + ); + continue; + } + } } }; + // ------------------------------------------------------------------ + // Apply weights + // ------------------------------------------------------------------ if let Err(e) = Self::do_set_weights( T::RuntimeOrigin::signed(who.clone()), netuid, - payload.uids, - payload.values, - payload.version_key, + uids, + values, + version_key, ) { log::warn!( "Failed to `do_set_weights` for subnet {netuid} submitted by {who:?}: {e:?}" ); continue; - } else { - Self::deposit_event(Event::CRV3WeightsRevealed(netuid, who)); - }; + } + + Self::deposit_event(Event::CRV3WeightsRevealed(netuid, who)); + } + + if !unrevealed.is_empty() { + CRV3WeightCommitsV2::::insert(netuid, reveal_epoch, unrevealed); } Ok(()) diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index d19cb88e9f..588d32bb6a 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -248,12 +248,12 @@ impl Pallet { // --- 7. Drain pending emission through the subnet based on tempo. // Run the epoch for *all* subnets, even if we don't emit anything. for &netuid in subnets.iter() { + // Reveal matured weights. + if let Err(e) = Self::reveal_crv3_commits(netuid) { + log::warn!("Failed to reveal commits for subnet {netuid} due to error: {e:?}"); + }; // Pass on subnets that have not reached their tempo. if Self::should_run_epoch(netuid, current_block) { - if let Err(e) = Self::reveal_crv3_commits(netuid) { - log::warn!("Failed to reveal commits for subnet {netuid} due to error: {e:?}"); - }; - // Restart counters. BlocksSinceLastStep::::insert(netuid, 0); LastMechansimStepBlock::::insert(netuid, current_block); diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index f8474bff0a..09a1d92f93 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -766,6 +766,11 @@ pub mod pallet { false } #[pallet::type_value] + /// Default value for weight commit/reveal version. + pub fn DefaultCommitRevealWeightsVersion() -> u16 { + 4 + } + #[pallet::type_value] /// Senate requirements pub fn DefaultSenateRequiredStakePercentage() -> u64 { T::InitialSenateRequiredStakePercentage::get() @@ -1746,6 +1751,11 @@ pub mod pallet { pub type AccumulatedLeaseDividends = StorageMap<_, Twox64Concat, LeaseId, AlphaCurrency, ValueQuery, DefaultZeroAlpha>; + #[pallet::storage] + /// --- ITEM ( CommitRevealWeightsVersion ) + pub type CommitRevealWeightsVersion = + StorageValue<_, u16, ValueQuery, DefaultCommitRevealWeightsVersion>; + /// ================== /// ==== Genesis ===== /// ================== @@ -1890,6 +1900,7 @@ pub enum CustomTransactionError { InvalidPort, BadRequest, ZeroMaxAmount, + InvalidRevealRound, } impl From for u8 { @@ -1911,6 +1922,7 @@ impl From for u8 { CustomTransactionError::InvalidPort => 13, CustomTransactionError::BadRequest => 255, CustomTransactionError::ZeroMaxAmount => 14, + CustomTransactionError::InvalidRevealRound => 15, } } } @@ -2101,8 +2113,35 @@ where Err(CustomTransactionError::StakeAmountTooLow.into()) } } - Some(Call::commit_crv3_weights { netuid, .. }) => { + Some(Call::commit_crv3_weights { + netuid, + reveal_round, + .. + }) => { + if Self::check_weights_min_stake(who, *netuid) { + if *reveal_round < pallet_drand::LastStoredRound::::get() { + return Err(CustomTransactionError::InvalidRevealRound.into()); + } + let priority: u64 = Pallet::::get_priority_set_weights(who, *netuid); + let validity = ValidTransaction { + priority, + longevity: 1, + ..Default::default() + }; + Ok((validity, Some(who.clone()), origin)) + } else { + Err(CustomTransactionError::StakeAmountTooLow.into()) + } + } + Some(Call::commit_timelocked_weights { + netuid, + reveal_round, + .. + }) => { if Self::check_weights_min_stake(who, *netuid) { + if *reveal_round < pallet_drand::LastStoredRound::::get() { + return Err(CustomTransactionError::InvalidRevealRound.into()); + } let priority: u64 = Pallet::::get_priority_set_weights(who, *netuid); let validity = ValidTransaction { priority, diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index 6f90f02340..474d6d3eff 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -280,7 +280,7 @@ mod dispatches { /// #[pallet::call_index(99)] #[pallet::weight((Weight::from_parts(73_750_000, 0) - .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().reads(7_u64)) .saturating_add(T::DbWeight::get().writes(2)), DispatchClass::Normal, Pays::No))] pub fn commit_crv3_weights( origin: T::RuntimeOrigin, @@ -288,7 +288,7 @@ mod dispatches { commit: BoundedVec>, reveal_round: u64, ) -> DispatchResult { - Self::do_commit_crv3_weights(origin, netuid, commit, reveal_round) + Self::do_commit_timelocked_weights(origin, netuid, commit, reveal_round, 4) } /// ---- The implementation for batch revealing committed weights. @@ -498,7 +498,7 @@ mod dispatches { /// - The delegate is setting a take which is not lower than the previous. /// #[pallet::call_index(65)] - #[pallet::weight((Weight::from_parts(37_380_000, 0) + #[pallet::weight((Weight::from_parts(29_320_000, 0) .saturating_add(T::DbWeight::get().reads(3)) .saturating_add(T::DbWeight::get().writes(2)), DispatchClass::Normal, Pays::No))] pub fn decrease_take( @@ -2175,5 +2175,49 @@ mod dispatches { Self::deposit_event(Event::SymbolUpdated { netuid, symbol }); Ok(()) } + + /// ---- Used to commit timelock encrypted commit-reveal weight values to later be revealed. + /// + /// # Args: + /// * `origin`: (`::RuntimeOrigin`): + /// - The committing hotkey. + /// + /// * `netuid` (`u16`): + /// - The u16 network identifier. + /// + /// * `commit` (`Vec`): + /// - The encrypted compressed commit. + /// The steps for this are: + /// 1. Instantiate [`WeightsTlockPayload`] + /// 2. Serialize it using the `parity_scale_codec::Encode` trait + /// 3. Encrypt it following the steps (here)[https://github.com/ideal-lab5/tle/blob/f8e6019f0fb02c380ebfa6b30efb61786dede07b/timelock/src/tlock.rs#L283-L336] + /// to produce a [`TLECiphertext`] type. + /// 4. Serialize and compress using the `ark-serialize` `CanonicalSerialize` trait. + /// + /// * reveal_round (`u64`): + /// - The drand reveal round which will be avaliable during epoch `n+1` from the current + /// epoch. + /// + /// * commit_reveal_version (`u16`): + /// - The client (bittensor-drand) version + #[pallet::call_index(113)] + #[pallet::weight((Weight::from_parts(73_750_000, 0) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(2)), DispatchClass::Normal, Pays::No))] + pub fn commit_timelocked_weights( + origin: T::RuntimeOrigin, + netuid: NetUid, + commit: BoundedVec>, + reveal_round: u64, + commit_reveal_version: u16, + ) -> DispatchResult { + Self::do_commit_timelocked_weights( + origin, + netuid, + commit, + reveal_round, + commit_reveal_version, + ) + } } } diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index 1151be75c4..71750e7534 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -242,5 +242,7 @@ mod errors { SymbolDoesNotExist, /// Symbol already in use. SymbolAlreadyInUse, + /// Incorrect commit-reveal version. + IncorrectCommitRevealVersion, } } diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index c0528d0600..e1255f938e 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -380,5 +380,10 @@ mod events { /// The symbol that has been updated. symbol: Vec, }, + + /// Commit Reveal Weights version has been updated. + /// + /// - **version**: The required version. + CommitRevealVersionSet(u16), } } diff --git a/pallets/subtensor/src/migrations/migrate_disable_commit_reveal.rs b/pallets/subtensor/src/migrations/migrate_disable_commit_reveal.rs new file mode 100644 index 0000000000..5465adbdd1 --- /dev/null +++ b/pallets/subtensor/src/migrations/migrate_disable_commit_reveal.rs @@ -0,0 +1,37 @@ +use super::*; +use crate::HasMigrationRun; +use frame_support::{traits::Get, weights::Weight}; +use scale_info::prelude::string::String; + +pub fn migrate_disable_commit_reveal() -> Weight { + const MIG_NAME: &[u8] = b"disable_commit_reveal_v1"; + + // 1 ─ check if we already ran + if HasMigrationRun::::get(MIG_NAME) { + log::info!( + "Migration '{}' already executed - skipping", + String::from_utf8_lossy(MIG_NAME) + ); + return T::DbWeight::get().reads(1); + } + + log::info!("Running migration '{}'", String::from_utf8_lossy(MIG_NAME)); + + let mut total_weight = T::DbWeight::get().reads(1); + + // 2 ─ iterate over every stored key and set value -> false + for (netuid, _) in CommitRevealWeightsEnabled::::drain() { + CommitRevealWeightsEnabled::::insert(netuid, false); + total_weight = total_weight.saturating_add(T::DbWeight::get().reads_writes(1, 1)); + } + + // 3 ─ mark migration as done + HasMigrationRun::::insert(MIG_NAME, true); + total_weight = total_weight.saturating_add(T::DbWeight::get().writes(1)); + + log::info!( + "Migration '{}' completed: commit-reveal disabled on all subnets", + String::from_utf8_lossy(MIG_NAME) + ); + total_weight +} diff --git a/pallets/subtensor/src/migrations/mod.rs b/pallets/subtensor/src/migrations/mod.rs index 0c56f222c0..cdf142357b 100644 --- a/pallets/subtensor/src/migrations/mod.rs +++ b/pallets/subtensor/src/migrations/mod.rs @@ -11,6 +11,7 @@ pub mod migrate_create_root_network; pub mod migrate_crv3_commits_add_block; pub mod migrate_delete_subnet_21; pub mod migrate_delete_subnet_3; +pub mod migrate_disable_commit_reveal; pub mod migrate_fix_is_network_member; pub mod migrate_fix_root_subnet_tao; pub mod migrate_identities_v2; diff --git a/pallets/subtensor/src/subnets/weights.rs b/pallets/subtensor/src/subnets/weights.rs index c009d70304..2e7162ab36 100644 --- a/pallets/subtensor/src/subnets/weights.rs +++ b/pallets/subtensor/src/subnets/weights.rs @@ -182,49 +182,56 @@ impl Pallet { Ok(()) } - /// ---- The implementation for committing commit-reveal v3 weights. - /// - /// # Args: - /// * `origin`: (`::RuntimeOrigin`): - /// - The signature of the committing hotkey. - /// - /// * `netuid` (`u16`): - /// - The u16 network identifier. - /// - /// * `commit` (`Vec`): - /// - The encrypted compressed commit. - /// The steps for this are: - /// 1. Instantiate [`WeightsPayload`] - /// 2. Serialize it using the `parity_scale_codec::Encode` trait - /// 3. Encrypt it following the steps (here)[https://github.com/ideal-lab5/tle/blob/f8e6019f0fb02c380ebfa6b30efb61786dede07b/timelock/src/tlock.rs#L283-L336] - /// to produce a [`TLECiphertext`] type. - /// 4. Serialize and compress using the `ark-serialize` `CanonicalSerialize` trait. - /// - /// * reveal_round (`u64`): - /// - The drand reveal round which will be avaliable during epoch `n+1` from the current - /// epoch. - /// - /// # Raises: - /// * `CommitRevealDisabled`: - /// - Raised if commit-reveal v3 is disabled for the specified network. - /// - /// * `HotKeyNotRegisteredInSubNet`: - /// - Raised if the hotkey is not registered on the specified network. - /// - /// * `CommittingWeightsTooFast`: - /// - Raised if the hotkey's commit rate exceeds the permitted limit. - /// - /// * `TooManyUnrevealedCommits`: - /// - Raised if the hotkey has reached the maximum number of unrevealed commits. - /// - /// # Events: - /// * `WeightsCommitted`: - /// - Emitted upon successfully storing the weight hash. - pub fn do_commit_crv3_weights( + /// ---- Commits a timelocked, encrypted weight payload (Commit-Reveal v3). + /// + /// # Args + /// * `origin` (`::RuntimeOrigin`): + /// The signed origin of the committing hotkey. + /// * `netuid` (`NetUid` = `u16`): + /// Unique identifier for the subnet on which the commit is made. + /// * `commit` (`BoundedVec>`): + /// The encrypted weight payload, produced as follows: + /// 1. Build a [`WeightsPayload`] structure. + /// 2. SCALE-encode it (`parity_scale_codec::Encode`). + /// 3. Encrypt it following the steps + /// [here](https://github.com/ideal-lab5/tle/blob/f8e6019f0fb02c380ebfa6b30efb61786dede07b/timelock/src/tlock.rs#L283-L336) to + /// produce a [`TLECiphertext`]. + /// 4. Compress & serialise. + /// * `reveal_round` (`u64`): + /// DRAND round whose output becomes known during epoch `n + 1`; the payload + /// must be revealed in that epoch. + /// * `commit_reveal_version` (`u16`): + /// Version tag that **must** match [`get_commit_reveal_weights_version`] for + /// the call to succeed. Used to gate runtime upgrades. + /// + /// # Behaviour + /// 1. Verifies the caller’s signature and registration on `netuid`. + /// 2. Ensures commit-reveal is enabled **and** the supplied + /// `commit_reveal_version` is current. + /// 3. Enforces per-neuron rate-limiting via [`Pallet::check_rate_limit`]. + /// 4. Rejects the call when the hotkey already has ≥ 10 unrevealed commits in + /// the current epoch. + /// 5. Appends `(hotkey, commit_block, commit, reveal_round)` to + /// `CRV3WeightCommitsV2[netuid][epoch]`. + /// 6. Emits `CRV3WeightsCommitted` with the Blake2 hash of `commit`. + /// 7. Updates `LastUpdateForUid` so subsequent rate-limit checks include this + /// commit. + /// + /// # Raises + /// * `CommitRevealDisabled` – Commit-reveal is disabled on `netuid`. + /// * `IncorrectCommitRevealVersion` – Provided version ≠ runtime version. + /// * `HotKeyNotRegisteredInSubNet` – Caller’s hotkey is not registered. + /// * `CommittingWeightsTooFast` – Caller exceeds commit-rate limit. + /// * `TooManyUnrevealedCommits` – Caller already has 10 unrevealed commits. + /// + /// # Events + /// * `CRV3WeightsCommitted(hotkey, netuid, commit_hash)` – Fired after the commit is successfully stored. + pub fn do_commit_timelocked_weights( origin: T::RuntimeOrigin, netuid: NetUid, commit: BoundedVec>, reveal_round: u64, + commit_reveal_version: u16, ) -> DispatchResult { // 1. Verify the caller's signature (hotkey). let who = ensure_signed(origin)?; @@ -237,13 +244,19 @@ impl Pallet { Error::::CommitRevealDisabled ); - // 3. Ensure the hotkey is registered on the network. + // 3. Ensure correct client version + ensure!( + commit_reveal_version == Self::get_commit_reveal_weights_version(), + Error::::IncorrectCommitRevealVersion + ); + + // 4. Ensure the hotkey is registered on the network. ensure!( Self::is_hotkey_registered_on_network(netuid, &who), Error::::HotKeyNotRegisteredInSubNet ); - // 4. Check that the commit rate does not exceed the allowed frequency. + // 5. Check that the commit rate does not exceed the allowed frequency. let commit_block = Self::get_current_block_as_u64(); let neuron_uid = Self::get_uid_for_net_and_hotkey(netuid, &who)?; ensure!( @@ -251,7 +264,7 @@ impl Pallet { Error::::CommittingWeightsTooFast ); - // 5. Retrieve or initialize the VecDeque of commits for the hotkey. + // 6. Retrieve or initialize the VecDeque of commits for the hotkey. let cur_block = Self::get_current_block_as_u64(); let cur_epoch = match Self::should_run_epoch(netuid, commit_block) { true => Self::get_epoch_index(netuid, cur_block).saturating_add(1), @@ -259,7 +272,7 @@ impl Pallet { }; CRV3WeightCommitsV2::::try_mutate(netuid, cur_epoch, |commits| -> DispatchResult { - // 6. Verify that the number of unrevealed commits is within the allowed limit. + // 7. Verify that the number of unrevealed commits is within the allowed limit. let unrevealed_commits_for_who = commits .iter() @@ -270,22 +283,22 @@ impl Pallet { Error::::TooManyUnrevealedCommits ); - // 7. Append the new commit with calculated reveal blocks. + // 8. Append the new commit with calculated reveal blocks. // Hash the commit before it is moved, for the event let commit_hash = BlakeTwo256::hash(&commit); commits.push_back((who.clone(), cur_block, commit, reveal_round)); - // 8. Emit the WeightsCommitted event + // 9. Emit the WeightsCommitted event Self::deposit_event(Event::CRV3WeightsCommitted( who.clone(), netuid, commit_hash, )); - // 9. Update the last commit block for the hotkey's UID. + // 10. Update the last commit block for the hotkey's UID. Self::set_last_update_for_uid(netuid, neuron_uid, commit_block); - // 10. Return success. + // 11. Return success. Ok(()) }) } diff --git a/pallets/subtensor/src/tests/migration.rs b/pallets/subtensor/src/tests/migration.rs index 611012b2b7..5653b6b822 100644 --- a/pallets/subtensor/src/tests/migration.rs +++ b/pallets/subtensor/src/tests/migration.rs @@ -1068,3 +1068,64 @@ fn test_migrate_crv3_commits_add_block() { ); }); } + +#[test] +fn test_migrate_disable_commit_reveal() { + const MIG_NAME: &[u8] = b"disable_commit_reveal_v1"; + let netuids = [NetUid::from(1), NetUid::from(2), NetUid::from(42)]; + + // --------------------------------------------------------------------- + // 1. build initial state ─ all nets enabled + // --------------------------------------------------------------------- + new_test_ext(1).execute_with(|| { + for (i, netuid) in netuids.iter().enumerate() { + add_network(*netuid, 5u16 + i as u16, 0); + CommitRevealWeightsEnabled::::insert(*netuid, true); + } + assert!( + !HasMigrationRun::::get(MIG_NAME), + "migration flag should be unset before run" + ); + + // ----------------------------------------------------------------- + // 2. run migration + // ----------------------------------------------------------------- + let w = crate::migrations::migrate_disable_commit_reveal::migrate_disable_commit_reveal::< + Test, + >(); + + assert!( + HasMigrationRun::::get(MIG_NAME), + "migration flag not set" + ); + + // ----------------------------------------------------------------- + // 3. verify every netuid is now disabled and only one value exists + // ----------------------------------------------------------------- + for netuid in netuids { + assert!( + !CommitRevealWeightsEnabled::::get(netuid), + "commit-reveal should be disabled for netuid {netuid}" + ); + } + + // There should be no stray keys + let collected: Vec<_> = CommitRevealWeightsEnabled::::iter().collect(); + assert_eq!(collected.len(), netuids.len(), "unexpected key count"); + for (k, v) in collected { + assert!(!v, "found an enabled flag after migration for netuid {k}"); + } + + // ----------------------------------------------------------------- + // 4. running again should be a no-op + // ----------------------------------------------------------------- + let w2 = crate::migrations::migrate_disable_commit_reveal::migrate_disable_commit_reveal::< + Test, + >(); + assert_eq!( + w2, + ::DbWeight::get().reads(1), + "second run should read the flag and do nothing else" + ); + }); +} diff --git a/pallets/subtensor/src/tests/weights.rs b/pallets/subtensor/src/tests/weights.rs index 2ff01bc875..f9d6d0de08 100644 --- a/pallets/subtensor/src/tests/weights.rs +++ b/pallets/subtensor/src/tests/weights.rs @@ -1,6 +1,7 @@ #![allow(clippy::indexing_slicing, clippy::unwrap_used)] use ark_serialize::CanonicalDeserialize; +use ark_serialize::CanonicalSerialize; use frame_support::{ assert_err, assert_ok, dispatch::{DispatchClass, DispatchResult, GetDispatchInfo, Pays}, @@ -29,7 +30,7 @@ use w3f_bls::EngineBLS; use super::mock; use super::mock::*; -use crate::coinbase::reveal_commits::WeightsTlockPayload; +use crate::coinbase::reveal_commits::{LegacyWeightsTlockPayload, WeightsTlockPayload}; use crate::*; /*************************** @@ -4727,8 +4728,6 @@ pub fn tlock_encrypt_decrypt_drand_quicknet_works() { #[test] fn test_reveal_crv3_commits_success() { new_test_ext(100).execute_with(|| { - use ark_serialize::CanonicalSerialize; - let netuid = NetUid::from(1); let hotkey1: AccountId = U256::from(1); let hotkey2: AccountId = U256::from(2); @@ -4767,6 +4766,7 @@ fn test_reveal_crv3_commits_success() { let version_key = SubtensorModule::get_weights_version_key(netuid); let payload = WeightsTlockPayload { + hotkey: hotkey1.encode(), values: vec![10, 20], uids: vec![neuron_uid1, neuron_uid2], version_key, @@ -4811,11 +4811,12 @@ fn test_reveal_crv3_commits_success() { "Commit bytes now contain {commit_bytes:#?}" ); - assert_ok!(SubtensorModule::do_commit_crv3_weights( + assert_ok!(SubtensorModule::do_commit_timelocked_weights( RuntimeOrigin::signed(hotkey1), netuid, commit_bytes.clone().try_into().expect("Failed to convert commit bytes into bounded vector"), - reveal_round + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() )); let sig_bytes = hex::decode("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39") @@ -4880,8 +4881,6 @@ fn test_reveal_crv3_commits_success() { #[test] fn test_reveal_crv3_commits_cannot_reveal_after_reveal_epoch() { new_test_ext(100).execute_with(|| { - use ark_serialize::CanonicalSerialize; - let netuid = NetUid::from(1); let hotkey1: AccountId = U256::from(1); let hotkey2: AccountId = U256::from(2); @@ -4905,6 +4904,7 @@ fn test_reveal_crv3_commits_cannot_reveal_after_reveal_epoch() { let version_key = SubtensorModule::get_weights_version_key(netuid); let payload = WeightsTlockPayload { + hotkey: hotkey1.encode(), values: vec![10, 20], uids: vec![neuron_uid1, neuron_uid2], version_key, @@ -4940,14 +4940,15 @@ fn test_reveal_crv3_commits_cannot_reveal_after_reveal_epoch() { ct.serialize_compressed(&mut commit_bytes) .expect("Failed to serialize commit"); - assert_ok!(SubtensorModule::do_commit_crv3_weights( + assert_ok!(SubtensorModule::do_commit_timelocked_weights( RuntimeOrigin::signed(hotkey1), netuid, commit_bytes .clone() .try_into() .expect("Failed to convert commit bytes into bounded vector"), - reveal_round + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() )); // Do NOT insert the pulse at this time; this simulates the missing pulse during the reveal epoch @@ -5017,14 +5018,15 @@ fn test_do_commit_crv3_weights_success() { SubtensorModule::set_weights_set_rate_limit(netuid, 0); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - assert_ok!(SubtensorModule::do_commit_crv3_weights( + assert_ok!(SubtensorModule::do_commit_timelocked_weights( RuntimeOrigin::signed(hotkey), netuid, commit_data .clone() .try_into() .expect("Failed to convert commit data into bounded vector"), - reveal_round + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() )); let cur_epoch = @@ -5052,13 +5054,14 @@ fn test_do_commit_crv3_weights_disabled() { SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); assert_err!( - SubtensorModule::do_commit_crv3_weights( + SubtensorModule::do_commit_timelocked_weights( RuntimeOrigin::signed(hotkey), netuid, commit_data .try_into() .expect("Failed to convert commit data into bounded vector"), - reveal_round + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() ), Error::::CommitRevealDisabled ); @@ -5081,13 +5084,14 @@ fn test_do_commit_crv3_weights_hotkey_not_registered() { SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); assert_err!( - SubtensorModule::do_commit_crv3_weights( + SubtensorModule::do_commit_timelocked_weights( RuntimeOrigin::signed(unregistered_hotkey), netuid, commit_data .try_into() .expect("Failed to convert commit data into bounded vector"), - reveal_round + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() ), Error::::HotKeyNotRegisteredInSubNet ); @@ -5112,25 +5116,27 @@ fn test_do_commit_crv3_weights_committing_too_fast() { SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey).expect("Expected uid"); SubtensorModule::set_last_update_for_uid(netuid, neuron_uid, 0); - assert_ok!(SubtensorModule::do_commit_crv3_weights( + assert_ok!(SubtensorModule::do_commit_timelocked_weights( RuntimeOrigin::signed(hotkey), netuid, commit_data_1 .clone() .try_into() .expect("Failed to convert commit data into bounded vector"), - reveal_round + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() )); assert_err!( - SubtensorModule::do_commit_crv3_weights( + SubtensorModule::do_commit_timelocked_weights( RuntimeOrigin::signed(hotkey), netuid, commit_data_2 .clone() .try_into() .expect("Failed to convert commit data into bounded vector"), - reveal_round + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() ), Error::::CommittingWeightsTooFast ); @@ -5138,27 +5144,29 @@ fn test_do_commit_crv3_weights_committing_too_fast() { step_block(2); assert_err!( - SubtensorModule::do_commit_crv3_weights( + SubtensorModule::do_commit_timelocked_weights( RuntimeOrigin::signed(hotkey), netuid, commit_data_2 .clone() .try_into() .expect("Failed to convert commit data into bounded vector"), - reveal_round + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() ), Error::::CommittingWeightsTooFast ); step_block(3); - assert_ok!(SubtensorModule::do_commit_crv3_weights( + assert_ok!(SubtensorModule::do_commit_timelocked_weights( RuntimeOrigin::signed(hotkey), netuid, commit_data_2 .try_into() .expect("Failed to convert commit data into bounded vector"), - reveal_round + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() )); }); } @@ -5185,11 +5193,12 @@ fn test_do_commit_crv3_weights_too_many_unrevealed_commits() { .try_into() .expect("Failed to convert commit data into bounded vector"); - assert_ok!(SubtensorModule::do_commit_crv3_weights( + assert_ok!(SubtensorModule::do_commit_timelocked_weights( RuntimeOrigin::signed(hotkey1), netuid, bounded_commit_data, - reveal_round + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() )); } @@ -5200,11 +5209,12 @@ fn test_do_commit_crv3_weights_too_many_unrevealed_commits() { .expect("Failed to convert new commit data into bounded vector"); assert_err!( - SubtensorModule::do_commit_crv3_weights( + SubtensorModule::do_commit_timelocked_weights( RuntimeOrigin::signed(hotkey1), netuid, bounded_new_commit_data, - reveal_round + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() ), Error::::TooManyUnrevealedCommits ); @@ -5215,11 +5225,12 @@ fn test_do_commit_crv3_weights_too_many_unrevealed_commits() { .try_into() .expect("Failed to convert commit data into bounded vector"); - assert_ok!(SubtensorModule::do_commit_crv3_weights( + assert_ok!(SubtensorModule::do_commit_timelocked_weights( RuntimeOrigin::signed(hotkey2), netuid, bounded_commit_data_hotkey2, - reveal_round + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() )); // Hotkey2 can submit up to 10 commits @@ -5229,11 +5240,12 @@ fn test_do_commit_crv3_weights_too_many_unrevealed_commits() { .try_into() .expect("Failed to convert commit data into bounded vector"); - assert_ok!(SubtensorModule::do_commit_crv3_weights( + assert_ok!(SubtensorModule::do_commit_timelocked_weights( RuntimeOrigin::signed(hotkey2), netuid, bounded_commit_data, - reveal_round + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() )); } @@ -5244,11 +5256,12 @@ fn test_do_commit_crv3_weights_too_many_unrevealed_commits() { .expect("Failed to convert new commit data into bounded vector"); assert_err!( - SubtensorModule::do_commit_crv3_weights( + SubtensorModule::do_commit_timelocked_weights( RuntimeOrigin::signed(hotkey2), netuid, bounded_new_commit_data, - reveal_round + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() ), Error::::TooManyUnrevealedCommits ); @@ -5259,11 +5272,12 @@ fn test_do_commit_crv3_weights_too_many_unrevealed_commits() { let bounded_new_commit_data = new_commit_data .try_into() .expect("Failed to convert new commit data into bounded vector"); - assert_ok!(SubtensorModule::do_commit_crv3_weights( + assert_ok!(SubtensorModule::do_commit_timelocked_weights( RuntimeOrigin::signed(hotkey1), netuid, bounded_new_commit_data, - reveal_round + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() )); }); } @@ -5287,11 +5301,12 @@ fn test_reveal_crv3_commits_decryption_failure() { .try_into() .expect("Failed to convert commit bytes into bounded vector"); - assert_ok!(SubtensorModule::do_commit_crv3_weights( + assert_ok!(SubtensorModule::do_commit_timelocked_weights( RuntimeOrigin::signed(hotkey), netuid, bounded_commit_bytes, - reveal_round + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() )); step_epochs(1, netuid); @@ -5323,8 +5338,6 @@ fn test_reveal_crv3_commits_decryption_failure() { #[test] fn test_reveal_crv3_commits_multiple_commits_some_fail_some_succeed() { new_test_ext(100).execute_with(|| { - use ark_serialize::CanonicalSerialize; - let netuid = NetUid::from(1); let hotkey1: AccountId = U256::from(1); let hotkey2: AccountId = U256::from(2); @@ -5342,6 +5355,7 @@ fn test_reveal_crv3_commits_multiple_commits_some_fail_some_succeed() { .expect("Failed to get neuron UID for hotkey1"); let version_key = SubtensorModule::get_weights_version_key(netuid); let valid_payload = WeightsTlockPayload { + hotkey: hotkey1.encode(), values: vec![10], uids: vec![neuron_uid1], version_key, @@ -5394,17 +5408,19 @@ fn test_reveal_crv3_commits_multiple_commits_some_fail_some_succeed() { .expect("Failed to serialize invalid commit"); // Insert both commits - assert_ok!(SubtensorModule::do_commit_crv3_weights( + assert_ok!(SubtensorModule::do_commit_timelocked_weights( RuntimeOrigin::signed(hotkey1), netuid, commit_bytes_valid.try_into().expect("Failed to convert valid commit data"), - reveal_round + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() )); - assert_ok!(SubtensorModule::do_commit_crv3_weights( + assert_ok!(SubtensorModule::do_commit_timelocked_weights( RuntimeOrigin::signed(hotkey2), netuid, commit_bytes_invalid.try_into().expect("Failed to convert invalid commit data"), - reveal_round + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() )); // Insert the pulse @@ -5447,8 +5463,6 @@ fn test_reveal_crv3_commits_multiple_commits_some_fail_some_succeed() { #[test] fn test_reveal_crv3_commits_do_set_weights_failure() { new_test_ext(1).execute_with(|| { - use ark_serialize::CanonicalSerialize; - let netuid = NetUid::from(1); let hotkey: AccountId = U256::from(1); let reveal_round: u64 = 1000; @@ -5462,6 +5476,7 @@ fn test_reveal_crv3_commits_do_set_weights_failure() { // Prepare payload with mismatched uids and values lengths let version_key = SubtensorModule::get_weights_version_key(netuid); let payload = WeightsTlockPayload { + hotkey: hotkey.encode(), values: vec![10, 20], // Length 2 uids: vec![0], // Length 1 version_key, @@ -5496,11 +5511,12 @@ fn test_reveal_crv3_commits_do_set_weights_failure() { ct.serialize_compressed(&mut commit_bytes) .expect("Failed to serialize commit"); - assert_ok!(SubtensorModule::do_commit_crv3_weights( + assert_ok!(SubtensorModule::do_commit_timelocked_weights( RuntimeOrigin::signed(hotkey), netuid, commit_bytes.try_into().expect("Failed to convert commit data into bounded vector"), - reveal_round + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() )); let sig_bytes = hex::decode("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39") @@ -5533,8 +5549,6 @@ fn test_reveal_crv3_commits_do_set_weights_failure() { #[test] fn test_reveal_crv3_commits_payload_decoding_failure() { new_test_ext(1).execute_with(|| { - use ark_serialize::CanonicalSerialize; - let netuid = NetUid::from(1); let hotkey: AccountId = U256::from(1); let reveal_round: u64 = 1000; @@ -5575,11 +5589,12 @@ fn test_reveal_crv3_commits_payload_decoding_failure() { ct.serialize_compressed(&mut commit_bytes) .expect("Failed to serialize commit"); - assert_ok!(SubtensorModule::do_commit_crv3_weights( + assert_ok!(SubtensorModule::do_commit_timelocked_weights( RuntimeOrigin::signed(hotkey), netuid, commit_bytes.try_into().expect("Failed to convert commit data into bounded vector"), - reveal_round + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() )); let sig_bytes = hex::decode("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39") @@ -5612,8 +5627,6 @@ fn test_reveal_crv3_commits_payload_decoding_failure() { #[test] fn test_reveal_crv3_commits_signature_deserialization_failure() { new_test_ext(1).execute_with(|| { - use ark_serialize::CanonicalSerialize; - let netuid = NetUid::from(1); let hotkey: AccountId = U256::from(1); let reveal_round: u64 = 1000; @@ -5626,6 +5639,7 @@ fn test_reveal_crv3_commits_signature_deserialization_failure() { let version_key = SubtensorModule::get_weights_version_key(netuid); let payload = WeightsTlockPayload { + hotkey: hotkey.encode(), values: vec![10, 20], uids: vec![0, 1], version_key, @@ -5660,11 +5674,12 @@ fn test_reveal_crv3_commits_signature_deserialization_failure() { ct.serialize_compressed(&mut commit_bytes) .expect("Failed to serialize commit"); - assert_ok!(SubtensorModule::do_commit_crv3_weights( + assert_ok!(SubtensorModule::do_commit_timelocked_weights( RuntimeOrigin::signed(hotkey), netuid, commit_bytes.try_into().expect("Failed to convert commit data into bounded vector"), - reveal_round + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() )); pallet_drand::Pulses::::insert( @@ -5724,11 +5739,12 @@ fn test_do_commit_crv3_weights_commit_size_exceeds_limit() { .expect("Failed to create BoundedVec with data at max size"); // Now call the function with valid data at max size - assert_ok!(SubtensorModule::do_commit_crv3_weights( + assert_ok!(SubtensorModule::do_commit_timelocked_weights( RuntimeOrigin::signed(hotkey), netuid, bounded_commit_data, - reveal_round + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() )); }); } @@ -5757,8 +5773,6 @@ fn test_reveal_crv3_commits_with_empty_commit_queue() { #[test] fn test_reveal_crv3_commits_with_incorrect_identity_message() { new_test_ext(1).execute_with(|| { - use ark_serialize::CanonicalSerialize; - let netuid = NetUid::from(1); let hotkey: AccountId = U256::from(1); let reveal_round: u64 = 1000; @@ -5774,6 +5788,7 @@ fn test_reveal_crv3_commits_with_incorrect_identity_message() { .expect("Failed to get neuron UID for hotkey"); let version_key = SubtensorModule::get_weights_version_key(netuid); let payload = WeightsTlockPayload { + hotkey: hotkey.encode(), values: vec![10], uids: vec![neuron_uid], version_key, @@ -5809,11 +5824,12 @@ fn test_reveal_crv3_commits_with_incorrect_identity_message() { ct.serialize_compressed(&mut commit_bytes) .expect("Failed to serialize commit"); - assert_ok!(SubtensorModule::do_commit_crv3_weights( + assert_ok!(SubtensorModule::do_commit_timelocked_weights( RuntimeOrigin::signed(hotkey), netuid, commit_bytes.try_into().expect("Failed to convert commit data into bounded vector"), - reveal_round + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() )); let sig_bytes = hex::decode("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39") @@ -5857,13 +5873,14 @@ fn test_multiple_commits_by_same_hotkey_within_limit() { for i in 0..10 { let commit_data: Vec = vec![i; 5]; - assert_ok!(SubtensorModule::do_commit_crv3_weights( + assert_ok!(SubtensorModule::do_commit_timelocked_weights( RuntimeOrigin::signed(hotkey), netuid, commit_data .try_into() .expect("Failed to convert commit data into bounded vector"), - reveal_round + i as u64 + reveal_round + i as u64, + SubtensorModule::get_commit_reveal_weights_version() )); } @@ -5884,58 +5901,54 @@ fn test_reveal_crv3_commits_removes_past_epoch_commits() { new_test_ext(100).execute_with(|| { let netuid = NetUid::from(1); let hotkey: AccountId = U256::from(1); - let reveal_round: u64 = 1000; + let reveal_round: u64 = 1_000; - // Initialize network and neuron - add_network(netuid, 5, 0); + add_network(netuid, /*tempo*/ 5, 0); register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - SubtensorModule::set_reveal_period(netuid, 1); + SubtensorModule::set_reveal_period(netuid, 1); // reveal_period = 1 epoch SubtensorModule::set_weights_set_rate_limit(netuid, 0); - let current_block = SubtensorModule::get_current_block_as_u64(); - let current_epoch = SubtensorModule::get_epoch_index(netuid, current_block); + // --------------------------------------------------------------------- + // Put dummy commits into the two epochs immediately *before* current. + // --------------------------------------------------------------------- + let cur_block = SubtensorModule::get_current_block_as_u64(); + let cur_epoch = SubtensorModule::get_epoch_index(netuid, cur_block); + let past_epoch = cur_epoch.saturating_sub(2); // definitely < reveal_epoch + let reveal_epoch = cur_epoch.saturating_sub(1); // == cur_epoch - reveal_period + + for &epoch in &[past_epoch, reveal_epoch] { + let bounded_commit = vec![epoch as u8; 5].try_into().expect("bounded vec"); - // Simulate commits in past epochs - let past_epochs = vec![current_epoch - 2, current_epoch - 1]; - for epoch in &past_epochs { - let commit_data: Vec = vec![*epoch as u8; 5]; - let bounded_commit_data = commit_data - .clone() - .try_into() - .expect("Failed to convert commit data into bounded vector"); assert_ok!(CRV3WeightCommitsV2::::try_mutate( netuid, - *epoch, - |commits| -> DispatchResult { - commits.push_back((hotkey, current_block, bounded_commit_data, reveal_round)); + epoch, + |q| -> DispatchResult { + q.push_back((hotkey, cur_block, bounded_commit, reveal_round)); Ok(()) } )); } - for epoch in &past_epochs { - let commits = CRV3WeightCommitsV2::::get(netuid, *epoch); - assert!( - !commits.is_empty(), - "Expected commits to be present for past epoch {epoch}" - ); - } + // Sanity – both epochs presently hold a commit. + assert!(!CRV3WeightCommitsV2::::get(netuid, past_epoch).is_empty()); + assert!(!CRV3WeightCommitsV2::::get(netuid, reveal_epoch).is_empty()); + // --------------------------------------------------------------------- + // Run the reveal pass WITHOUT a pulse – only expiry housekeeping runs. + // --------------------------------------------------------------------- assert_ok!(SubtensorModule::reveal_crv3_commits(netuid)); - for epoch in &past_epochs { - let commits = CRV3WeightCommitsV2::::get(netuid, *epoch); - assert!( - commits.is_empty(), - "Expected commits for past epoch {epoch} to be removed" - ); - } + // past_epoch (< reveal_epoch) must be gone + assert!( + CRV3WeightCommitsV2::::get(netuid, past_epoch).is_empty(), + "expired epoch {past_epoch} should be cleared" + ); - let current_epoch_commits = CRV3WeightCommitsV2::::get(netuid, current_epoch); + // reveal_epoch queue is *kept* because its commit could still be revealed later. assert!( - current_epoch_commits.is_empty(), - "Expected no commits for current epoch {current_epoch}" + !CRV3WeightCommitsV2::::get(netuid, reveal_epoch).is_empty(), + "reveal-epoch {reveal_epoch} must be retained until commit can be revealed" ); }); } @@ -5944,182 +5957,110 @@ fn test_reveal_crv3_commits_removes_past_epoch_commits() { #[test] fn test_reveal_crv3_commits_multiple_valid_commits_all_processed() { new_test_ext(100).execute_with(|| { - use ark_serialize::CanonicalSerialize; - let netuid = NetUid::from(1); - let reveal_round: u64 = 1000; + let reveal_round: u64 = 1_000; - // Initialize the network + // ───── network parameters ─────────────────────────────────────────── add_network(netuid, 5, 0); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); SubtensorModule::set_reveal_period(netuid, 1); SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_stake_threshold(0); SubtensorModule::set_max_registrations_per_block(netuid, 100); SubtensorModule::set_target_registrations_per_interval(netuid, 100); - // Register multiple neurons (e.g., 5 neurons) - let num_neurons = 5; - let mut hotkeys = Vec::new(); - let mut neuron_uids = Vec::new(); - for i in 0..num_neurons { - let hotkey: AccountId = U256::from(i + 1); - register_ok_neuron(netuid, hotkey, U256::from(i + 100), 100_000); + // pulse for round 1000 + let sig_bytes = hex::decode( + "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e\ + 342b73a8dd2bacbe47e4b6b63ed5e39", + ) + .unwrap(); + pallet_drand::Pulses::::insert( + reveal_round, + Pulse { + round: reveal_round, + randomness: vec![0; 32].try_into().unwrap(), + signature: sig_bytes.try_into().unwrap(), + }, + ); + + // ───── five neurons (hotkeys 1‑5) ─────────────────────────────────── + let hotkeys: Vec<_> = (1..=5).map(U256::from).collect(); + for (i, hk) in hotkeys.iter().enumerate() { + let cold: AccountId = U256::from(i + 100); + + register_ok_neuron(netuid, *hk, cold, 100_000); SubtensorModule::set_validator_permit_for_uid(netuid, i as u16, true); - hotkeys.push(hotkey); - neuron_uids.push( - SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey) - .expect("Failed to get neuron UID"), - ); - } - let version_key = SubtensorModule::get_weights_version_key(netuid); + // add minimal stake so `do_set_weights` will succeed + SubtensorModule::add_balance_to_coldkey_account(&cold, 1); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + hk, + &cold, + netuid, + 1.into(), + ); - // Prepare payloads and commits for each hotkey - let esk = [2; 32]; - let pk_bytes = hex::decode("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a") - .expect("Failed to decode public key bytes"); - let pub_key = ::PublicKeyGroup::deserialize_compressed(&*pk_bytes) - .expect("Failed to deserialize public key"); + step_block(1); // avoids TooManyRegistrationsThisBlock + } - let message = { - let mut hasher = sha2::Sha256::new(); - hasher.update(reveal_round.to_be_bytes()); - hasher.finalize().to_vec() - }; - let identity = Identity::new(b"", vec![message]); + // ───── create & submit commits for each hotkey ────────────────────── + let esk = [2u8; 32]; + let pk_bytes = hex::decode( + "83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c\ + 8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb\ + 5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a", + ) + .unwrap(); + let pk = + ::PublicKeyGroup::deserialize_compressed(&*pk_bytes).unwrap(); - let mut commits = Vec::new(); - for (i, hotkey) in hotkeys.iter().enumerate() { - // Each neuron will assign weights to all neurons, including itself - let values: Vec = (0..num_neurons as u16) - .map(|v| (v + i as u16 + 1) * 10) - .collect(); + for (i, hk) in hotkeys.iter().enumerate() { let payload = WeightsTlockPayload { - values: values.clone(), - uids: neuron_uids.clone(), - version_key, + hotkey: hk.encode(), + values: vec![10, 20, 30, 40, 50], + uids: (0..5).map(|u| u as u16).collect(), + version_key: SubtensorModule::get_weights_version_key(netuid), }; - let serialized_payload = payload.encode(); - - let rng = ChaCha20Rng::seed_from_u64(i as u64); + let id_msg = { + let mut h = sha2::Sha256::new(); + h.update(reveal_round.to_be_bytes()); + h.finalize().to_vec() + }; let ct = tle::( - pub_key, + pk, esk, - &serialized_payload, - identity.clone(), - rng, + &payload.encode(), + Identity::new(b"", vec![id_msg]), + ChaCha20Rng::seed_from_u64(i as u64), ) - .expect("Encryption failed"); + .unwrap(); let mut commit_bytes = Vec::new(); - ct.serialize_compressed(&mut commit_bytes) - .expect("Failed to serialize commit"); + ct.serialize_compressed(&mut commit_bytes).unwrap(); - // Submit the commit - assert_ok!(SubtensorModule::do_commit_crv3_weights( - RuntimeOrigin::signed(*hotkey), + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(*hk), netuid, - commit_bytes - .try_into() - .expect("Failed to convert commit data"), - reveal_round + commit_bytes.try_into().unwrap(), + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() )); - - // Store the expected weights for later comparison - commits.push((hotkey, payload)); } - // Insert the pulse - let sig_bytes = hex::decode("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39") - .expect("Failed to decode signature bytes"); - - pallet_drand::Pulses::::insert( - reveal_round, - Pulse { - round: reveal_round, - randomness: vec![0; 32] - .try_into() - .expect("Failed to convert randomness vector"), - signature: sig_bytes - .try_into() - .expect("Failed to convert signature bytes"), - }, - ); - - // Advance epoch to trigger reveal - step_epochs(1, netuid); - - // Verify weights for all hotkeys - let weights_sparse = SubtensorModule::get_weights_sparse(netuid); - - // Set acceptable delta for `I32F32` weights - let delta = I32F32::from_num(0.0001); - - for (hotkey, expected_payload) in commits { - let neuron_uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, hotkey) - .expect("Failed to get neuron UID for hotkey") as usize; - let weights = weights_sparse - .get(neuron_uid) - .cloned() - .unwrap_or_default(); + // advance reveal_period + 1 epochs → 2 epochs + step_epochs(2, netuid); + // ───── assertions ─────────────────────────────────────────────────── + let w_sparse = SubtensorModule::get_weights_sparse(netuid); + for hk in hotkeys { + let uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hk).unwrap() as usize; assert!( - !weights.is_empty(), - "Weights for neuron_uid {neuron_uid} should be set" + !w_sparse.get(uid).unwrap_or(&Vec::new()).is_empty(), + "weights for uid {uid} should be set" ); - - // Normalize expected weights - let expected_weights: Vec<(u16, I32F32)> = expected_payload - .uids - .iter() - .zip(expected_payload.values.iter()) - .map(|(&uid, &value)| (uid, I32F32::from_num(value))) - .collect(); - - let total_expected_weight: I32F32 = - expected_weights.iter().map(|&(_, w)| w).sum(); - - let normalized_expected_weights: Vec<(u16, I32F32)> = expected_weights - .iter() - .map(|&(uid, w)| (uid, w / total_expected_weight * I32F32::from_num(30))) - .collect(); - - // Normalize actual weights - let total_weight: I32F32 = weights.iter().map(|&(_, w)| w).sum(); - - let normalized_weights: Vec<(u16, I32F32)> = weights - .iter() - .map(|&(uid, w)| (uid, w / total_weight * I32F32::from_num(30))) - .collect(); - - // Compare expected and actual weights with acceptable delta - for ((uid_expected, weight_expected), (uid_actual, weight_actual)) in - normalized_expected_weights.iter().zip(normalized_weights.iter()) - { - assert_eq!( - uid_expected, uid_actual, - "UID mismatch: expected {uid_expected}, got {uid_actual}" - ); - - let diff = (*weight_expected - *weight_actual).abs(); - assert!( - diff <= delta, - "Weight mismatch for uid {uid_expected}: expected {weight_expected}, got {weight_actual}, diff {diff}" - ); - } } - - // Verify that commits storage is empty - let cur_epoch = SubtensorModule::get_epoch_index( - netuid, - SubtensorModule::get_current_block_as_u64(), - ); - let commits = CRV3WeightCommitsV2::::get(netuid, cur_epoch); - assert!( - commits.is_empty(), - "Expected no commits left in storage after reveal" - ); }); } @@ -6127,180 +6068,110 @@ fn test_reveal_crv3_commits_multiple_valid_commits_all_processed() { #[test] fn test_reveal_crv3_commits_max_neurons() { new_test_ext(100).execute_with(|| { - use ark_serialize::CanonicalSerialize; - let netuid = NetUid::from(1); - let reveal_round: u64 = 1000; + let reveal_round: u64 = 1_000; + // ───── network parameters ─────────────────────────────────────────── add_network(netuid, 5, 0); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); SubtensorModule::set_reveal_period(netuid, 1); SubtensorModule::set_weights_set_rate_limit(netuid, 0); - SubtensorModule::set_max_registrations_per_block(netuid, 10000); - SubtensorModule::set_target_registrations_per_interval(netuid, 10000); - SubtensorModule::set_max_allowed_uids(netuid, 10024); - - let num_neurons = 1_024; - let mut hotkeys = Vec::new(); - let mut neuron_uids = Vec::new(); - for i in 0..num_neurons { - let hotkey: AccountId = U256::from(i + 1); - register_ok_neuron(netuid, hotkey, U256::from(i + 100), 100_000); - SubtensorModule::set_validator_permit_for_uid(netuid, i as u16, true); - hotkeys.push(hotkey); - neuron_uids.push( - SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey) - .expect("Failed to get neuron UID"), + SubtensorModule::set_stake_threshold(0); + SubtensorModule::set_max_registrations_per_block(netuid, 10_000); + SubtensorModule::set_target_registrations_per_interval(netuid, 10_000); + SubtensorModule::set_max_allowed_uids(netuid, 10_024); + + // ───── register 1 024 neurons ─────────────────────────────────────── + for i in 0..1_024u16 { + let hk: AccountId = U256::from(i as u64 + 1); + let cold: AccountId = U256::from(i as u64 + 10_000); + + register_ok_neuron(netuid, hk, cold, 100_000); + SubtensorModule::set_validator_permit_for_uid(netuid, i, true); + + // give each neuron a nominal stake (safe even if not needed) + SubtensorModule::add_balance_to_coldkey_account(&cold, 1); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hk, + &cold, + netuid, + 1.into(), ); - } - let version_key = SubtensorModule::get_weights_version_key(netuid); + step_block(1); // avoid registration‑limit panic + } - // Prepare payloads and commits for 3 hotkeys - let esk = [2; 32]; - let pk_bytes = hex::decode("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a") - .expect("Failed to decode public key bytes"); - let pub_key = ::PublicKeyGroup::deserialize_compressed(&*pk_bytes) - .expect("Failed to deserialize public key"); + // ───── pulse for round 1000 ───────────────────────────────────────── + let sig_bytes = hex::decode( + "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e\ + 342b73a8dd2bacbe47e4b6b63ed5e39", + ) + .unwrap(); + pallet_drand::Pulses::::insert( + reveal_round, + Pulse { + round: reveal_round, + randomness: vec![0; 32].try_into().unwrap(), + signature: sig_bytes.try_into().unwrap(), + }, + ); - let message = { - let mut hasher = sha2::Sha256::new(); - hasher.update(reveal_round.to_be_bytes()); - hasher.finalize().to_vec() - }; - let identity = Identity::new(b"", vec![message]); + // ───── three committing hotkeys ───────────────────────────────────── + let esk = [2u8; 32]; + let pk_bytes = hex::decode( + "83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c\ + 8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb\ + 5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a", + ) + .unwrap(); + let pk = + ::PublicKeyGroup::deserialize_compressed(&*pk_bytes).unwrap(); + let committing_hotkeys = [U256::from(1), U256::from(2), U256::from(3)]; - let hotkeys_to_commit = &hotkeys[0..3]; // First 3 hotkeys will submit weight commits - let mut commits = Vec::new(); - for (i, hotkey) in hotkeys_to_commit.iter().enumerate() { - // Each neuron will assign weights to all neurons - let values: Vec = vec![10; num_neurons]; // Assign weight of 10 to each neuron + for (i, hk) in committing_hotkeys.iter().enumerate() { let payload = WeightsTlockPayload { - values: values.clone(), - uids: neuron_uids.clone(), - version_key, + hotkey: hk.encode(), + values: vec![10u16; 1_024], + uids: (0..1_024).collect(), + version_key: SubtensorModule::get_weights_version_key(netuid), + }; + let id_msg = { + let mut h = sha2::Sha256::new(); + h.update(reveal_round.to_be_bytes()); + h.finalize().to_vec() }; - let serialized_payload = payload.encode(); - - let rng = ChaCha20Rng::seed_from_u64(i as u64); - let ct = tle::( - pub_key, + pk, esk, - &serialized_payload, - identity.clone(), - rng, + &payload.encode(), + Identity::new(b"", vec![id_msg]), + ChaCha20Rng::seed_from_u64(i as u64), ) - .expect("Encryption failed"); - + .unwrap(); let mut commit_bytes = Vec::new(); - ct.serialize_compressed(&mut commit_bytes) - .expect("Failed to serialize commit"); + ct.serialize_compressed(&mut commit_bytes).unwrap(); - // Submit the commit - assert_ok!(SubtensorModule::do_commit_crv3_weights( - RuntimeOrigin::signed(*hotkey), + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(*hk), netuid, - commit_bytes - .try_into() - .expect("Failed to convert commit data"), - reveal_round + commit_bytes.try_into().unwrap(), + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() )); - - // Store the expected weights for later comparison - commits.push((hotkey, payload)); } - // Insert the pulse - let sig_bytes = hex::decode("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39") - .expect("Failed to decode signature bytes"); - - pallet_drand::Pulses::::insert( - reveal_round, - Pulse { - round: reveal_round, - randomness: vec![0; 32] - .try_into() - .expect("Failed to convert randomness vector"), - signature: sig_bytes - .try_into() - .expect("Failed to convert signature bytes"), - }, - ); - - // Advance epoch to trigger reveal - step_epochs(1, netuid); - - // Verify weights for the hotkeys that submitted commits - let weights_sparse = SubtensorModule::get_weights_sparse(netuid); - - // Set acceptable delta for `I32F32` weights - let delta = I32F32::from_num(0.0001); // Adjust delta as needed - - for (hotkey, expected_payload) in commits { - let neuron_uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, hotkey) - .expect("Failed to get neuron UID for hotkey") as usize; - let weights = weights_sparse - .get(neuron_uid) - .cloned() - .unwrap_or_default(); + // ───── advance reveal_period + 1 epochs ───────────────────────────── + step_epochs(2, netuid); + // ───── verify weights ─────────────────────────────────────────────── + let w_sparse = SubtensorModule::get_weights_sparse(netuid); + for hk in &committing_hotkeys { + let uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, hk).unwrap() as usize; assert!( - !weights.is_empty(), - "Weights for neuron_uid {neuron_uid} should be set" + !w_sparse.get(uid).unwrap_or(&Vec::new()).is_empty(), + "weights for uid {uid} should be set" ); - - // Normalize expected weights - let expected_weights: Vec<(u16, I32F32)> = expected_payload - .uids - .iter() - .zip(expected_payload.values.iter()) - .map(|(&uid, &value)| (uid, I32F32::from_num(value))) - .collect(); - - let total_expected_weight: I32F32 = - expected_weights.iter().map(|&(_, w)| w).sum(); - - let normalized_expected_weights: Vec<(u16, I32F32)> = expected_weights - .iter() - .map(|&(uid, w)| (uid, w / total_expected_weight * I32F32::from_num(30))) - .collect(); - - // Normalize actual weights - let total_weight: I32F32 = weights.iter().map(|&(_, w)| w).sum(); - - let normalized_weights: Vec<(u16, I32F32)> = weights - .iter() - .map(|&(uid, w)| (uid, w / total_weight * I32F32::from_num(30))) - .collect(); - - // Compare expected and actual weights with acceptable delta - for ((uid_expected, weight_expected), (uid_actual, weight_actual)) in - normalized_expected_weights.iter().zip(normalized_weights.iter()) - { - assert_eq!( - uid_expected, uid_actual, - "UID mismatch: expected {uid_expected}, got {uid_actual}" - ); - - let diff = (*weight_expected - *weight_actual).abs(); - assert!( - diff <= delta, - "Weight mismatch for uid {uid_expected}: expected {weight_expected}, got {weight_actual}, diff {diff}" - ); - } } - - // Verify that commits storage is empty - let cur_epoch = SubtensorModule::get_epoch_index( - netuid, - SubtensorModule::get_current_block_as_u64(), - ); - let commits = CRV3WeightCommitsV2::::get(netuid, cur_epoch); - assert!( - commits.is_empty(), - "Expected no commits left in storage after reveal" - ); }); } @@ -6412,3 +6283,524 @@ fn test_get_first_block_of_epoch_step_blocks_and_assert_with_until_next() { } }); } + +#[test] +fn test_reveal_crv3_commits_hotkey_check() { + new_test_ext(100).execute_with(|| { + // Failure case: hotkey mismatch + let netuid = NetUid::from(1); + let hotkey1: AccountId = U256::from(1); + let hotkey2: AccountId = U256::from(2); + let reveal_round: u64 = 1000; + + add_network(netuid, 5, 0); + register_ok_neuron(netuid, hotkey1, U256::from(3), 100_000); + register_ok_neuron(netuid, hotkey2, U256::from(4), 100_000); + SubtensorModule::set_stake_threshold(0); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + SubtensorModule::set_reveal_period(netuid, 3); + + let neuron_uid1 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey1) + .expect("Failed to get neuron UID for hotkey1"); + let neuron_uid2 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey2) + .expect("Failed to get neuron UID for hotkey2"); + + SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid1, true); + SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid2, true); + SubtensorModule::add_balance_to_coldkey_account(&U256::from(3), 1); + SubtensorModule::add_balance_to_coldkey_account(&U256::from(4), 1); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, + &(U256::from(3)), + netuid, + 1.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey2, + &(U256::from(4)), + netuid, + 1.into(), + ); + + let version_key = SubtensorModule::get_weights_version_key(netuid); + + let payload = WeightsTlockPayload { + hotkey: hotkey2.encode(), // Mismatch: using hotkey2 instead of hotkey1 + values: vec![10, 20], + uids: vec![neuron_uid1, neuron_uid2], + version_key, + }; + + let serialized_payload = payload.encode(); + + let esk = [2; 32]; + let rng = ChaCha20Rng::seed_from_u64(0); + + let pk_bytes = hex::decode("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a") + .expect("Failed to decode public key bytes"); + let pub_key = ::PublicKeyGroup::deserialize_compressed(&*pk_bytes) + .expect("Failed to deserialize public key"); + + let message = { + let mut hasher = sha2::Sha256::new(); + hasher.update(reveal_round.to_be_bytes()); + hasher.finalize().to_vec() + }; + let identity = Identity::new(b"", vec![message]); + + let ct = tle::( + pub_key, + esk, + &serialized_payload, + identity, + rng, + ) + .expect("Encryption failed"); + + let mut commit_bytes = Vec::new(); + ct.serialize_compressed(&mut commit_bytes) + .expect("Failed to serialize commit"); + + assert!( + !commit_bytes.is_empty(), + "commit_bytes is empty after serialization" + ); + + log::debug!( + "Commit bytes now contain {commit_bytes:#?}" + ); + + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey1), + netuid, + commit_bytes.clone().try_into().expect("Failed to convert commit bytes into bounded vector"), + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + )); + + let sig_bytes = hex::decode("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39") + .expect("Failed to decode signature bytes"); + + pallet_drand::Pulses::::insert( + reveal_round, + Pulse { + round: reveal_round, + randomness: vec![0; 32].try_into().expect("Failed to convert randomness vector"), + signature: sig_bytes.try_into().expect("Failed to convert signature bytes"), + }, + ); + + // Step epochs to run the epoch via the blockstep + step_epochs(3, netuid); + + let weights_sparse = SubtensorModule::get_weights_sparse(netuid); + let weights = weights_sparse.get(neuron_uid1 as usize).cloned().unwrap_or_default(); + + assert!( + weights.is_empty(), + "Weights for neuron_uid1 should be empty due to hotkey mismatch." + ); + }); + + new_test_ext(100).execute_with(|| { + // Success case: hotkey match + let netuid = NetUid::from(1); + let hotkey1: AccountId = U256::from(1); + let hotkey2: AccountId = U256::from(2); + let reveal_round: u64 = 1000; + + add_network(netuid, 5, 0); + register_ok_neuron(netuid, hotkey1, U256::from(3), 100_000); + register_ok_neuron(netuid, hotkey2, U256::from(4), 100_000); + SubtensorModule::set_stake_threshold(0); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + SubtensorModule::set_reveal_period(netuid, 3); + + let neuron_uid1 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey1) + .expect("Failed to get neuron UID for hotkey1"); + let neuron_uid2 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey2) + .expect("Failed to get neuron UID for hotkey2"); + + SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid1, true); + SubtensorModule::set_validator_permit_for_uid(netuid, neuron_uid2, true); + SubtensorModule::add_balance_to_coldkey_account(&U256::from(3), 1); + SubtensorModule::add_balance_to_coldkey_account(&U256::from(4), 1); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, + &(U256::from(3)), + netuid, + 1.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey2, + &(U256::from(4)), + netuid, + 1.into(), + ); + + let version_key = SubtensorModule::get_weights_version_key(netuid); + + let payload = WeightsTlockPayload { + hotkey: hotkey1.encode(), // Match: using hotkey1 + values: vec![10, 20], + uids: vec![neuron_uid1, neuron_uid2], + version_key, + }; + + let serialized_payload = payload.encode(); + + let esk = [2; 32]; + let rng = ChaCha20Rng::seed_from_u64(0); + + let pk_bytes = hex::decode("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a") + .expect("Failed to decode public key bytes"); + let pub_key = ::PublicKeyGroup::deserialize_compressed(&*pk_bytes) + .expect("Failed to deserialize public key"); + + let message = { + let mut hasher = sha2::Sha256::new(); + hasher.update(reveal_round.to_be_bytes()); + hasher.finalize().to_vec() + }; + let identity = Identity::new(b"", vec![message]); + + let ct = tle::( + pub_key, + esk, + &serialized_payload, + identity, + rng, + ) + .expect("Encryption failed"); + + let mut commit_bytes = Vec::new(); + ct.serialize_compressed(&mut commit_bytes) + .expect("Failed to serialize commit"); + + assert!( + !commit_bytes.is_empty(), + "commit_bytes is empty after serialization" + ); + + log::debug!( + "Commit bytes now contain {commit_bytes:#?}" + ); + + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey1), + netuid, + commit_bytes.clone().try_into().expect("Failed to convert commit bytes into bounded vector"), + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + )); + + let sig_bytes = hex::decode("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39") + .expect("Failed to decode signature bytes"); + + pallet_drand::Pulses::::insert( + reveal_round, + Pulse { + round: reveal_round, + randomness: vec![0; 32].try_into().expect("Failed to convert randomness vector"), + signature: sig_bytes.try_into().expect("Failed to convert signature bytes"), + }, + ); + + // Step epochs to run the epoch via the blockstep + step_epochs(3, netuid); + + let weights_sparse = SubtensorModule::get_weights_sparse(netuid); + let weights = weights_sparse.get(neuron_uid1 as usize).cloned().unwrap_or_default(); + + assert!( + !weights.is_empty(), + "Weights for neuron_uid1 are empty, expected weights to be set." + ); + + let expected_weights: Vec<(u16, I32F32)> = payload + .uids + .iter() + .zip(payload.values.iter()) + .map(|(&uid, &value)| (uid, I32F32::from_num(value))) + .collect(); + + let total_weight: I32F32 = weights.iter().map(|(_, w)| *w).sum(); + + let normalized_weights: Vec<(u16, I32F32)> = weights + .iter() + .map(|&(uid, w)| (uid, w * I32F32::from_num(30) / total_weight)) + .collect(); + + for ((uid_a, w_a), (uid_b, w_b)) in normalized_weights.iter().zip(expected_weights.iter()) { + assert_eq!(uid_a, uid_b); + + let actual_weight_f64: f64 = w_a.to_num::(); + let rounded_actual_weight = actual_weight_f64.round() as i64; + + assert!( + rounded_actual_weight != 0, + "Actual weight for uid {uid_a} is zero" + ); + + let expected_weight = w_b.to_num::(); + + assert_eq!( + rounded_actual_weight, expected_weight, + "Weight mismatch for uid {uid_a}: expected {expected_weight}, got {rounded_actual_weight}" + ); + } + }); +} + +#[test] +fn test_reveal_crv3_commits_retry_on_missing_pulse() { + new_test_ext(100).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey: AccountId = U256::from(1); + let reveal_round: u64 = 1_000; + + // ─── network & neuron ─────────────────────────────────────────────── + add_network(netuid, 5, 0); + register_ok_neuron(netuid, hotkey, U256::from(3), 100_000); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + SubtensorModule::set_reveal_period(netuid, 3); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_stake_threshold(0); + + let uid = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey).unwrap(); + SubtensorModule::set_validator_permit_for_uid(netuid, uid, true); + + // ─── craft commit ─────────────────────────────────────────────────── + let payload = WeightsTlockPayload { + hotkey: hotkey.encode(), + values: vec![10], + uids: vec![uid], + version_key: SubtensorModule::get_weights_version_key(netuid), + }; + let esk = [2u8; 32]; + let pk_bytes = hex::decode( + "83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c\ + 8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb\ + 5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a", + ) + .unwrap(); + let pk = + ::PublicKeyGroup::deserialize_compressed(&*pk_bytes).unwrap(); + let id_msg = { + let mut h = sha2::Sha256::new(); + h.update(reveal_round.to_be_bytes()); + h.finalize().to_vec() + }; + let ct = tle::( + pk, + esk, + &payload.encode(), + Identity::new(b"", vec![id_msg]), + ChaCha20Rng::seed_from_u64(0), + ) + .unwrap(); + let mut commit_bytes = Vec::new(); + ct.serialize_compressed(&mut commit_bytes).unwrap(); + + // submit commit + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_bytes.clone().try_into().unwrap(), + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + )); + + // epoch in which commit was stored + let stored_epoch = CRV3WeightCommitsV2::::iter_prefix(netuid) + .next() + .map(|(e, _)| e) + .expect("commit stored"); + + // first block of reveal epoch (commit_epoch + RP) + let first_reveal_epoch = stored_epoch + SubtensorModule::get_reveal_period(netuid); + let first_reveal_block = + SubtensorModule::get_first_block_of_epoch(netuid, first_reveal_epoch); + run_to_block_no_epoch(netuid, first_reveal_block); + + // run *one* block inside reveal epoch without pulse → commit should stay queued + step_block(1); + assert!( + !CRV3WeightCommitsV2::::get(netuid, stored_epoch).is_empty(), + "commit must remain queued when pulse is missing" + ); + + // ─── insert pulse & step one more block ───────────────────────────── + let sig_bytes = hex::decode( + "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e\ + 342b73a8dd2bacbe47e4b6b63ed5e39", + ) + .unwrap(); + pallet_drand::Pulses::::insert( + reveal_round, + Pulse { + round: reveal_round, + randomness: vec![0; 32].try_into().unwrap(), + signature: sig_bytes.try_into().unwrap(), + }, + ); + + step_block(1); // automatic reveal runs here + + let weights = SubtensorModule::get_weights_sparse(netuid) + .get(uid as usize) + .cloned() + .unwrap_or_default(); + assert!(!weights.is_empty(), "weights must be set after pulse"); + + assert!( + CRV3WeightCommitsV2::::get(netuid, stored_epoch).is_empty(), + "queue should be empty after successful reveal" + ); + }); +} + +#[test] +fn test_reveal_crv3_commits_legacy_payload_success() { + new_test_ext(100).execute_with(|| { + // ───────────────────────────────────── + // 1 ▸ network + neurons + // ───────────────────────────────────── + let netuid = NetUid::from(1); + let hotkey1: AccountId = U256::from(1); + let hotkey2: AccountId = U256::from(2); + let reveal_round: u64 = 1_000; + + add_network(netuid, /*tempo*/ 5, /*modality*/ 0); + register_ok_neuron(netuid, hotkey1, U256::from(3), 100_000); + register_ok_neuron(netuid, hotkey2, U256::from(4), 100_000); + + SubtensorModule::set_stake_threshold(0); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + SubtensorModule::set_reveal_period(netuid, 3); + + let uid1 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey1).unwrap(); + let uid2 = SubtensorModule::get_uid_for_net_and_hotkey(netuid, &hotkey2).unwrap(); + + SubtensorModule::set_validator_permit_for_uid(netuid, uid1, true); + SubtensorModule::set_validator_permit_for_uid(netuid, uid2, true); + + SubtensorModule::add_balance_to_coldkey_account(&U256::from(3), 1); + SubtensorModule::add_balance_to_coldkey_account(&U256::from(4), 1); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, + &U256::from(3), + netuid, + 1.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey2, + &U256::from(4), + netuid, + 1.into(), + ); + + // ───────────────────────────────────── + // 2 ▸ craft legacy payload (NO hotkey) + // ───────────────────────────────────── + let legacy_payload = LegacyWeightsTlockPayload { + uids: vec![uid1, uid2], + values: vec![10, 20], + version_key: SubtensorModule::get_weights_version_key(netuid), + }; + let serialized_payload = legacy_payload.encode(); + + // encrypt with TLE + let esk = [2u8; 32]; + let rng = ChaCha20Rng::seed_from_u64(0); + + let pk_bytes = hex::decode( + "83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c\ + 8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb\ + 5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a", + ) + .unwrap(); + let pk = + ::PublicKeyGroup::deserialize_compressed(&*pk_bytes).unwrap(); + + let msg_hash = { + let mut h = sha2::Sha256::new(); + h.update(reveal_round.to_be_bytes()); + h.finalize().to_vec() + }; + let identity = Identity::new(b"", vec![msg_hash]); + + let ct = tle::( + pk, + esk, + &serialized_payload, + identity, + rng, + ) + .expect("encryption must succeed"); + + let mut commit_bytes = Vec::new(); + ct.serialize_compressed(&mut commit_bytes).unwrap(); + let bounded_commit: BoundedVec<_, ConstU32> = + commit_bytes.clone().try_into().unwrap(); + + // ───────────────────────────────────── + // 3 ▸ put commit on‑chain + // ───────────────────────────────────── + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey1), + netuid, + bounded_commit, + reveal_round, + SubtensorModule::get_commit_reveal_weights_version() + )); + + // insert pulse so reveal can succeed the first time + let sig_bytes = hex::decode( + "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e3\ + 42b73a8dd2bacbe47e4b6b63ed5e39", + ) + .unwrap(); + pallet_drand::Pulses::::insert( + reveal_round, + Pulse { + round: reveal_round, + randomness: vec![0; 32].try_into().unwrap(), + signature: sig_bytes.try_into().unwrap(), + }, + ); + + let commit_block = SubtensorModule::get_current_block_as_u64(); + let commit_epoch = SubtensorModule::get_epoch_index(netuid, commit_block); + + // ───────────────────────────────────── + // 4 ▸ advance epochs to trigger reveal + // ───────────────────────────────────── + step_epochs(3, netuid); + + // ───────────────────────────────────── + // 5 ▸ assertions + // ───────────────────────────────────── + let weights_sparse = SubtensorModule::get_weights_sparse(netuid); + let w1 = weights_sparse + .get(uid1 as usize) + .cloned() + .unwrap_or_default(); + assert!(!w1.is_empty(), "weights must be set for uid1"); + + // find raw values for uid1 & uid2 + let w_map: std::collections::HashMap<_, _> = w1.into_iter().collect(); + let v1 = *w_map.get(&uid1).expect("uid1 weight"); + let v2 = *w_map.get(&uid2).expect("uid2 weight"); + assert!(v2 > v1, "uid2 weight should be greater than uid1 (20 > 10)"); + + // commit should be gone + assert!( + CRV3WeightCommitsV2::::get(netuid, commit_epoch).is_empty(), + "commit storage should be cleaned after reveal" + ); + }); +} diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index 655d320c2f..7149d501ee 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -481,7 +481,13 @@ impl Pallet { CommitRevealWeightsEnabled::::set(netuid, enabled); Self::deposit_event(Event::CommitRevealEnabled(netuid, enabled)); } - + pub fn get_commit_reveal_weights_version() -> u16 { + CommitRevealWeightsVersion::::get() + } + pub fn set_commit_reveal_weights_version(version: u16) { + CommitRevealWeightsVersion::::set(version); + Self::deposit_event(Event::CommitRevealVersionSet(version)); + } pub fn get_rho(netuid: NetUid) -> u16 { Rho::::get(netuid) }