diff --git a/Cargo.lock b/Cargo.lock index 986d54a..44e4460 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1195,7 +1195,7 @@ dependencies = [ [[package]] name = "casper-runtime" -version = "4.0.14" +version = "4.0.15" dependencies = [ "casper-runtime-constants", "frame-benchmarking", @@ -1209,6 +1209,7 @@ dependencies = [ "frame-try-runtime", "ghost-core-primitives", "ghost-exodus", + "ghost-exodus-runtime-api", "ghost-governance", "ghost-helpers", "ghost-networks", @@ -3684,7 +3685,7 @@ dependencies = [ [[package]] name = "ghost-exodus" -version = "0.0.17" +version = "0.1.1" dependencies = [ "chacha20poly1305", "frame-benchmarking", @@ -3694,6 +3695,7 @@ dependencies = [ "frost-core", "frost-ed25519", "frost-secp256k1-tr", + "ghost-exodus-runtime-api", "ghost-helpers", "ghost-networks", "ghost-traits", @@ -3725,10 +3727,12 @@ dependencies = [ [[package]] name = "ghost-exodus-runtime-api" -version = "0.0.0" +version = "0.0.2" dependencies = [ + "ghost-helpers", "parity-scale-codec", "sp-api", + "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk.git?tag=polkadot-v1.12.0)", ] [[package]] @@ -3758,7 +3762,7 @@ dependencies = [ [[package]] name = "ghost-helpers" -version = "0.0.11" +version = "0.0.14" dependencies = [ "frame-support", "ghost-traits", @@ -3840,7 +3844,7 @@ dependencies = [ [[package]] name = "ghost-networks" -version = "0.2.10" +version = "0.2.12" dependencies = [ "frame-benchmarking", "frame-support", @@ -4086,7 +4090,7 @@ dependencies = [ [[package]] name = "ghost-traits" -version = "0.4.6" +version = "0.4.9" dependencies = [ "frame-support", "frost-core", @@ -4110,7 +4114,7 @@ dependencies = [ [[package]] name = "ghost-weaver" -version = "0.0.14" +version = "0.0.16" dependencies = [ "frame-benchmarking", "frame-support", diff --git a/Cargo.toml b/Cargo.toml index b47d70e..7b3696a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -239,6 +239,7 @@ ghost-networks = { path = "pallets/networks", default-features = false } ghost-governance = { path = "pallets/governance", default-features = false } ghost-weaver = { path = "pallets/weaver", default-features = false } ghost-exodus = { path = "pallets/exodus", default-features = false } +ghost-exodus-runtime-api = { path = "pallets/exodus/runtime-api", default-features = false } ghost-sudo = { path = "pallets/sudo", default-features = false } ghost-nomination-pools = { path = "pallets/pools", default-features = false } ghost-nomination-pools-runtime-api = { path = "pallets/pools/runtime-api", default-features = false } diff --git a/pallets/exodus/Cargo.toml b/pallets/exodus/Cargo.toml index 16613d6..cd9efd5 100644 --- a/pallets/exodus/Cargo.toml +++ b/pallets/exodus/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ghost-exodus" -version = "0.1.0" +version = "0.1.1" description = "Threshold signature generation with DKG included" license.workspace = true authors.workspace = true @@ -40,6 +40,7 @@ parking_lot = { workspace = true } ghost-traits = { workspace = true } ghost-helpers = { workspace = true } +ghost-exodus-runtime-api = { workspace = true } pallet-balances = { workspace = true } pallet-session = { workspace = true } @@ -80,6 +81,7 @@ std = [ "frost-core/std", "ghost-traits/std", "ghost-helpers/std", + "ghost-exodus-runtime-api/std", "pallet-balances/std", "pallet-session/std", "pallet-staking/std", diff --git a/pallets/exodus/runtime-api/Cargo.toml b/pallets/exodus/runtime-api/Cargo.toml index f32117c..94a1e60 100644 --- a/pallets/exodus/runtime-api/Cargo.toml +++ b/pallets/exodus/runtime-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ghost-exodus-runtime-api" -version = "0.0.1" +version = "0.0.2" description = "RPC runtime API for ghost EXODUS pallet" license.workspace = true authors.workspace = true @@ -14,7 +14,14 @@ targets = ["x86_64-unknown-linux-gnu"] [dependencies] codec = { workspace = true, features = ["derive"] } sp-api = { workspace = true } +sp-std = { workspace = true } +ghost-helpers = { workspace = true } [features] default = ["std"] -std = ["codec/std", "sp-api/std"] +std = [ + "codec/std", + "sp-api/std", + "sp-std/std", + "ghost-helpers/std", +] diff --git a/pallets/exodus/runtime-api/src/lib.rs b/pallets/exodus/runtime-api/src/lib.rs index 2941fe9..b89d7d2 100644 --- a/pallets/exodus/runtime-api/src/lib.rs +++ b/pallets/exodus/runtime-api/src/lib.rs @@ -1,28 +1,17 @@ #![cfg_attr(not(feature = "std"), no_std)] +use sp_std::vec::Vec; +use ghost_helpers::networks::{NetworkType, NetworkCurve}; + +pub type PreparedEvmMessage = Option<(Vec, Vec, Vec)>; + sp_api::decl_runtime_apis! { - pub trait ExodusApi { - fn get_signed_rotation_message( - session: ExodusSession, - network_id: NetworkId, - dkg_index: DkgIndex - ) -> Vec; - - fn get_signed_bridge_message( - session: ExodusSession, - network_id: NetworkId, - dkg_index: DkgIndex - ) -> Vec; - - fn get_signed_governance_message( - session: ExodusSession, - network_id: NetworkId, - dkg_index: DkgIndex - ) -> Vec; - - fn active_verifying_key(network_curve: NetworkCurve) -> Vec; - fn pending_exodus_request_count(network_curve: NetworkCurve) -> u32; - fn is_qualification_onging(network_curve: NetworkCurve) -> bool; + pub trait ExodusApi { + fn get_evm_rotation_metadata(dkg_index: u64, network_type: NetworkType) -> PreparedEvmMessage; + fn get_evm_bridge_metadata(dkg_index: u64, network_id: u64, session: u64) -> PreparedEvmMessage; + fn get_evm_governance_metadata(dkg_index: u64, network_id: u64, session: u64) -> PreparedEvmMessage; + fn pending_exodus_request_count() -> u32; + fn is_qualification_ongoing(curve: NetworkCurve) -> bool; } } diff --git a/pallets/exodus/src/benchmarking.rs b/pallets/exodus/src/benchmarking.rs index cb8909d..298785d 100644 --- a/pallets/exodus/src/benchmarking.rs +++ b/pallets/exodus/src/benchmarking.rs @@ -539,7 +539,7 @@ mod benchmarks { intervals: [34, 35], booleans: [true, true], }; - let request = ExodusRequest::>::evm_governance( + let request = ExodusRequest::::evm_governance( dummy_address, create_bond, ); @@ -625,7 +625,7 @@ mod benchmarks { intervals: [34, 35], booleans: [true, true], }; - let request = ExodusRequest::>::evm_governance( + let request = ExodusRequest::::evm_governance( dummy_address, create_bond, ); @@ -732,7 +732,7 @@ mod benchmarks { intervals: [34, 35], booleans: [true, true], }; - let request = ExodusRequest::>::evm_governance( + let request = ExodusRequest::::evm_governance( dummy_address, create_bond, ); diff --git a/pallets/exodus/src/lib.rs b/pallets/exodus/src/lib.rs index 0d2208f..0c025ab 100644 --- a/pallets/exodus/src/lib.rs +++ b/pallets/exodus/src/lib.rs @@ -31,6 +31,7 @@ use sp_io::hashing::blake2_256; use ghost_helpers::{ get_byzantium_quorum_threshold, SubstrateBlake2Hasher, + lcg::PerfectSlotMapperU128, hash_chain::cumulative_hash, networks::{NetworkData, NetworkCurve, NetworkType}, bounded_bitmap::{validate_bitmap_sizes, BoundedBitmap}, @@ -227,9 +228,16 @@ type EvmBytes32 = sp_core::H256; type ParticipantsBitmap = BoundedBitmap::MaxAuthoritiesChunks>; type BitmapByAuthority = BoundedBTreeMap, ::MaxAuthorities>; +pub type EvmSessionsBundle = BoundedVec::>; pub type BalanceOf = <::Currency as Inspect<::AccountId>>::Balance; pub type NetworkIdOf = <::NetworkDataHandler as NetworkDataBasicHandler>::NetworkId; +pub type ExodusSessionsMap = BoundedBTreeMap< + NetworkIdOf, + ExodusSession, + <::NetworkDataHandler as NetworkDataBasicHandler>::MaxNetworks, +>; + pub type ValidatorId = <::ValidatorSet as ValidatorSet< ::AccountId, >>::ValidatorId; @@ -243,6 +251,16 @@ pub type IdentificationTuple = ( type ExodusResult = Result; +pub struct MaxEvmRotationSessionsSize(sp_std::marker::PhantomData); +impl Get for MaxEvmRotationSessionsSize { + fn get() -> u32 { + let exodus_session_size = core::mem::size_of::() as u32; + let max_networks = ::MaxNetworks::get(); + + exodus_session_size.saturating_mul(max_networks) + } +} + pub struct MaxAuthoritiesBitmaskSize(sp_std::marker::PhantomData); impl Get for MaxAuthoritiesBitmaskSize { fn get() -> u32 { @@ -415,7 +433,7 @@ pub mod pallet { type MaxAuthorities: Get; #[pallet::constant] - type MaxAuthoritiesChunks: Get + core::fmt::Debug; + type MaxAuthoritiesChunks: Get; #[pallet::constant] type DkgRoundPeriod: Get; @@ -554,15 +572,8 @@ pub mod pallet { #[pallet::storage] #[pallet::getter(fn current_exodus_sessions)] - pub(super) type CurrentExodusSessions = StorageValue< - _, - BoundedBTreeMap< - NetworkIdOf, - ExodusSession, - ::MaxNetworks, - >, - ValueQuery - >; + pub(super) type CurrentExodusSessions = + StorageValue<_, ExodusSessionsMap, ValueQuery>; #[pallet::storage] #[pallet::getter(fn exodus_requests)] @@ -572,7 +583,7 @@ pub mod pallet { NMapKey, NMapKey>>, ), - ExodusRequest>, + ExodusRequest, OptionQuery, >; @@ -646,8 +657,8 @@ pub mod pallet { pub(super) type ExodusSignedRotations = StorageDoubleMap< _, Twox64Concat, DkgIndex, - Twox64Concat, ExodusContext>, - ExodusSignedMessage>, + Twox64Concat, NetworkType, + (ExodusContext>, ExodusSignedMessage), OptionQuery, >; @@ -657,7 +668,7 @@ pub mod pallet { _, Twox64Concat, DkgIndex, Twox64Concat, ExodusContext>, - ExodusSignedMessage>, + ExodusSignedMessage, OptionQuery, >; @@ -667,7 +678,7 @@ pub mod pallet { _, Twox64Concat, DkgIndex, Twox64Concat, ExodusContext>, - ExodusSignedMessage>, + ExodusSignedMessage, OptionQuery, >; @@ -865,18 +876,34 @@ pub mod pallet { SCALAR_MAX_BYTES, ); - for (network_id, network_type) in T::NetworkDataHandler::iter_types_by_curve(&network_curve) { + for (network_type, network_ids) in T::NetworkDataHandler::iter_types_by_curve(&network_curve) { let rotation_message_size = match network_type { NetworkType::Evm => { + let dummy_evm_xor_key = u128::MAX; + let dummy_evm_bitmask = u128::MIN; let dummy_public_address = EvmBytes32::repeat_byte(69u8); + let mut dummy_packed_sessions = + EvmSessionsBundle::::with_max_capacity(); + + let max_size = EvmSessionsBundle::::bound(); + dummy_packed_sessions.bounded_resize(max_size, 69u8); + + let network_id = network_ids + .as_ref() + .first() + .copied() + .expect("At this point we should have at least one `NetworkId` registered; qed"); let dummy_exodus_request = - ExodusRequest::>::evm_rotation( + ExodusRequest::::evm_rotation( dummy_public_address, - 0u8, // parity + dummy_evm_xor_key, + dummy_evm_bitmask, + dummy_packed_sessions, ); - let dummy_exodus_context = ExodusContext::new(network_id, 0); + let dummy_exodus_context = + ExodusContext::>::new(network_id, 0); let mut message_buffer = [0u8; MAX_MESSAGE_SIZE as usize]; dummy_exodus_request @@ -1637,9 +1664,14 @@ pub mod pallet { ensure!( match exodus_request.r#type { - ExodusRequestType::EvmRotation { .. } | ExodusRequestType::UtxoRotation => { + ExodusRequestType::EvmRotation { .. } => { !ExodusSignedRotations::::contains_key( - active_dkg_index, exodus_context, + active_dkg_index, NetworkType::Evm, + ) + }, + ExodusRequestType::UtxoRotation => { + !ExodusSignedRotations::::contains_key( + active_dkg_index, NetworkType::Utxo, ) }, ExodusRequestType::EvmBridgeOut { .. } | ExodusRequestType::UtxoBridgeOut { .. } => { @@ -1789,25 +1821,26 @@ pub mod pallet { ExodusRequests::::remove((&network_curve, exodus_context)); - match exodus_request.r#type { - ExodusRequestType::EvmRotation { .. } | ExodusRequestType::UtxoRotation => { - ExodusSignedRotations::::insert( - active_dkg_index, exodus_context, - exodus_signed_message, - ); - }, - ExodusRequestType::EvmBridgeOut { .. } | ExodusRequestType::UtxoBridgeOut { .. } => { - ExodusSignedBridges::::insert( - active_dkg_index, exodus_context, - exodus_signed_message, - ); - } - ExodusRequestType::EvmGovernance { .. } => { - ExodusSignedGovernance::::insert( - active_dkg_index, exodus_context, - exodus_signed_message, - ); - } + if exodus_signed_message.is_evm_rotation() { + ExodusSignedRotations::::insert( + active_dkg_index, NetworkType::Evm, + (exodus_context, exodus_signed_message), + ); + } else if exodus_signed_message.is_utxo_rotation() { + ExodusSignedRotations::::insert( + active_dkg_index, NetworkType::Utxo, + (exodus_context, exodus_signed_message), + ); + } else if exodus_signed_message.is_bridge_out() { + ExodusSignedBridges::::insert( + active_dkg_index, exodus_context, + exodus_signed_message, + ); + } else if exodus_signed_message.is_evm_governance() { + ExodusSignedGovernance::::insert( + active_dkg_index, exodus_context, + exodus_signed_message, + ); } Ok(()) @@ -2133,7 +2166,7 @@ impl Pallet { fn do_register_evm_exodus( network_id: NetworkIdOf, - exodus_request: ExodusRequest>, + exodus_request: ExodusRequest, ) -> DispatchResult { let network = T::NetworkDataHandler::get(&network_id) .ok_or(Error::::NetworkDoesNotExist)?; @@ -2588,68 +2621,55 @@ impl Pallet { return; } - weight.saturating_accrue(T::DbWeight::get().reads(2)); + weight.saturating_accrue(T::DbWeight::get().reads(1)); let mut all_exodus_sessions = CurrentExodusSessions::::get(); + let mut rotation_requests = BoundedVec::< + (ExodusRequest, ExodusContext>), + ::MaxNetworks, + >::with_max_capacity(); - let max_networks = ::MaxNetworks::get(); - let mut exodus_requests = Vec::with_capacity(max_networks as usize); - let mut too_much_entries = false; + let mut rotation_failed = false; - for (network_id, network_type) in T::NetworkDataHandler::iter_types_by_curve(&network_curve) { - if !matches!(network_type, NetworkType::Evm) { - continue; + weight.saturating_accrue(T::DbWeight::get().reads(1)); + for (network_type, network_ids) in T::NetworkDataHandler::iter_types_by_curve(&network_curve) { + let processed_network_type = matches!(network_type, NetworkType::Evm); + if !processed_network_type { continue; } + + let mut process_iteration = || -> Option<()> { + let element_len = network_curve.element_bytes_len(); + let evm_public_key = EvmBytes32::from_slice( + &verifying_key_slice[1..element_len] + ); + + let (request, context) = Self::prepare_evm_rotation( + evm_public_key, + network_ids.as_ref(), + &mut all_exodus_sessions + )?; + + if !request.r#type.is_evm_rotation() { return None; } + + dkg_qualification.try_push_rotation(network_type, context)?; + rotation_requests.try_push((request, context)).ok()?; + + Some(()) + }; + + if process_iteration().is_none() { + rotation_failed = true; + break; } - - let exodus_session = match all_exodus_sessions.get_mut(&network_id) { - Some(session_ref) => { - let current = *session_ref; - *session_ref = current.saturating_add(1); - current - }, - None => { - if let Err(_) = all_exodus_sessions.try_insert(network_id, 1) { - too_much_entries = true; - } - 0 - } - }; - - if too_much_entries { break; } - - let exodus_request = match network_type { - NetworkType::Evm => { - let parity_byte = verifying_key_slice[0]; - let parity = if parity_byte % 2 == 0 { 0u8 } else { 1u8 }; - - let public_key = EvmBytes32::from_slice(&verifying_key_slice[1..33]); - - ExodusRequest::evm_rotation(public_key, parity) - } - _ => { - unreachable!(); - } - }; - - let exodus_context = ExodusContext::new(network_id, exodus_session); - - if let None = dkg_qualification - .try_push_rotation_context(exodus_context) { - too_much_entries = true; - break; - } - - let key = (network_curve, exodus_context); - exodus_requests.push((key, exodus_request)); } - if too_much_entries { + if rotation_failed { dkg_qualification.nullify_indexes(); } else { - let requests_count = exodus_requests.len() as u64; - weight.saturating_accrue(T::DbWeight::get().writes(requests_count)); - - for exodus in exodus_requests { - ExodusRequests::::insert(exodus.0, exodus.1); + for (exodus_request, exodus_context) in rotation_requests { + weight.saturating_accrue(T::DbWeight::get().writes(1)); + ExodusRequests::::insert( + (network_curve, exodus_context), + exodus_request + ); } weight.saturating_accrue(T::DbWeight::get().writes(1)); @@ -2678,15 +2698,15 @@ impl Pallet { !ActiveVerifyingKey::::contains_key(network_curve); let all_rotations_have_signatures = dkg_qualification - .iter_rotation_contexts() - .fold(true, |accumulator, exodus_context| { + .iter_rotations() + .fold(true, |accumulator, (network_type, exodus_context)| { weight.saturating_accrue(T::DbWeight::get().writes(1)); ExodusRequests::::remove((&network_curve, &exodus_context)); weight.saturating_accrue(T::DbWeight::get().reads(1)); let has_signature = ExodusSignedRotations::::contains_key( dkg_index, - exodus_context, + network_type, ); accumulator && has_signature @@ -2709,6 +2729,93 @@ impl Pallet { }) } + fn prepare_evm_rotation( + evm_public_key: EvmBytes32, + network_ids: &[NetworkIdOf], + evm_exodus_sessions: &mut ExodusSessionsMap, + ) -> Option<(ExodusRequest, ExodusContext>)> { + if network_ids.len() >= 128 { return None; } + + let exodus_session_size = core::mem::size_of::(); + let packed_sessions_size = exodus_session_size * network_ids.len(); + + let evm_xor_key = PerfectSlotMapperU128::get_lcg_xor_mask(network_ids)?; + let (bit_shift, bit_mask) = PerfectSlotMapperU128::get_bits_metadata()?; + + let mut evm_sessions_bitmask: u128 = 0; + let mut maybe_best_exodus_context: Option>> = None; + let mut evm_packed_sessions = EvmSessionsBundle::with_max_capacity(); + evm_packed_sessions.bounded_resize(packed_sessions_size, 0u8); + + let mut evm_slots = [0usize; 128]; + for (i, network_id) in network_ids.iter().enumerate() { + let slot_index = + PerfectSlotMapperU128::get_lcg_based_slot_index( + bit_mask, + bit_shift, + evm_xor_key, + network_id + ); + + if slot_index > 127 { return None; } + + evm_sessions_bitmask |= 1u128 << slot_index; + evm_slots[i] = slot_index; + } + + for (i, &network_id) in network_ids.into_iter().enumerate() { + let exodus_session = match evm_exodus_sessions.get_mut(&network_id) { + Some(session_ref) => { + let current = *session_ref; + *session_ref = current.saturating_add(1); + current + }, + None => { + evm_exodus_sessions.try_insert(network_id, 1).ok()?; + 0 + } + }; + + if maybe_best_exodus_context + .as_ref() + .map_or(true, |c| exodus_session > c.session) { + maybe_best_exodus_context = Some( + ExodusContext::>::new( + network_id, + exodus_session + ) + ); + } + + let lcg_slot_index = evm_slots[i]; + + let lower_bits = (1u128 << lcg_slot_index) - 1; + let slot_index = (evm_sessions_bitmask & lower_bits).count_ones() as usize; + + let offset_start = slot_index * exodus_session_size; + let offset_end = offset_start + exodus_session_size; + + if offset_end > evm_packed_sessions.len() { + return None; + } + + evm_packed_sessions[offset_start..offset_end] + .copy_from_slice(&exodus_session.to_be_bytes()); + } + + let best_exodus_context = maybe_best_exodus_context?; + + let rotation_request = + ExodusRequest::::evm_rotation( + evm_public_key, + evm_xor_key, + evm_sessions_bitmask, + evm_packed_sessions + ); + + Some((rotation_request, best_exodus_context)) + } + fn validate_round0_package_size( dkg_package: & DkgPackage, T>, signature: &::Signature, @@ -3231,7 +3338,7 @@ impl Pallet { fn validate_exodus_nonce_commitment_size_and_get_metadata<'a>( exodus_package: &'a ExodusPackage, BlockNumberFor, T>, signature: &::Signature, - ) -> Result<(ExodusRequest>, RoastSession), InvalidTransaction> { + ) -> Result<(ExodusRequest, RoastSession), InvalidTransaction> { let ExodusPackage::NonceCommitment(package) = exodus_package else { return Err(InvalidTransaction::BadProof); }; @@ -3526,11 +3633,7 @@ impl Pallet { let mut exodus_requests_by_network = BoundedBTreeMap::< NetworkIdOf, - BoundedBTreeMap< - ExodusSession, - ExodusRequest>, - T::MaxExodusRequestCount - >, + BoundedBTreeMap, T::MaxExodusRequestCount>, ::MaxNetworks, >::new(); @@ -5273,6 +5376,75 @@ impl Pallet { } } +impl Pallet { + pub fn api_get_evm_rotation( + dkg_index_u64: u64, + network_type: NetworkType, + ) -> ghost_exodus_runtime_api::PreparedEvmMessage { + let dkg_index: DkgIndex = dkg_index_u64.unique_saturated_into(); + let (context, rotation) = + ExodusSignedRotations::::get(dkg_index, network_type)?; + + let mut message_buffer = [0u8; MAX_MESSAGE_SIZE as usize]; + let message = rotation.get_message(&mut message_buffer, context)?; + + let nonce_bytes = rotation.nonce[1..].to_vec(); + let signature_bytes = rotation.signature.to_vec(); + + Some((message.to_vec(), nonce_bytes, signature_bytes)) + } + + pub fn api_get_evm_bridge( + dkg_index_u64: u64, + network_id_u64: u64, + session_u64: u64, + ) -> ghost_exodus_runtime_api::PreparedEvmMessage { + let dkg_index: DkgIndex = dkg_index_u64.unique_saturated_into(); + let session: ExodusSession = session_u64.unique_saturated_into(); + let network_id: NetworkIdOf = network_id_u64.unique_saturated_into(); + + let context = ExodusContext::>::new(network_id, session); + let bridge = ExodusSignedBridges::::get(dkg_index, context)?; + + let mut message_buffer = [0u8; MAX_MESSAGE_SIZE as usize]; + let message = bridge.get_message(&mut message_buffer, context)?; + + let nonce_bytes = bridge.nonce[1..].to_vec(); + let signature_bytes = bridge.signature.to_vec(); + + Some((message.to_vec(), nonce_bytes, signature_bytes)) + } + + pub fn api_get_evm_governance( + dkg_index_u64: u64, + network_id_u64: u64, + session_u64: u64, + ) -> ghost_exodus_runtime_api::PreparedEvmMessage { + let dkg_index: DkgIndex = dkg_index_u64.unique_saturated_into(); + let session: ExodusSession = session_u64.unique_saturated_into(); + let network_id: NetworkIdOf = network_id_u64.unique_saturated_into(); + + let context = ExodusContext::>::new(network_id, session); + let governance = ExodusSignedGovernance::::get(dkg_index, context)?; + + let mut message_buffer = [0u8; MAX_MESSAGE_SIZE as usize]; + let message = governance.get_message(&mut message_buffer, context)?; + + let nonce_bytes = governance.nonce[1..].to_vec(); + let signature_bytes = governance.signature.to_vec(); + + Some((message.to_vec(), nonce_bytes, signature_bytes)) + } + + pub fn api_pending_exodus_request_count() -> u32 { + ExodusRequests::::count() + } + + pub fn api_is_qualification_ongoing(network_curve: NetworkCurve) -> bool { + QualificationDkgState::::get(network_curve).is_dkg_pending() + } +} + impl sp_runtime::BoundToRuntimeAppPublic for Pallet { type Public = T::AuthorityId; } diff --git a/pallets/exodus/src/tests.rs b/pallets/exodus/src/tests.rs index ff10768..154ffbb 100644 --- a/pallets/exodus/src/tests.rs +++ b/pallets/exodus/src/tests.rs @@ -926,6 +926,16 @@ fn test_all_validators_can_execute_dkg() { let (mut ext, tx_pool_state) = run_initial_dkg_session(&authorities_1, curve); + ext.execute_with(|| { + let network_data = NetworkDataBuilder::default() + .with_network_curve(curve) + .with_network_type(NetworkType::Evm) + .with_outgoing_share(500_000_000) + .build(); + + let _ = ::NetworkDataHandler::register(network_id, network_data).unwrap(); + }); + let mut authorities_2 = authorities_1.clone(); let new_authority = authorities_2.iter().max().unwrap() + 1; authorities_2.push(new_authority); @@ -935,13 +945,6 @@ fn test_all_validators_can_execute_dkg() { run_dkg_session(&mut ext, &tx_pool_state, authorities_1.clone(), authorities_2.clone(), curve); ext.execute_with(|| { - let network_data = NetworkDataBuilder::default() - .with_network_curve(curve) - .with_network_type(NetworkType::Evm) - .with_outgoing_share(500_000_000) - .build(); - - let _ = ::NetworkDataHandler::register(network_id, network_data).unwrap(); let _ = ::NetworkDataHandler::register_incoming(&network_id, 1337).unwrap(); Exodus::do_register_evm_bridge_out_exodus( @@ -1007,6 +1010,14 @@ fn test_all_validators_can_execute_dkg() { Exodus::do_register_evm_governance(network_id, dummy_address, audit).unwrap(); }); + ext.execute_with(|| { + CurrentExodusSessions::::mutate(|sessions| { + if let Some(session) = sessions.get_mut(&420) { + *session = 1337; + } + }); + }); + run_dkg_session(&mut ext, &tx_pool_state, authorities_1, authorities_2.clone(), curve); let rotations_count = ext.execute_with(|| { @@ -1025,7 +1036,7 @@ fn test_all_validators_can_execute_dkg() { ExodusRequests::::count() }); - assert_eq!(rotations_count, 5); + assert_eq!(rotations_count, 3); assert_eq!(bridges_count, 2); assert_eq!(governances_count, 15); assert_eq!(pending_exodus_requests, 0); diff --git a/pallets/exodus/src/types/dkg.rs b/pallets/exodus/src/types/dkg.rs index 69395a3..be20812 100644 --- a/pallets/exodus/src/types/dkg.rs +++ b/pallets/exodus/src/types/dkg.rs @@ -12,16 +12,17 @@ use ghost_traits::{ }; use crate::{ - ELEMENT_MAX_BYTES, Config, ExodusContext, NetworkIdOf, + ELEMENT_MAX_BYTES, Config, NetworkType, NetworkIdOf, ExodusContext, ParticipantsBitmap, BlockNumberFor, DkgIndex, }; -type RotationSessions = BoundedVec< - ExodusContext>, - <::NetworkDataHandler as NetworkDataBasicHandler>::MaxNetworks, +type RotationItem = (NetworkType, ExodusContext>); +type Rotations = BoundedVec< + RotationItem, + <::NetworkDataHandler as NetworkDataBasicHandler>::MaxNetworks, >; -#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] +#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo)] #[scale_info(skip_type_params(T))] pub struct ReadyDkgAuthorities { authorities_indexes: ParticipantsBitmap, @@ -72,12 +73,12 @@ where } } -#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] +#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo)] #[scale_info(skip_type_params(T))] pub struct PendingDkgAuthorities { verifying_key: BoundedVec>, authorities_indexes: ParticipantsBitmap, - rotation_sessions: RotationSessions, + rotations: Rotations, dkg_state: DkgState>, } @@ -86,7 +87,7 @@ impl Default for PendingDkgAuthorities { Self { verifying_key: Default::default(), authorities_indexes: Default::default(), - rotation_sessions: Default::default(), + rotations: Default::default(), dkg_state: Default::default(), } } @@ -101,7 +102,7 @@ where pub fn new_dkg(&mut self, authorities_len: usize) { self.authorities_indexes = ParticipantsBitmap::::empty(authorities_len); self.dkg_state.restart(); - self.rotation_sessions = Default::default(); + self.rotations = Default::default(); } pub fn set_release_block(&mut self, block: BlockNumberFor) { @@ -124,7 +125,7 @@ where self.verifying_key = Default::default(); self.authorities_indexes.nullify(); self.dkg_state.restart(); - self.rotation_sessions = Default::default(); + self.rotations = Default::default(); } pub fn next_phase(&mut self) { @@ -206,11 +207,13 @@ where self.authorities_indexes.insert(i) } - pub fn try_push_rotation_context( + pub fn try_push_rotation( &mut self, - context: ExodusContext>, + network_type: NetworkType, + exodus_context: ExodusContext>, ) -> Option<()> { - self.rotation_sessions.try_push(context).ok() + let item = (network_type, exodus_context); + self.rotations.try_push(item).ok() } pub fn count_ones(&self) -> N @@ -229,10 +232,10 @@ where &self.authorities_indexes } - pub fn iter_rotation_contexts( + pub fn iter_rotations( &self - ) -> impl Iterator>> { - self.rotation_sessions.iter() + ) -> impl Iterator> { + self.rotations.iter() } } diff --git a/pallets/exodus/src/types/exodus_request.rs b/pallets/exodus/src/types/exodus_request.rs index 66c22f2..3b46fba 100644 --- a/pallets/exodus/src/types/exodus_request.rs +++ b/pallets/exodus/src/types/exodus_request.rs @@ -6,8 +6,8 @@ use sp_runtime::{ }; use crate::{ - EvmAddress, EvmBytes32, ExodusContext, RoastSession, - MAX_MESSAGE_SIZE, + EvmAddress, EvmBytes32, ExodusContext, RoastSession, EvmSessionsBundle, + Config, BalanceOf, MAX_MESSAGE_SIZE, }; pub trait EvmAbiEncode { @@ -308,24 +308,31 @@ impl GovernanceAction { } } -#[derive(Encode, Decode, Clone, Copy, PartialEq, TypeInfo, RuntimeDebug)] -pub struct ExodusRequest { - pub r#type: ExodusRequestType, +#[derive(Encode, Decode, Clone, PartialEq, TypeInfo, RuntimeDebug)] +#[scale_info(skip_type_params(T))] +pub struct ExodusRequest { + pub r#type: ExodusRequestType, pub next_roast_session: RoastSession, } -#[derive(Encode, Decode, Clone, Copy, PartialEq, TypeInfo, RuntimeDebug)] -pub enum ExodusRequestType { - EvmRotation { public_key: EvmBytes32, parity: u8 }, +#[derive(Encode, Decode, Clone, PartialEq, TypeInfo, RuntimeDebug)] +#[scale_info(skip_type_params(T))] +pub enum ExodusRequestType { + EvmRotation { + public_key: EvmBytes32, + xor_key: u128, + bitmask: u128, + sessions: EvmSessionsBundle, + }, UtxoRotation, - EvmBridgeOut { amount: Balance, bounty: Perbill, receiver: EvmAddress }, + EvmBridgeOut { amount: BalanceOf, bounty: Perbill, receiver: EvmAddress }, EvmGovernance { target: EvmAddress, action: GovernanceAction }, UtxoBridgeOut, } -impl ExodusRequestType +impl ExodusRequestType where - Balance: UniqueSaturatedInto + Copy, + BalanceOf: UniqueSaturatedInto + Copy, { pub fn get_message<'a, NetworkId>( &self, @@ -336,21 +343,19 @@ where NetworkId: UniqueSaturatedInto + Copy + Default, { match &self { - ExodusRequestType::EvmRotation { public_key, parity } => { + ExodusRequestType::EvmRotation { public_key, xor_key, bitmask, sessions } => { let mut buffer = [0u8; 32]; - buffer[0] = *parity; - let chain_id_u64: u64 = exodus_context.network_id - .unique_saturated_into(); + buffer[..16].copy_from_slice(&xor_key.to_be_bytes()); + buffer[16..].copy_from_slice(&bitmask.to_be_bytes()); + let packed_bits = EvmBytes32::from(&buffer); - buffer[1..9].copy_from_slice(&chain_id_u64.to_be_bytes()); - - let packed_metadata = EvmBytes32::from(buffer); + let payload = sessions.as_slice(); abi_encode_evm_message!( message_buffer, - "rotate(uint256,bytes32,uint256)", - [&exodus_context.session, public_key, &packed_metadata] + "rotate(uint256,bytes32,uint256,bytes)", + [&exodus_context.session, public_key, &packed_bits, payload] ) }, ExodusRequestType::EvmGovernance { target, action } => { @@ -407,11 +412,39 @@ where _ => None } } + + pub fn is_evm_rotation(&self) -> bool { + match &self { + ExodusRequestType::EvmRotation { .. } => true, + _ => false, + } + } + + pub fn is_utxo_rotation(&self) -> bool { + match &self { + ExodusRequestType::UtxoRotation { .. } => true, + _ => false, + } + } + + pub fn is_bridge_out(&self) -> bool { + match &self { + ExodusRequestType::EvmBridgeOut { .. } | ExodusRequestType::UtxoBridgeOut { .. } => true, + _ => false, + } + } + + pub fn is_evm_governance(&self) -> bool { + match &self { + ExodusRequestType::EvmGovernance { .. } => true, + _ => false, + } + } } -impl ExodusRequest +impl ExodusRequest where - Balance: UniqueSaturatedInto + Copy, + BalanceOf: UniqueSaturatedInto + Copy, { pub fn get_message<'a, NetworkId>( &self, @@ -424,15 +457,26 @@ where self.r#type.get_message(message_buffer, exodus_context) } - pub fn evm_rotation(public_key: EvmBytes32, parity: u8) -> Self { + pub fn evm_rotation( + public_key: EvmBytes32, + xor_key: u128, + bitmask: u128, + sessions: EvmSessionsBundle, + ) -> Self { let rotation = ExodusRequestType::EvmRotation { public_key, - parity, + xor_key, + bitmask, + sessions, }; Self::default_request_for(rotation) } - pub fn evm_bridge_out(amount: Balance, bounty: Perbill, receiver: EvmAddress) -> Self { + pub fn evm_bridge_out( + amount: BalanceOf, + bounty: Perbill, + receiver: EvmAddress + ) -> Self { let evm_bridge_out = ExodusRequestType::EvmBridgeOut { amount, bounty, @@ -449,14 +493,7 @@ where Self::default_request_for(evm_governance) } - pub fn is_rotation(&self) -> bool { - match &self.r#type { - ExodusRequestType::EvmRotation { .. } | ExodusRequestType::UtxoRotation { .. } => true, - _ => false, - } - } - - fn default_request_for(request_type: ExodusRequestType) -> Self { + fn default_request_for(request_type: ExodusRequestType) -> Self { Self { r#type: request_type, next_roast_session: Default::default() @@ -467,6 +504,7 @@ where #[cfg(test)] mod tests { use super::*; + use crate::mock::*; use sp_io::hashing::keccak_256; #[test] @@ -477,27 +515,45 @@ mod tests { let network_id = 420; let context = ExodusContext::new(network_id, session); + let dummy_xor_key = u128::MAX; + let dummy_evm_bitmask = 1337; - let rotation_request = ExodusRequest::::evm_rotation( + let mut dummy_packed_sessions = + EvmSessionsBundle::::with_max_capacity(); + + let max_size = EvmSessionsBundle::::bound(); + dummy_packed_sessions.bounded_resize(max_size, 69); + + let rotation_request = ExodusRequest::::evm_rotation( public_key.clone(), - 1, + dummy_xor_key, + dummy_evm_bitmask, + dummy_packed_sessions, ); let calldata = rotation_request.get_message(&mut buffer, context).unwrap(); - assert_eq!(calldata.len(), 100); + // prefix + public_key + packed_bits + pointer + length + bytes array + padding + // 4 + 32 + 32 + 32 + 32 + 32 + (8 * 5) + (32 - 40 % 32) + assert_eq!(calldata.len(), 228); - let expected_hash = keccak_256("rotate(uint256,bytes32,uint256)".as_bytes()); + let expected_hash = keccak_256("rotate(uint256,bytes32,uint256,bytes)".as_bytes()); assert_eq!(calldata[0..4], expected_hash[0..4]); assert_eq!(calldata[35], 0x45); assert_eq!(&calldata[4..35], &[0u8; 31]); assert_eq!(&calldata[36..68], public_key.as_ref()); - assert_eq!(calldata[68], 1); - assert_eq!(&calldata[69..75], &[0u8; 6]); - assert_eq!(calldata[75], 0x01); - assert_eq!(calldata[76], 0xA4); - assert_eq!(&calldata[77..100], &[0u8; 23]); + assert_eq!(&calldata[68..84], &dummy_xor_key.to_be_bytes()); + assert_eq!(&calldata[84..100], &dummy_evm_bitmask.to_be_bytes()); + + assert_eq!(&calldata[100..131], &[0u8; 31]); + assert_eq!(calldata[131], 128); + + assert_eq!(&calldata[132..163], &[0u8; 31]); + assert_eq!(calldata[163], 40); + + assert_eq!(calldata[164], 69); + assert_eq!(&calldata[204..228], &[0u8; 24]); } #[test] @@ -509,8 +565,8 @@ mod tests { let context = ExodusContext::new(network_id, session); - let bridge_request = ExodusRequest::::evm_bridge_out( - 1_000_000u128, + let bridge_request = ExodusRequest::::evm_bridge_out( + 1_000_000u64, sp_runtime::Perbill::from_percent(1), receiver.clone() ); @@ -586,7 +642,7 @@ mod tests { let current_session = session + (index as u64); let context = ExodusContext::new(network_id, current_session); - let governance_request = ExodusRequest::::evm_governance( + let governance_request = ExodusRequest::::evm_governance( target, action, ); diff --git a/pallets/exodus/src/types/signature.rs b/pallets/exodus/src/types/signature.rs index f5e0d07..e440a55 100644 --- a/pallets/exodus/src/types/signature.rs +++ b/pallets/exodus/src/types/signature.rs @@ -6,27 +6,28 @@ use sp_runtime::{ traits::{AtLeast32BitUnsigned, UniqueSaturatedInto}, }; -use crate::ExodusContext; +use crate::{BalanceOf, Config, ExodusContext}; use super::ExodusRequestType; type ExodusNonceT = BoundedVec>; type ExodusSignatureT = BoundedVec>; #[derive(Encode, Decode, Clone, PartialEq, TypeInfo, RuntimeDebug)] -pub struct ExodusSignedMessage { +#[scale_info(skip_type_params(T))] +pub struct ExodusSignedMessage { pub nonce: ExodusNonceT, pub signature: ExodusSignatureT, - pub r#type: ExodusRequestType, + pub r#type: ExodusRequestType, } -impl ExodusSignedMessage +impl ExodusSignedMessage where - Balance: UniqueSaturatedInto + Copy + Default, + BalanceOf: UniqueSaturatedInto + Copy + Default, { pub fn new( nonce_iter: E, signature_iter: S, - r#type: ExodusRequestType, + r#type: ExodusRequestType, ) -> Option where E: Iterator + ExactSizeIterator, @@ -48,4 +49,17 @@ where ) -> Option<&'a [u8]> { self.r#type.get_message(message_buffer, exodus_context) } + + pub fn is_evm_rotation(&self) -> bool { + self.r#type.is_evm_rotation() + } + pub fn is_utxo_rotation(&self) -> bool { + self.r#type.is_utxo_rotation() + } + pub fn is_bridge_out(&self) -> bool { + self.r#type.is_bridge_out() + } + pub fn is_evm_governance(&self) -> bool { + self.r#type.is_evm_governance() + } } diff --git a/pallets/helpers/Cargo.toml b/pallets/helpers/Cargo.toml index 81b39e0..2a497b2 100644 --- a/pallets/helpers/Cargo.toml +++ b/pallets/helpers/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ghost-helpers" -version = "0.0.13" +version = "0.0.14" description = "Cryptographic utility suite for custom runtimes: optimized Bitmaps, UTXO parsing, Merkle Tree proofs, and Hash Chain components." license.workspace = true authors.workspace = true diff --git a/pallets/helpers/src/lcg.rs b/pallets/helpers/src/lcg.rs new file mode 100644 index 0000000..66d6ee0 --- /dev/null +++ b/pallets/helpers/src/lcg.rs @@ -0,0 +1,452 @@ +use sp_runtime::traits::UniqueSaturatedInto; + +macro_rules! generate_perfect_slot_mapper { + ( + struct_name: $struct_name:ident, + inner_type: $inner_type:ty, + init_rng: $init_rng:expr, + golden_ratio: $golden_ratio:expr, + lcg_multiplier: $lcg_multiplier:expr, + lcg_increment: $lcg_increment:expr, + ) => { + pub struct $struct_name; + + impl $struct_name { + const TOTAL_BITS: usize = core::mem::size_of::<$inner_type>() * 8; + + fn get_exact_iterations(items_count: usize) -> Option { + let table_size = Self::TOTAL_BITS; + + if items_count <= 1 { return Some(1); } + if items_count > table_size { return None; } + + let density_pct = (items_count * 100) / table_size; + + match table_size { + 16 => match density_pct { + 0 => Some(1), + 1..=5 => Some(1), // max 0 items + 6..=10 => Some(1), // max 1 items + 11..=15 => Some(2), // max 2 items + 16..=20 => Some(2), // max 3 items + 21..=25 => Some(3), // max 4 items + 26..=30 => Some(3), // max 4 items + 31..=35 => Some(5), // max 5 items + 36..=40 => Some(8), // max 6 items + 41..=45 => Some(13), // max 7 items + 46..=50 => Some(24), // max 8 items + 51..=55 => Some(24), // max 8 items + 56..=60 => Some(49), // max 9 items + 61..=65 => Some(112), // max 10 items + 66..=70 => Some(301), // max 11 items + 71..=75 => Some(966), // max 12 items + 76..=80 => Some(966), // max 12 items + 81..=85 => Some(3868), // max 13 items + 86..=90 => Some(20633), // max 14 items + 91..=95 => Some(165075), // max 15 items + _ => None, + }, + 32 => match density_pct { + 0 => Some(1), + 1..=5 => Some(1), // max 1 items + 6..=10 => Some(2), // max 3 items + 11..=15 => Some(2), // max 4 items + 16..=20 => Some(4), // max 6 items + 21..=25 => Some(7), // max 8 items + 26..=30 => Some(9), // max 9 items + 31..=35 => Some(20), // max 11 items + 36..=40 => Some(31), // max 12 items + 41..=45 => Some(85), // max 14 items + 46..=50 => Some(287), // max 16 items + 51..=55 => Some(575), // max 17 items + 56..=60 => Some(2807), // max 19 items + 61..=65 => Some(6912), // max 20 items + 66..=70 => Some(53627), // max 22 items + 71..=75 => Some(610170), // max 24 items + _ => None, + }, + 64 => match density_pct { + 0 => Some(1), + 1..=5 => Some(1), // max 3 items + 6..=10 => Some(2), // max 6 items + 11..=15 => Some(4), // max 9 items + 16..=20 => Some(8), // max 12 items + 21..=25 => Some(22), // max 16 items + 26..=30 => Some(58), // max 19 items + 31..=35 => Some(180), // max 22 items + 36..=40 => Some(686), // max 25 items + 41..=45 => Some(3285), // max 28 items + 46..=50 => Some(38995), // max 32 items + 51..=55 => Some(343497), // max 35 items + _ => None, + }, + 128 => match density_pct { + 0 => Some(1), + 1..=5 => Some(2), // max 6 items + 6..=10 => Some(4), // max 12 items + 11..=15 => Some(11), // max 19 items + 16..=20 => Some(36), // max 25 items + 21..=25 => Some(207), // max 32 items + 26..=30 => Some(1368), // max 38 items + 31..=35 => Some(13426), // max 44 items + 36..=40 => Some(331105), // max 51 items + _ => None, + }, + _ => None, + } + } + + pub fn get_bits_metadata() -> Option<($inner_type, $inner_type)> { + let table_size = Self::TOTAL_BITS; + let bits_needed = table_size.trailing_zeros() as usize; + + let bit_shift_usize = table_size.checked_sub(bits_needed)?; + let bit_shift = bit_shift_usize as $inner_type; + let bit_mask = (table_size - 1) as $inner_type; + + Some((bit_shift, bit_mask)) + } + + pub fn get_lcg_xor_mask(values: &[V]) -> Option<$inner_type> + where + V: UniqueSaturatedInto<$inner_type> + Copy, + { + let table_size = Self::TOTAL_BITS; + + if values.len() > table_size { return None; } + + let max_iterations = Self::get_exact_iterations(values.len())?; + let (bit_shift, bit_mask) = Self::get_bits_metadata()?; + + let mut lcg_rng_state: $inner_type = $init_rng; + let mut xor_key: $inner_type = lcg_rng_state; + let mut slots_taken = [false; Self::TOTAL_BITS]; + + for _ in 0..max_iterations { + slots_taken.fill(false); + let mut collision_found = false; + + for value in values { + let slot_index = Self::get_lcg_based_slot_index( + bit_mask, + bit_shift, + xor_key, + value + ); + + if slot_index >= table_size || slots_taken[slot_index] { + collision_found = true; + break; + } + + slots_taken[slot_index] = true; + } + + if !collision_found { return Some(xor_key); } + + lcg_rng_state = lcg_rng_state + .wrapping_mul($lcg_multiplier) + .wrapping_add($lcg_increment); + + xor_key = lcg_rng_state; + } + + None + } + + #[inline(always)] + pub fn get_lcg_based_slot_index( + bit_mask: $inner_type, + bit_shift: $inner_type, + xor_key: $inner_type, + value: &V + ) -> usize + where + V: UniqueSaturatedInto<$inner_type> + Copy, + { + let converted_value: $inner_type = (*value).unique_saturated_into(); + let knuth_hash = (converted_value ^ xor_key).wrapping_mul($golden_ratio); + + let shifted_hash = knuth_hash.checked_shr(bit_shift as u32).unwrap_or(knuth_hash); + let slot_index_converted = shifted_hash & bit_mask; + + slot_index_converted as usize + } + } + } +} + +generate_perfect_slot_mapper!( + struct_name: PerfectSlotMapperU16, + inner_type: u16, + + init_rng: 0x1234, + golden_ratio: 0x9E37, // Computed with: 2^16 * ( (sqrt(5) - 1) / 2) + lcg_multiplier: 25173, // Reference: Peter Grogono, Programming in Pascal, Section 4.5. + lcg_increment: 13849, // http://engr.case.edu/merat_francis/eeap282f97/labs/Random_Lab.pdf +); + +generate_perfect_slot_mapper!( + struct_name: PerfectSlotMapperU32, + inner_type: u32, + + init_rng: 0x12345678, + golden_ratio: 0x9E3779B9, // Computd with: 2^32 * ( (sqrt(5) - 1) / 2) + lcg_multiplier: 1664525, // Numerical Recipes `ranqd1`, Chapter 7.1, + lcg_increment: 1013904223, // An Even Quicker Generator, Eq. 7.1.6 +); + +generate_perfect_slot_mapper!( + struct_name: PerfectSlotMapperU64, + inner_type: u64, + + init_rng: 0x123456789ABCDEF, + golden_ratio: 0x9E3779B97F4A7C15, // Taken from `splitmix64`, this generator is specifically used in PostgreSQL + lcg_multiplier: 6364136223846793005, // Newlib / Musl libc 64-bit LCG multiplier + lcg_increment: 1, // https://sourceware.org/git/?p=newlib-cygwin.git;a=blob;f=newlib/libc/stdlib/rand.c +); + +generate_perfect_slot_mapper!( + struct_name: PerfectSlotMapperU128, + inner_type: u128, + + init_rng: 0x123456789ABCDEF0123456789ABCDEF0, + golden_ratio: 0x9E3779B97F4A7C15F39CC0605CEDC834, // Computd with: 2^128 * ( (sqrt(5) - 1) / 2) with python + lcg_multiplier: 0x2360ED051FC65DA44385DF649FCCF645, // 2549297995355413924ULL,4865540595714422341ULL + lcg_increment: 0x5851F42D4C957F2D14057B7EF767814F, // 6364136223846793005ULL,1442695040888963407ULL + // https://github.com/imneme/pcg-cpp/blob/master/include/pcg_random.hpp +); + +#[cfg(test)] +mod perfect_mapper_tests { + use super::*; + + macro_rules! generate_mapper_test { + ($mod_name:ident, $struct_name:ident, $init_rng:expr, $inner_type:ty) => { + mod $mod_name { + use super::*; + + #[test] + fn test_standard_pack_20_percentages() { + const TABLE_SIZE: usize = $struct_name::TOTAL_BITS; + const ITEMS_COUNT: usize = (TABLE_SIZE * 20) / 100; + + let mut values = [0 as $inner_type; TABLE_SIZE]; + for i in 0..ITEMS_COUNT { + values[i] = ((i as $inner_type).wrapping_mul(999)) ^ 0xABC; + } + + let filled_values = &values[0..ITEMS_COUNT]; + let mask = $struct_name::get_lcg_xor_mask(filled_values); + + assert!(mask.is_some()); + let xor_key = mask.unwrap(); + + let bits_needed = TABLE_SIZE.trailing_zeros() as $inner_type; + let bit_mask = (TABLE_SIZE - 1) as $inner_type; + let bit_shift = (TABLE_SIZE as $inner_type) - bits_needed; + + let mut slots = vec![false; TABLE_SIZE]; + + for val in filled_values { + let idx = $struct_name::get_lcg_based_slot_index( + bit_mask, + bit_shift, + xor_key, + val + ); + assert!(idx < TABLE_SIZE); + assert!(!slots[idx]); + slots[idx] = true; + } + } + + #[test] + fn test_heavy_math() { + const TABLE_SIZE: usize = $struct_name::TOTAL_BITS; + + let max_val = <$inner_type>::MAX; + let one_val = 1 as $inner_type; + + let mut block_pattern = 0 as $inner_type; + let mut noise_pattern = 0 as $inner_type; + + let total_bytes = core::mem::size_of::<$inner_type>(); + let mut i = 0; + while i < total_bytes { + let byte_val = if i % 2 == 0 { 0x55 } else { 0xAA }; + noise_pattern = (noise_pattern << 8) | (byte_val as $inner_type); + + let block_val = if i < total_bytes / 3 { + 0xEE + } else if i < (total_bytes * 2) / 3 { + 0xFF + } else { + 0xAA + }; + block_pattern = (block_pattern << 8) | (block_val as $inner_type); + + i += 1; + } + + let center_bit_shift = (TABLE_SIZE / 2) as u32; + let center_bit_pattern = (1 as $inner_type) + .checked_shl(center_bit_shift) + .unwrap_or(1); + + let values: [$inner_type; 6] = [ + $init_rng, + noise_pattern, + max_val, + one_val, + center_bit_pattern, + block_pattern + ]; + + let mask = $struct_name::get_lcg_xor_mask(&values); + assert!(mask.is_some()); + } + + #[test] + fn test_single_element() { + const TABLE_SIZE: usize = $struct_name::TOTAL_BITS; + + let single_value = [420 as $inner_type]; + let mask = $struct_name::get_lcg_xor_mask(&single_value); + assert!(mask.is_some()); + + let xor_key = mask.unwrap(); + + let bits_needed = TABLE_SIZE.trailing_zeros() as $inner_type; + let bit_mask = (TABLE_SIZE - 1) as $inner_type; + let bit_shift = (TABLE_SIZE as $inner_type) - bits_needed; + + let idx = $struct_name::get_lcg_based_slot_index( + bit_mask, + bit_shift, + xor_key, + &single_value[0] + ); + + assert!(idx < TABLE_SIZE); + } + + #[test] + fn test_invalid_bounds_returns_none() { + const TABLE_SIZE: usize = $struct_name::TOTAL_BITS; + let oversized_values = [1 as $inner_type; TABLE_SIZE]; + let mask = $struct_name::get_lcg_xor_mask(&oversized_values); + assert!(mask.is_none()); + } + + #[test] + fn test_top_25_evm_chains_by_tvl() { + const TABLE_SIZE: usize = $struct_name::TOTAL_BITS; + + let evm_network_ids: [u64; 25] = [ + 1, // Ethereum Mainnet + 8453, // Base + 56, // BNB Smart Chain + 42161, // Arbitrum One + 10143, // Monad + 137, // Polygon PoS + 43114, // Avalanche C-Chain + 10, // OP Mainnet + 57073, // Ink + 14, // Flare Network + 196, // X Layer + 25, // Cronos + 5000, // Mantle + 100, // Gnosis Chain + 59144, // Linea + 534352, // Scroll + 324, // zkSync Era + 146, // Sonic (Fantom) + 1116, // Core + 42220, // Celo + 1088, // Metis + 592, // Astar + 1284, // Moonbeam + 98865, // Plume Network + 60808, // BOB (Build on Bitcoin) + ]; + + let mask = $struct_name::get_lcg_xor_mask(&evm_network_ids); + + if let Some(xor_key) = mask { + let mut slots_check = [false; TABLE_SIZE]; + + let bits_needed = TABLE_SIZE.trailing_zeros() as $inner_type; + let bit_mask = (TABLE_SIZE - 1) as $inner_type; + let bit_shift = (TABLE_SIZE as $inner_type) - bits_needed; + + for chain_id in &evm_network_ids { + let slot_idx = $struct_name::get_lcg_based_slot_index( + bit_mask, + bit_shift, + xor_key, + chain_id + ); + + assert!(slot_idx < TABLE_SIZE); + assert!(!slots_check[slot_idx]); + + slots_check[slot_idx] = true; + } + } else { + assert!(TABLE_SIZE <= 32); + } + } + + #[test] + fn test_top_25_evm_testnet_chains() { + const TABLE_SIZE: usize = $struct_name::TOTAL_BITS; + + let evm_network_ids: [u64; 10] = [ + 63, // Mordor (Ethereum Classic Testnet) + 11155111, // Ethereum Sepolia + 80002, // Polygon Amoy + 421614, // Arbitrum Sepolia + 84532, // Base Sepolia + 11155420, // Optimism Sepolia + 59141, // Linea Sepolia + 534351, // Scroll Sepolia + 80084, // Berachain Bartio + 98864, // Plume Testnet + ]; + + let mask = $struct_name::get_lcg_xor_mask(&evm_network_ids); + + if let Some(xor_key) = mask { + let mut slots_check = [false; TABLE_SIZE]; + + let bits_needed = TABLE_SIZE.trailing_zeros() as $inner_type; + let bit_mask = (TABLE_SIZE - 1) as $inner_type; + let bit_shift = (TABLE_SIZE as $inner_type) - bits_needed; + + for chain_id in &evm_network_ids { + let slot_idx = $struct_name::get_lcg_based_slot_index( + bit_mask, + bit_shift, + xor_key, + chain_id + ); + + assert!(slot_idx < TABLE_SIZE); + assert!(!slots_check[slot_idx]); + + slots_check[slot_idx] = true; + } + } else { + assert!(TABLE_SIZE == 16); + } + } + } + } + } + + generate_mapper_test!(u16_mapper, PerfectSlotMapperU16, 0x1234, u16); + generate_mapper_test!(u32_mapper, PerfectSlotMapperU32, 0x12345678, u32); + generate_mapper_test!(u64_mapper, PerfectSlotMapperU64, 0x123456789ABCDEF, u64); + generate_mapper_test!(u128_mapper, PerfectSlotMapperU128, 0x123456789ABCDEF0123456789ABCDEF0, u128); +} diff --git a/pallets/helpers/src/lib.rs b/pallets/helpers/src/lib.rs index c13576b..f1c95aa 100644 --- a/pallets/helpers/src/lib.rs +++ b/pallets/helpers/src/lib.rs @@ -9,6 +9,7 @@ pub mod hash_chain; pub mod merkle_tree; pub mod networks; pub mod utxo; +pub mod lcg; pub type AuthIndexU8 = u8; pub type AuthIndexU16 = u16; diff --git a/pallets/networks/Cargo.toml b/pallets/networks/Cargo.toml index b73f6b9..5e8c63b 100644 --- a/pallets/networks/Cargo.toml +++ b/pallets/networks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ghost-networks" -version = "0.2.11" +version = "0.2.12" description = "Registry and lifecycle management for external network metadata and cryptographic profiles." license.workspace = true authors.workspace = true diff --git a/pallets/networks/src/lib.rs b/pallets/networks/src/lib.rs index 91f79c6..82152b9 100644 --- a/pallets/networks/src/lib.rs +++ b/pallets/networks/src/lib.rs @@ -805,6 +805,8 @@ impl NetworkDataBasicHandler for Pallet { } impl NetworkDataInspectHandler for Pallet { + type TypesItem = BoundedVec; + fn count() -> u32 { NetworkIndexes::::decode_len() .map(|len| len as u32) @@ -850,11 +852,11 @@ impl NetworkDataInspectHandler for Pallet { fn iter_types_by_curve( curve: &Self::NetworkCurve - ) -> impl Iterator { + ) -> impl Iterator { let network_types = NetworkTypes::::get(curve); - network_types.into_iter().flat_map(|(network_type, network_ids)| { - network_ids.into_iter().map(move |network_id| (network_id, network_type)) + network_types.into_iter().map(|(network_type, network_ids)| { + (network_type, network_ids) }) } } diff --git a/pallets/traits/Cargo.toml b/pallets/traits/Cargo.toml index 4f7f9a1..fd24f1b 100644 --- a/pallets/traits/Cargo.toml +++ b/pallets/traits/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ghost-traits" -version = "0.4.8" +version = "0.4.9" description = "Shared traits including `GhostHasher`, `NetworkDataBasicHandler`, `BoundedBTreeMap`, `MerkleTree` and more." license.workspace = true authors.workspace = true diff --git a/pallets/traits/src/networks.rs b/pallets/traits/src/networks.rs index 9ef4f7f..c33b491 100644 --- a/pallets/traits/src/networks.rs +++ b/pallets/traits/src/networks.rs @@ -36,6 +36,8 @@ pub trait NetworkDataBasicHandler { } pub trait NetworkDataInspectHandler: NetworkDataBasicHandler { + type TypesItem: AsRef<[Self::NetworkId]>; + fn count() -> u32; fn contains_key(n: &Self::NetworkId) -> bool; fn curve_exists(c: &Self::NetworkCurve) -> bool; @@ -46,7 +48,7 @@ pub trait NetworkDataInspectHandler: NetworkDataBasicHandler { fn iter_curves() -> impl Iterator; fn iter_types_by_curve( c: &Self::NetworkCurve - ) -> impl Iterator; + ) -> impl Iterator; } pub trait NetworkDataMutateHandler: NetworkDataInspectHandler {