From d045acc4334ba0cfa4e4adef3bc048842d83f7c5 Mon Sep 17 00:00:00 2001 From: Dastan <88332432+dastansam@users.noreply.github.com> Date: Fri, 28 Nov 2025 14:43:31 +0600 Subject: [PATCH 01/27] No pad hasher header (#327) * no circuit padding hasher for block header * *use custom hasher for header that encodes the pre-image in a felt aligned manner. * *bespoke header hasher * *patch bug with hash header fall back * *replace custom poseidon header hasher on generic header with a fork of header that has a custom hasher that overrides default on the header trait. * *rmv commented out impl of prior hash method * Update primitives/header/src/lib.rs Co-authored-by: Dastan <88332432+dastansam@users.noreply.github.com> * fixed tests * Use inherent struct method * Update Cargo.toml --------- Co-authored-by: Ethan Co-authored-by: illuzen --- Cargo.lock | 19 ++ Cargo.toml | 4 + primitives/header/Cargo.toml | 40 +++ primitives/header/src/lib.rs | 346 ++++++++++++++++++++++++++ runtime/Cargo.toml | 3 +- runtime/src/genesis_config_presets.rs | 1 + runtime/src/lib.rs | 5 +- 7 files changed, 414 insertions(+), 4 deletions(-) create mode 100644 primitives/header/Cargo.toml create mode 100644 primitives/header/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index a93e158f..52e610fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8903,6 +8903,24 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "qp-header" +version = "0.1.0" +dependencies = [ + "hex", + "log", + "p3-field", + "p3-goldilocks", + "parity-scale-codec", + "qp-poseidon", + "qp-poseidon-core", + "scale-info", + "serde", + "serde_json", + "sp-core", + "sp-runtime", +] + [[package]] name = "qp-plonky2" version = "1.1.1" @@ -9216,6 +9234,7 @@ dependencies = [ "parity-scale-codec", "primitive-types 0.13.1", "qp-dilithium-crypto", + "qp-header", "qp-poseidon", "qp-scheduler", "qp-wormhole-circuit", diff --git a/Cargo.toml b/Cargo.toml index c1697086..fe5056a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ members = [ "primitives/consensus/pow", "primitives/consensus/qpow", "primitives/dilithium-crypto", + "primitives/header", "primitives/scheduler", "primitives/state-machine", "primitives/trie", @@ -76,6 +77,8 @@ names = { version = "0.14.0", default-features = false } nohash-hasher = { version = "0.2.0" } num-traits = { version = "0.2", default-features = false, features = ["libm"] } once_cell = { version = "1.21.3" } +p3-field = { version = "0.3.0" } +p3-goldilocks = { version = "0.3.0" } parking_lot = { version = "0.12.1", default-features = false } partial_sort = { version = "0.2.0" } paste = { version = "1.0.15", default-features = false } @@ -126,6 +129,7 @@ pallet-reversible-transfers = { path = "./pallets/reversible-transfers", default pallet-scheduler = { path = "./pallets/scheduler", default-features = false } pallet-wormhole = { path = "./pallets/wormhole", default-features = false } qp-dilithium-crypto = { path = "./primitives/dilithium-crypto", version = "0.2.0", default-features = false } +qp-header = { path = "./primitives/header", default-features = false } qp-scheduler = { path = "./primitives/scheduler", default-features = false } qp-wormhole = { path = "./primitives/wormhole", default-features = false } qpow-math = { path = "./qpow-math", default-features = false } diff --git a/primitives/header/Cargo.toml b/primitives/header/Cargo.toml new file mode 100644 index 00000000..a164947d --- /dev/null +++ b/primitives/header/Cargo.toml @@ -0,0 +1,40 @@ +[package] +authors.workspace = true +description = "Fork of sp-runtime's Header type with a custom hash function that's felt aligned for our wormhole circuits" +edition.workspace = true +homepage.workspace = true +license = "Apache-2.0" +name = "qp-header" +publish = false +repository.workspace = true +version = "0.1.0" + +[dependencies] +codec = { features = ["derive"], workspace = true } +log.workspace = true +p3-field = { workspace = true } +p3-goldilocks = { workspace = true } +qp-poseidon = { workspace = true, features = ["serde"] } +qp-poseidon-core = { workspace = true } +scale-info = { features = ["derive", "serde"], workspace = true } +serde = { workspace = true, features = ["derive"], optional = true } +sp-core = { features = ["serde"], workspace = true } +sp-runtime = { features = ["serde"], workspace = true } + +[dev-dependencies] +hex = { workspace = true } +serde_json = { workspace = true, default-features = false, features = [ + "alloc", + "std", +] } + + +[features] +default = ["serde", "std"] +std = [ + "codec/std", + "qp-poseidon/std", + "scale-info/std", + "sp-core/std", + "sp-runtime/std", +] diff --git a/primitives/header/src/lib.rs b/primitives/header/src/lib.rs new file mode 100644 index 00000000..bbe897fe --- /dev/null +++ b/primitives/header/src/lib.rs @@ -0,0 +1,346 @@ +//! Fork of sp-runtime's generic implementation of a block header. +//! We override the hashing function to ensure a felt aligned pre-image for the block hash. + +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::{Codec, Decode, DecodeWithMemTracking, Encode}; +use p3_field::integers::QuotientMap; +use p3_goldilocks::Goldilocks; +use qp_poseidon_core::{ + hash_variable_length, + serialization::{injective_bytes_to_felts, unsafe_digest_bytes_to_felts}, +}; +use scale_info::TypeInfo; +use sp_core::U256; +use sp_runtime::{ + generic::Digest, + traits::{AtLeast32BitUnsigned, BlockNumber, Hash as HashT, MaybeDisplay, Member}, + RuntimeDebug, +}; +extern crate alloc; + +use alloc::vec::Vec; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Custom block header that hashes itself with Poseidon over Goldilocks field elements. +#[derive(Encode, Decode, PartialEq, Eq, Clone, RuntimeDebug, TypeInfo, DecodeWithMemTracking)] +#[scale_info(skip_type_params(Hash))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +pub struct Header + TryFrom, Hash: HashT> { + pub parent_hash: Hash::Output, + #[cfg_attr( + feature = "serde", + serde(serialize_with = "serialize_number", deserialize_with = "deserialize_number") + )] + pub number: Number, + pub state_root: Hash::Output, + pub extrinsics_root: Hash::Output, + pub digest: Digest, +} + +#[cfg(feature = "serde")] +pub fn serialize_number + TryFrom>( + val: &T, + s: S, +) -> Result +where + S: serde::Serializer, +{ + let u256: U256 = (*val).into(); + serde::Serialize::serialize(&u256, s) +} + +#[cfg(feature = "serde")] +pub fn deserialize_number<'a, D, T: Copy + Into + TryFrom>(d: D) -> Result +where + D: serde::Deserializer<'a>, +{ + let u256: U256 = serde::Deserialize::deserialize(d)?; + TryFrom::try_from(u256).map_err(|_| serde::de::Error::custom("Try from failed")) +} + +impl sp_runtime::traits::Header for Header +where + Number: BlockNumber, + Hash: HashT, + Hash::Output: From<[u8; 32]>, +{ + type Number = Number; + type Hash = ::Output; + type Hashing = Hash; + + fn new( + number: Self::Number, + extrinsics_root: Self::Hash, + state_root: Self::Hash, + parent_hash: Self::Hash, + digest: Digest, + ) -> Self { + Self { number, extrinsics_root, state_root, parent_hash, digest } + } + fn number(&self) -> &Self::Number { + &self.number + } + + fn set_number(&mut self, num: Self::Number) { + self.number = num + } + fn extrinsics_root(&self) -> &Self::Hash { + &self.extrinsics_root + } + + fn set_extrinsics_root(&mut self, root: Self::Hash) { + self.extrinsics_root = root + } + fn state_root(&self) -> &Self::Hash { + &self.state_root + } + + fn set_state_root(&mut self, root: Self::Hash) { + self.state_root = root + } + fn parent_hash(&self) -> &Self::Hash { + &self.parent_hash + } + + fn set_parent_hash(&mut self, hash: Self::Hash) { + self.parent_hash = hash + } + + fn digest(&self) -> &Digest { + &self.digest + } + + fn digest_mut(&mut self) -> &mut Digest { + #[cfg(feature = "std")] + log::debug!(target: "header", "Retrieving mutable reference to digest"); + &mut self.digest + } + // We override the default hashing function to use + // a felt aligned pre-image for poseidon hashing. + fn hash(&self) -> Self::Hash { + Header::hash(&self) + } +} + +impl Header +where + Number: Member + + core::hash::Hash + + Copy + + MaybeDisplay + + AtLeast32BitUnsigned + + Codec + + Into + + TryFrom, + Hash: HashT, + Hash::Output: From<[u8; 32]>, +{ + /// Convenience helper for computing the hash of the header without having + /// to import the trait. + pub fn hash(&self) -> Hash::Output { + let max_encoded_felts = 4 * 3 + 1 + 28; // 3 hashout fields + 1 u32 + 28 felts for injective digest encoding + let mut felts = Vec::with_capacity(max_encoded_felts); + + // parent_hash : 32 bytes → 4 felts + felts.extend(unsafe_digest_bytes_to_felts::( + self.parent_hash.as_ref().try_into().expect("hash is 32 bytes"), + )); + + // block number as u64 (compact encoded, but we only need the value) + // constrain the block number to be with u32 range for simplicity + let number = self.number.into(); + felts.push(Goldilocks::from_int(number.as_u32() as u64)); + + // state_root : 32 bytes → 4 felts + felts.extend(unsafe_digest_bytes_to_felts::( + self.state_root.as_ref().try_into().expect("hash is 32 bytes"), + )); + + // extrinsics_root : 32 bytes → 4 felts + felts.extend(unsafe_digest_bytes_to_felts::( + self.extrinsics_root.as_ref().try_into().expect("hash is 32 bytes"), + )); + + // digest – injective encoding + felts.extend(injective_bytes_to_felts::(&self.digest.encode())); + + let poseidon_hash: [u8; 32] = hash_variable_length(felts); + poseidon_hash.into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use qp_poseidon::PoseidonHasher; + use sp_core::H256; + use sp_runtime::{traits::BlakeTwo256, DigestItem}; + + #[test] + fn should_serialize_numbers() { + fn serialize(num: u128) -> String { + let mut v = vec![]; + { + let mut ser = serde_json::Serializer::new(std::io::Cursor::new(&mut v)); + serialize_number(&num, &mut ser).unwrap(); + } + String::from_utf8(v).unwrap() + } + + assert_eq!(serialize(0), "\"0x0\"".to_owned()); + assert_eq!(serialize(1), "\"0x1\"".to_owned()); + assert_eq!(serialize(u64::MAX as u128), "\"0xffffffffffffffff\"".to_owned()); + assert_eq!(serialize(u64::MAX as u128 + 1), "\"0x10000000000000000\"".to_owned()); + } + + #[test] + fn should_deserialize_number() { + fn deserialize(num: &str) -> u128 { + let mut der = serde_json::Deserializer::from_str(num); + deserialize_number(&mut der).unwrap() + } + + assert_eq!(deserialize("\"0x0\""), 0); + assert_eq!(deserialize("\"0x1\""), 1); + assert_eq!(deserialize("\"0xffffffffffffffff\""), u64::MAX as u128); + assert_eq!(deserialize("\"0x10000000000000000\""), u64::MAX as u128 + 1); + } + + #[test] + fn ensure_format_is_unchanged() { + let header = Header:: { + parent_hash: BlakeTwo256::hash(b"1"), + number: 2, + state_root: BlakeTwo256::hash(b"3"), + extrinsics_root: BlakeTwo256::hash(b"4"), + digest: Digest { logs: vec![sp_runtime::generic::DigestItem::Other(b"6".to_vec())] }, + }; + + let header_encoded = header.encode(); + assert_eq!( + header_encoded, + vec![ + 146, 205, 245, 120, 196, 112, 133, 165, 153, 34, 86, 240, 220, 249, 125, 11, 25, + 241, 241, 201, 222, 77, 95, 227, 12, 58, 206, 97, 145, 182, 229, 219, 2, 0, 0, 0, + 88, 19, 72, 51, 123, 15, 62, 20, 134, 32, 23, 61, 170, 165, 249, 77, 0, 216, 129, + 112, 93, 203, 240, 170, 131, 239, 218, 186, 97, 210, 237, 225, 235, 134, 73, 33, + 73, 151, 87, 78, 32, 196, 100, 56, 138, 23, 36, 32, 210, 84, 3, 104, 43, 187, 184, + 12, 73, 104, 49, 200, 204, 31, 143, 13, 4, 0, 4, 54 + ], + ); + assert_eq!(Header::::decode(&mut &header_encoded[..]).unwrap(), header); + + let header = Header:: { + parent_hash: BlakeTwo256::hash(b"1000"), + number: 2000, + state_root: BlakeTwo256::hash(b"3000"), + extrinsics_root: BlakeTwo256::hash(b"4000"), + digest: Digest { logs: vec![sp_runtime::generic::DigestItem::Other(b"5000".to_vec())] }, + }; + + let header_encoded = header.encode(); + assert_eq!( + header_encoded, + vec![ + 197, 243, 254, 225, 31, 117, 21, 218, 179, 213, 92, 6, 247, 164, 230, 25, 47, 166, + 140, 117, 142, 159, 195, 202, 67, 196, 238, 26, 44, 18, 33, 92, 208, 7, 0, 0, 219, + 225, 47, 12, 107, 88, 153, 146, 55, 21, 226, 186, 110, 48, 167, 187, 67, 183, 228, + 232, 118, 136, 30, 254, 11, 87, 48, 112, 7, 97, 31, 82, 146, 110, 96, 87, 152, 68, + 98, 162, 227, 222, 78, 14, 244, 194, 120, 154, 112, 97, 222, 144, 174, 101, 220, + 44, 111, 126, 54, 34, 155, 220, 253, 124, 4, 0, 16, 53, 48, 48, 48 + ], + ); + assert_eq!(Header::::decode(&mut &header_encoded[..]).unwrap(), header); + } + + fn hash_header(x: &[u8]) -> [u8; 32] { + let mut y = x; + if let Ok(header) = Header::::decode(&mut y) { + // Only treat this as a header if we consumed the entire input. + if y.is_empty() { + let max_encoded_felts = 4 * 3 + 1 + 28; // 3 hashout fields + 1 u32 + 28 felts + let mut felts = Vec::with_capacity(max_encoded_felts); + + let parent_hash = header.parent_hash.as_bytes(); + let number = header.number; + let state_root = header.state_root.as_bytes(); + let extrinsics_root = header.extrinsics_root.as_bytes(); + let digest = header.digest.encode(); + + felts.extend(unsafe_digest_bytes_to_felts::( + parent_hash.try_into().expect("Parent hash expected to equal 32 bytes"), + )); + felts.push(Goldilocks::from_int(number as u64)); + felts.extend(unsafe_digest_bytes_to_felts::( + state_root.try_into().expect("State root expected to equal 32 bytes"), + )); + felts.extend(unsafe_digest_bytes_to_felts::( + extrinsics_root.try_into().expect("Extrinsics root expected to equal 32 bytes"), + )); + felts.extend(injective_bytes_to_felts::(&digest)); + + return hash_variable_length(felts); + } + } + // Fallback: canonical bytes hashing for non-header data + PoseidonHasher::hash_padded(x) + } + + #[test] + fn poseidon_header_hash_matches_old_path() { + use codec::Encode; + + // Example header from a real block on devnet + let parent_hash = "839b2d2ac0bf4aa71b18ad1ba5e2880b4ef06452cefacd255cfd76f6ad2c7966"; + let number = 4; + let state_root = "1688817041c572d6c971681465f401f06d0fdcfaed61d28c06d42dc2d07816d5"; + let extrinsics_root = "7c6cace2e91b6314e05410b91224c11f5dd4a4a2dbf0e39081fddbe4ac9ad252"; + let digest = Digest { + logs: vec![ + DigestItem::PreRuntime( + [112, 111, 119, 95], + [ + 233, 182, 183, 107, 158, 1, 115, 19, 219, 126, 253, 86, 30, 208, 176, 70, + 21, 45, 180, 229, 9, 62, 91, 4, 6, 53, 245, 52, 48, 38, 123, 225, + ] + .to_vec(), + ), + DigestItem::Seal( + [112, 111, 119, 95], + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 77, 142, + ] + .to_vec(), + ), + ], + }; + let header = Header:: { + parent_hash: H256::from_slice( + hex::decode(parent_hash).expect("valid hex parent hash").as_slice(), + ), + number, + state_root: H256::from_slice( + hex::decode(state_root).expect("valid hex state root").as_slice(), + ), + extrinsics_root: H256::from_slice( + hex::decode(extrinsics_root).expect("valid hex extrinsics root").as_slice(), + ), + digest, + }; + + let encoded = header.encode(); + + let old = hash_header(&encoded); // old path + let new: [u8; 32] = header.hash().into(); + println!("Old hash: 0x{}", hex::encode(old)); + + assert_eq!(old, new); + } +} diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index ed4ce404..65ba6ce7 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -46,6 +46,7 @@ pallet-utility.workspace = true pallet-vesting.workspace = true primitive-types.workspace = true qp-dilithium-crypto.workspace = true +qp-header = { workspace = true, features = ["serde"] } qp-poseidon = { workspace = true, features = ["serde"] } qp-scheduler.workspace = true qp-wormhole-circuit = { workspace = true, default-features = false } @@ -114,13 +115,13 @@ std = [ "primitive-types/std", "qp-dilithium-crypto/full_crypto", "qp-dilithium-crypto/std", + "qp-header/std", "qp-poseidon/std", "qp-scheduler/std", "qp-wormhole-circuit/std", "qp-wormhole-verifier/std", "qp-zk-circuits-common/std", "scale-info/std", - "scale-info/std", "serde_json/std", "sp-api/std", "sp-block-builder/std", diff --git a/runtime/src/genesis_config_presets.rs b/runtime/src/genesis_config_presets.rs index 16517f28..0fdba5ca 100644 --- a/runtime/src/genesis_config_presets.rs +++ b/runtime/src/genesis_config_presets.rs @@ -74,6 +74,7 @@ pub fn development_config_genesis() -> Value { let ss58_version = sp_core::crypto::Ss58AddressFormat::custom(189); for account in endowed_accounts.iter() { log::info!("🍆 Endowed account: {:?}", account.to_ss58check_with_version(ss58_version)); + log::info!("🍆 Endowed account raw: {:?}", account); } genesis_template(endowed_accounts, crystal_alice().into_account()) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 5d2de3f4..7c54c2b5 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -38,7 +38,6 @@ pub mod governance; use crate::governance::pallet_custom_origins; use qp_poseidon::PoseidonHasher; - /// Opaque types. These are used by the CLI to instantiate machinery that don't need to know /// the specifics of the runtime. They can then be made to be agnostic over specific formats /// of data like extrinsics, allowing for them to continue syncing the network through upgrades @@ -55,7 +54,7 @@ pub mod opaque { // However, some internal checks in dev build expect extrinsics_root to be computed with same // Hash function, so we change the configs/mod.rs Hashing type as well // Opaque block header type. - pub type Header = generic::Header; + pub type Header = qp_header::Header; // Opaque block type. pub type Block = generic::Block; @@ -143,7 +142,7 @@ pub type BlockNumber = u32; pub type Address = MultiAddress; /// Block header type as expected by this runtime. -pub type Header = generic::Header; +pub type Header = qp_header::Header; /// Block type as expected by this runtime. pub type Block = generic::Block; From 0123aebf13c4ebf742b122003c660b598f7bab5a Mon Sep 17 00:00:00 2001 From: Dastan <88332432+dastansam@users.noreply.github.com> Date: Sat, 29 Nov 2025 20:16:12 +0600 Subject: [PATCH 02/27] Verify header in the wormhole proof (#295) --- Cargo.lock | 1823 ++++++++--------- Cargo.toml | 16 +- node/build.rs | 5 +- pallets/balances/src/lib.rs | 15 +- .../balances/src/tests/reentrancy_tests.rs | 6 +- pallets/mining-rewards/src/mock.rs | 8 +- pallets/mining-rewards/src/tests.rs | 8 +- pallets/wormhole/Cargo.toml | 13 + pallets/wormhole/common.bin | Bin 1037 -> 1905 bytes pallets/wormhole/proof_from_bins.hex | 2 +- pallets/wormhole/src/benchmarking.rs | 47 +- pallets/wormhole/src/lib.rs | 45 +- pallets/wormhole/src/mock.rs | 27 +- pallets/wormhole/src/tests.rs | 405 ++-- pallets/wormhole/src/weights.rs | 18 +- pallets/wormhole/verifier.bin | Bin 552 -> 552 bytes runtime/Cargo.toml | 4 + runtime/src/benchmarks.rs | 1 + runtime/src/configs/mod.rs | 11 + runtime/src/lib.rs | 3 + 20 files changed, 1248 insertions(+), 1209 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 52e610fb..6a2353e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,11 +23,11 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ - "gimli 0.31.1", + "gimli 0.32.3", ] [[package]] @@ -79,7 +79,7 @@ checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", "const-random", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -87,9 +87,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -102,9 +102,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy-core" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe6c56d58fbfa9f0f6299376e8ce33091fc6494239466814c3f54b55743cb09" +checksum = "5ca96214615ec8cf3fa2a54b32f486eb49100ca7fe7eb0b8c1137cd316e7250a" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -115,9 +115,9 @@ dependencies = [ [[package]] name = "alloy-dyn-abi" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f56873f3cac7a2c63d8e98a4314b8311aa96adb1a0f82ae923eb2119809d2c" +checksum = "3fdff496dd4e98a81f4861e66f7eaf5f2488971848bb42d9c892f871730245c8" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -131,9 +131,9 @@ dependencies = [ [[package]] name = "alloy-json-abi" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "125a1c373261b252e53e04d6e92c37d881833afc1315fceab53fd46045695640" +checksum = "5513d5e6bd1cba6bdcf5373470f559f320c05c8c59493b6e98912fbe6733943f" dependencies = [ "alloy-primitives", "alloy-sol-type-parser", @@ -143,18 +143,18 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc9485c56de23438127a731a6b4c87803d49faf1a7068dcd1d8768aca3a9edb9" +checksum = "355bf68a433e0fd7f7d33d5a9fc2583fde70bf5c530f63b80845f8da5505cf28" dependencies = [ "alloy-rlp", - "bytes 1.10.1", + "bytes 1.11.0", "cfg-if", "const-hex", "derive_more 2.0.1", - "foldhash 0.1.5", - "hashbrown 0.15.5", - "indexmap 2.11.4", + "foldhash 0.2.0", + "hashbrown 0.16.0", + "indexmap 2.12.0", "itoa", "k256", "keccak-asm", @@ -175,46 +175,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f70d83b765fdc080dbcd4f4db70d8d23fe4761f2f02ebfa9146b833900634b4" dependencies = [ "arrayvec 0.7.6", - "bytes 1.10.1", + "bytes 1.11.0", ] [[package]] name = "alloy-sol-macro" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d20d867dcf42019d4779519a1ceb55eba8d7f3d0e4f0a89bcba82b8f9eb01e48" +checksum = "f3ce480400051b5217f19d6e9a82d9010cdde20f1ae9c00d53591e4a1afbb312" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "alloy-sol-macro-expander" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b74e91b0b553c115d14bd0ed41898309356dc85d0e3d4b9014c4e7715e48c8ad" +checksum = "6d792e205ed3b72f795a8044c52877d2e6b6e9b1d13f431478121d8d4eaa9028" dependencies = [ "alloy-sol-macro-input", "const-hex", "heck 0.5.0", - "indexmap 2.11.4", + "indexmap 2.12.0", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", "syn-solidity", "tiny-keccak", ] [[package]] name = "alloy-sol-macro-input" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84194d31220803f5f62d0a00f583fd3a062b36382e2bea446f1af96727754565" +checksum = "0bd1247a8f90b465ef3f1207627547ec16940c35597875cdc09c49d58b19693c" dependencies = [ "const-hex", "dunce", @@ -222,15 +222,15 @@ dependencies = [ "macro-string", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", "syn-solidity", ] [[package]] name = "alloy-sol-type-parser" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe8c27b3cf6b2bb8361904732f955bc7c05e00be5f469cec7e2280b6167f3ff0" +checksum = "954d1b2533b9b2c7959652df3076954ecb1122a28cc740aa84e7b0a49f6ac0a9" dependencies = [ "serde", "winnow", @@ -238,9 +238,9 @@ dependencies = [ [[package]] name = "alloy-sol-types" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5383d34ea00079e6dd89c652bcbdb764db160cef84e6250926961a0b2295d04" +checksum = "70319350969a3af119da6fb3e9bddb1bce66c9ea933600cb297c8b1850ad2a3c" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -265,9 +265,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.20" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -280,9 +280,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" @@ -295,22 +295,22 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -339,7 +339,16 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", +] + +[[package]] +name = "ar_archive_writer" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a" +dependencies = [ + "object 0.32.2", ] [[package]] @@ -521,7 +530,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -559,7 +568,7 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -644,7 +653,7 @@ checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -723,7 +732,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27d55334c98d756b32dcceb60248647ab34f027690f87f9a362fd292676ee927" dependencies = [ "smallvec", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -778,7 +787,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror 2.0.16", + "thiserror 2.0.17", "time", ] @@ -790,7 +799,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", "synstructure 0.13.2", ] @@ -802,7 +811,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", "synstructure 0.13.2", ] @@ -814,7 +823,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -886,7 +895,7 @@ dependencies = [ "polling", "rustix 1.1.2", "slab", - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -944,7 +953,7 @@ dependencies = [ "rustix 1.1.2", "signal-hook-registry", "slab", - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -966,7 +975,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -983,7 +992,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -992,7 +1001,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4057f2c32adbb2fc158e22fb38433c8e9bbf76b75a4732c7c0cbaf695fb65568" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.0", "futures-sink", "futures-util", "memchr", @@ -1005,7 +1014,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a860072022177f903e59730004fb5dc13db9275b79bb2aef7ba8ce831956c233" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.0", "futures-sink", "futures-util", "memchr", @@ -1043,7 +1052,7 @@ checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -1054,17 +1063,17 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backtrace" -version = "0.3.75" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ - "addr2line 0.24.2", + "addr2line 0.25.1", "cfg-if", "libc", "miniz_oxide", - "object 0.36.7", + "object 0.37.3", "rustc-demangle", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -1079,6 +1088,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base256emoji" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c" +dependencies = [ + "const-str", + "match-lookup", +] + [[package]] name = "base58" version = "0.2.0" @@ -1105,9 +1124,9 @@ checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "binary-merkle-tree" -version = "16.0.0" +version = "16.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "181f5380e435b8ba6d901f8b16fc8908c6f0f8bea8973113d1c8718d89bb1809" +checksum = "95c9f6900c9fd344d53fbdfb36e1343429079d73f4168c8ef48884bf15616dbd" dependencies = [ "hash-db", "log", @@ -1141,7 +1160,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -1227,9 +1246,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bitvec" @@ -1401,9 +1420,9 @@ checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" [[package]] name = "bytemuck" -version = "1.23.2" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" [[package]] name = "byteorder" @@ -1419,9 +1438,9 @@ checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" dependencies = [ "serde", ] @@ -1448,9 +1467,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1de8bc0aa9e9385ceb3bf0c152e3a9b9544f6c4a912c8ae504e80c1f0368603" +checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" dependencies = [ "serde_core", ] @@ -1486,9 +1505,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.38" +version = "1.2.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9" +checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" dependencies = [ "find-msvc-tools", "jobserver", @@ -1522,9 +1541,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -1583,7 +1602,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link 0.2.0", + "windows-link", ] [[package]] @@ -1671,9 +1690,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.48" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" dependencies = [ "clap_builder", "clap_derive", @@ -1681,9 +1700,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.48" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" dependencies = [ "anstream", "anstyle", @@ -1694,21 +1713,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.47" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "clap_lex" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "clatter" @@ -1744,9 +1763,9 @@ dependencies = [ [[package]] name = "codespan-reporting" -version = "0.12.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" dependencies = [ "serde", "termcolor", @@ -1765,7 +1784,7 @@ version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.0", "memchr", ] @@ -1809,9 +1828,9 @@ dependencies = [ [[package]] name = "const-hex" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6407bff74dea37e0fa3dc1c1c974e5d46405f0c987bf9997a0762adce71eda6" +checksum = "3bb320cac8a0750d7f25280aa97b09c26edfe161164238ecbbb31092b079e735" dependencies = [ "cfg-if", "cpufeatures", @@ -1845,11 +1864,17 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "const-str" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" + [[package]] name = "const_format" -version = "0.2.34" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" dependencies = [ "const_format_proc_macros", ] @@ -2067,7 +2092,7 @@ dependencies = [ "serde_derive", "serde_json", "tinytemplate", - "tokio 1.47.1", + "tokio 1.48.0", "walkdir", ] @@ -2150,9 +2175,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array 0.14.7", "rand_core 0.6.4", @@ -2227,11 +2252,12 @@ dependencies = [ [[package]] name = "cumulus-pallet-parachain-system" -version = "0.21.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fa7c354e70d62b5bb6014cbad499b831e27629b8a2af2f86497cae7f74d309d" +checksum = "ac1d9d194bc0faaef14a95ed6e78d4afccd117e67ff03c0ed712298e1124921a" dependencies = [ - "bytes 1.10.1", + "array-bytes 6.2.3", + "bytes 1.11.0", "cumulus-pallet-parachain-system-proc-macro", "cumulus-primitives-core", "cumulus-primitives-parachain-inherent", @@ -2272,7 +2298,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2378,14 +2404,14 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "cxx" -version = "1.0.185" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f81de88da10862f22b5b3a60f18f6f42bbe7cb8faa24845dd7b1e4e22190e77" +checksum = "47ac4eaf7ebe29e92f1b091ceefec7710a53a6f6154b2460afda626c113b65b9" dependencies = [ "cc", "cxx-build", @@ -2398,50 +2424,49 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.185" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5edd58bf75c3fdfc80d79806403af626570662f7b6cc782a7fabe156166bd6d6" +checksum = "2abd4c3021eefbac5149f994c117b426852bca3a0aad227698527bca6d4ea657" dependencies = [ "cc", "codespan-reporting", - "indexmap 2.11.4", + "indexmap 2.12.0", "proc-macro2", "quote", "scratch", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "cxxbridge-cmd" -version = "1.0.185" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd46bf2b541a4e0c2d5abba76607379ee05d68e714868e3cb406dc8d591ce2d2" +checksum = "6f12fbc5888b2311f23e52a601e11ad7790d8f0dbb903ec26e2513bf5373ed70" dependencies = [ "clap", "codespan-reporting", - "indexmap 2.11.4", + "indexmap 2.12.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "cxxbridge-flags" -version = "1.0.185" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c79b68f6a3a8f809d39b38ae8af61305a6113819b19b262643b9c21353b92d9" +checksum = "83d3dd7870af06e283f3f8ce0418019c96171c9ce122cfb9c8879de3d84388fd" [[package]] name = "cxxbridge-macro" -version = "1.0.185" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862b7fdb048ff9ef0779a0d0a03affd09746c4c875543746b640756be9cff2af" +checksum = "a26f0d82da663316786791c3d0e9f9edc7d1ee1f04bdad3d2643086a69d6256c" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.0", "proc-macro2", "quote", - "rustversion", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2475,7 +2500,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2489,7 +2514,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2500,7 +2525,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2511,7 +2536,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2524,7 +2549,7 @@ dependencies = [ "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core 0.9.11", + "parking_lot_core 0.9.12", ] [[package]] @@ -2550,7 +2575,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" dependencies = [ "data-encoding", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2594,9 +2619,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.3" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", ] @@ -2620,7 +2645,7 @@ checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2631,7 +2656,7 @@ checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2642,7 +2667,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2655,7 +2680,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version 0.4.1", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2684,7 +2709,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2695,7 +2720,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", "unicode-xid", ] @@ -2794,7 +2819,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2818,7 +2843,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.106", + "syn 2.0.110", "termcolor", "toml 0.8.23", "walkdir", @@ -2866,7 +2891,7 @@ checksum = "7e8671d54058979a37a26f3511fbf8d198ba1aa35ffb202c42587d918d77213a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2940,7 +2965,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2993,34 +3018,34 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "enum-ordinalize" -version = "4.3.0" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" dependencies = [ "enum-ordinalize-derive", ] [[package]] name = "enum-ordinalize-derive" -version = "4.3.1" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "env_filter" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" dependencies = [ "log", "regex", @@ -3071,7 +3096,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -3162,7 +3187,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -3191,7 +3216,7 @@ checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" dependencies = [ "arrayvec 0.7.6", "auto_impl", - "bytes 1.10.1", + "bytes 1.11.0", ] [[package]] @@ -3202,7 +3227,7 @@ checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" dependencies = [ "arrayvec 0.7.6", "auto_impl", - "bytes 1.10.1", + "bytes 1.11.0", ] [[package]] @@ -3222,11 +3247,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb42427514b063d97ce21d5199f36c0c307d981434a6be32582bc79fe5bd2303" dependencies = [ "expander", - "indexmap 2.11.4", + "indexmap 2.12.0", "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -3299,15 +3324,15 @@ dependencies = [ "log", "num-traits", "parity-scale-codec", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "scale-info", ] [[package]] name = "find-msvc-tools" -version = "0.1.2" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "fixed-hash" @@ -3336,6 +3361,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "fnv" version = "1.0.7" @@ -3485,7 +3516,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7cb8796f93fa038f979a014234d632e9688a120e745f936e2635123c77537f7" dependencies = [ - "frame-metadata 20.0.0", + "frame-metadata 21.0.0", "parity-scale-codec", "scale-decode", "scale-info", @@ -3502,7 +3533,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -3525,9 +3556,9 @@ dependencies = [ [[package]] name = "frame-executive" -version = "41.0.1" +version = "41.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7e5477db02bf54b4413611f55ec59673fa7b071eef08b7097e739f999a215cd" +checksum = "11e495475817addd877fa08bc17fc92d7308f4d5e99be5488433ccf19d222122" dependencies = [ "aquamarine", "frame-support", @@ -3554,6 +3585,17 @@ dependencies = [ "serde", ] +[[package]] +name = "frame-metadata" +version = "21.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20dfd1d7eae1d94e32e869e2fb272d81f52dd8db57820a373adb83ea24d7d862" +dependencies = [ + "cfg-if", + "parity-scale-codec", + "scale-info", +] + [[package]] name = "frame-metadata" version = "23.0.0" @@ -3658,7 +3700,7 @@ dependencies = [ "proc-macro2", "quote", "sp-crypto-hashing", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -3671,7 +3713,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -3682,14 +3724,14 @@ checksum = "ed971c6435503a099bdac99fe4c5bea08981709e5b5a0a8535a1856f48561191" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "frame-system" -version = "41.0.0" +version = "41.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce7df989cefbaab681101774748a1bbbcd23d0cc66e392f8f22d3d3159914db" +checksum = "4189198074d7964bd6cb9d1cf69d77d5d224026bce95bb0ca7efc7768bb8e29c" dependencies = [ "cfg-if", "docify", @@ -3854,7 +3896,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -3914,20 +3956,6 @@ dependencies = [ "byteorder", ] -[[package]] -name = "generator" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "605183a538e3e2a9c1038635cc5c2d194e2ee8fd0d1b66b8349fad7dbacce5a2" -dependencies = [ - "cc", - "cfg-if", - "libc", - "log", - "rustversion", - "windows 0.61.3", -] - [[package]] name = "generic-array" version = "0.12.4" @@ -3967,21 +3995,21 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", - "wasi 0.14.7+wasi-0.2.4", + "wasip2", "wasm-bindgen", ] @@ -4026,6 +4054,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "glob" version = "0.3.3" @@ -4044,7 +4078,7 @@ dependencies = [ "futures-timer", "no-std-compat", "nonzero_ext", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "portable-atomic", "quanta", "rand 0.8.5", @@ -4069,15 +4103,15 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.0", "fnv", "futures-core", "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.11.4", + "indexmap 2.12.0", "slab", - "tokio 1.47.1", + "tokio 1.48.0", "tokio-util", "tracing", ] @@ -4089,26 +4123,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", - "bytes 1.10.1", + "bytes 1.11.0", "fnv", "futures-core", "futures-sink", "http 1.3.1", - "indexmap 2.11.4", + "indexmap 2.12.0", "slab", - "tokio 1.47.1", + "tokio 1.48.0", "tokio-util", "tracing", ] [[package]] name = "half" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", + "zerocopy", ] [[package]] @@ -4176,6 +4211,15 @@ dependencies = [ "allocator-api2", "equivalent", "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +dependencies = [ + "foldhash 0.2.0", "serde", ] @@ -4259,7 +4303,7 @@ dependencies = [ "socket2 0.5.10", "thiserror 1.0.69", "tinyvec", - "tokio 1.47.1", + "tokio 1.48.0", "tracing", "url", ] @@ -4282,9 +4326,9 @@ dependencies = [ "once_cell", "rand 0.9.2", "ring 0.17.14", - "thiserror 2.0.16", + "thiserror 2.0.17", "tinyvec", - "tokio 1.47.1", + "tokio 1.48.0", "tracing", "url", ] @@ -4301,12 +4345,12 @@ dependencies = [ "ipconfig", "lru-cache", "once_cell", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "rand 0.8.5", "resolv-conf", "smallvec", "thiserror 1.0.69", - "tokio 1.47.1", + "tokio 1.48.0", "tracing", ] @@ -4322,12 +4366,12 @@ dependencies = [ "ipconfig", "moka", "once_cell", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "rand 0.9.2", "resolv-conf", "smallvec", - "thiserror 2.0.16", - "tokio 1.47.1", + "thiserror 2.0.17", + "tokio 1.48.0", "tracing", ] @@ -4376,7 +4420,7 @@ version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.0", "fnv", "itoa", ] @@ -4387,7 +4431,7 @@ version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.0", "fnv", "itoa", ] @@ -4398,7 +4442,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.0", "http 0.2.12", "pin-project-lite 0.2.16", ] @@ -4409,7 +4453,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.0", "http 1.3.1", ] @@ -4419,7 +4463,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.0", "futures-core", "http 1.3.1", "http-body 1.0.1", @@ -4469,7 +4513,7 @@ version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.0", "futures-channel", "futures-core", "futures-util", @@ -4481,7 +4525,7 @@ dependencies = [ "itoa", "pin-project-lite 0.2.16", "socket2 0.5.10", - "tokio 1.47.1", + "tokio 1.48.0", "tower-service", "tracing", "want", @@ -4489,12 +4533,12 @@ dependencies = [ [[package]] name = "hyper" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", - "bytes 1.10.1", + "bytes 1.11.0", "futures-channel", "futures-core", "h2 0.4.12", @@ -4506,7 +4550,7 @@ dependencies = [ "pin-project-lite 0.2.16", "pin-utils", "smallvec", - "tokio 1.47.1", + "tokio 1.48.0", "want", ] @@ -4517,34 +4561,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http 1.3.1", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-util", "log", "rustls", "rustls-native-certs", "rustls-pki-types", - "tokio 1.47.1", + "tokio 1.48.0", "tokio-rustls", "tower-service", ] [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.0", "futures-channel", "futures-core", "futures-util", "http 1.3.1", "http-body 1.0.1", - "hyper 1.7.0", + "hyper 1.8.1", "libc", "pin-project-lite 0.2.16", - "socket2 0.6.0", - "tokio 1.47.1", + "socket2 0.6.1", + "tokio 1.48.0", "tower-service", "tracing", ] @@ -4561,7 +4605,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.0", + "windows-core 0.62.2", ] [[package]] @@ -4575,9 +4619,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -4588,9 +4632,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -4601,11 +4645,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -4616,42 +4659,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -4715,7 +4754,7 @@ dependencies = [ "netlink-sys", "rtnetlink", "system-configuration 0.6.1", - "tokio 1.47.1", + "tokio 1.48.0", "windows 0.53.0", ] @@ -4727,13 +4766,13 @@ checksum = "064d90fec10d541084e7b39ead8875a5a80d9114a2b18791565253bae25f49e4" dependencies = [ "async-trait", "attohttpc", - "bytes 1.10.1", + "bytes 1.11.0", "futures 0.3.31", "http 0.2.12", "hyper 0.14.32", "log", "rand 0.8.5", - "tokio 1.47.1", + "tokio 1.48.0", "url", "xmltree", ] @@ -4793,7 +4832,7 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -4828,12 +4867,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.4" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.16.0", "serde", "serde_core", ] @@ -4882,17 +4921,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", -] - [[package]] name = "ip_network" version = "0.4.1" @@ -4919,20 +4947,20 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "is-terminal" -version = "0.4.16" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi 0.5.2", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -4987,26 +5015,26 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", - "serde", + "serde_core", ] [[package]] name = "jiff-static" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -5037,15 +5065,15 @@ version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" -version = "0.3.80" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", @@ -5053,9 +5081,9 @@ dependencies = [ [[package]] name = "jsonrpsee" -version = "0.24.9" +version = "0.24.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b26c20e2178756451cfeb0661fb74c47dd5988cb7e3939de7e9241fd604d42" +checksum = "e281ae70cc3b98dac15fced3366a880949e65fc66e345ce857a5682d152f3e62" dependencies = [ "jsonrpsee-client-transport", "jsonrpsee-core", @@ -5063,15 +5091,15 @@ dependencies = [ "jsonrpsee-server", "jsonrpsee-types", "jsonrpsee-ws-client", - "tokio 1.47.1", + "tokio 1.48.0", "tracing", ] [[package]] name = "jsonrpsee-client-transport" -version = "0.24.9" +version = "0.24.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bacb85abf4117092455e1573625e21b8f8ef4dec8aff13361140b2dc266cdff2" +checksum = "cc4280b709ac3bb5e16cf3bad5056a0ec8df55fa89edfe996361219aadc2c7ea" dependencies = [ "base64 0.22.1", "futures-util", @@ -5083,7 +5111,7 @@ dependencies = [ "rustls-platform-verifier", "soketto", "thiserror 1.0.69", - "tokio 1.47.1", + "tokio 1.48.0", "tokio-rustls", "tokio-util", "tracing", @@ -5092,54 +5120,54 @@ dependencies = [ [[package]] name = "jsonrpsee-core" -version = "0.24.9" +version = "0.24.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456196007ca3a14db478346f58c7238028d55ee15c1df15115596e411ff27925" +checksum = "348ee569eaed52926b5e740aae20863762b16596476e943c9e415a6479021622" dependencies = [ "async-trait", - "bytes 1.10.1", + "bytes 1.11.0", "futures-timer", "futures-util", "http 1.3.1", "http-body 1.0.1", "http-body-util", "jsonrpsee-types", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pin-project", "rand 0.8.5", "rustc-hash 2.1.1", "serde", "serde_json", "thiserror 1.0.69", - "tokio 1.47.1", + "tokio 1.48.0", "tokio-stream", "tracing", ] [[package]] name = "jsonrpsee-proc-macros" -version = "0.24.9" +version = "0.24.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e65763c942dfc9358146571911b0cd1c361c2d63e2d2305622d40d36376ca80" +checksum = "7398cddf5013cca4702862a2692b66c48a3bd6cf6ec681a47453c93d63cf8de5" dependencies = [ "heck 0.5.0", "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "jsonrpsee-server" -version = "0.24.9" +version = "0.24.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55e363146da18e50ad2b51a0a7925fc423137a0b1371af8235b1c231a0647328" +checksum = "21429bcdda37dcf2d43b68621b994adede0e28061f816b038b0f18c70c143d51" dependencies = [ "futures-util", "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-util", "jsonrpsee-core", "jsonrpsee-types", @@ -5149,7 +5177,7 @@ dependencies = [ "serde_json", "soketto", "thiserror 1.0.69", - "tokio 1.47.1", + "tokio 1.48.0", "tokio-stream", "tokio-util", "tower", @@ -5158,9 +5186,9 @@ dependencies = [ [[package]] name = "jsonrpsee-types" -version = "0.24.9" +version = "0.24.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a8e70baf945b6b5752fc8eb38c918a48f1234daf11355e07106d963f860089" +checksum = "b0f05e0028e55b15dbd2107163b3c744cd3bb4474f193f95d9708acbf5677e44" dependencies = [ "http 1.3.1", "serde", @@ -5170,9 +5198,9 @@ dependencies = [ [[package]] name = "jsonrpsee-ws-client" -version = "0.24.9" +version = "0.24.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01b3323d890aa384f12148e8d2a1fd18eb66e9e7e825f9de4fa53bcc19b93eef" +checksum = "78fc744f17e7926d57f478cf9ca6e1ee5d8332bf0514860b1a3cdf1742e614cc" dependencies = [ "http 1.3.1", "jsonrpsee-client-transport", @@ -5278,7 +5306,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf7a85fe66f9ff9cd74e169fdd2c94c6e1e74c412c99a73b4df3200b5d3760b2" dependencies = [ "kvdb", - "parking_lot 0.12.4", + "parking_lot 0.12.5", ] [[package]] @@ -5289,7 +5317,7 @@ checksum = "b644c70b92285f66bfc2032922a79000ea30af7bc2ab31902992a5dcb9b434f6" dependencies = [ "kvdb", "num_cpus", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "regex", "rocksdb", "smallvec", @@ -5312,9 +5340,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.175" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libloading" @@ -5323,7 +5351,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-link 0.2.0", + "windows-link", ] [[package]] @@ -5338,7 +5366,7 @@ version = "0.54.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbe80f9c7e00526cd6b838075b9c171919404a4732cb2fa8ece0a093223bfc4" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.0", "either", "futures 0.3.31", "futures-timer", @@ -5406,7 +5434,7 @@ dependencies = [ "multihash 0.19.3", "multistream-select", "once_cell", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pin-project", "quick-protobuf", "rand 0.8.5", @@ -5430,7 +5458,7 @@ dependencies = [ "hickory-resolver 0.24.4", "libp2p-core", "libp2p-identity", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "smallvec", "tracing", ] @@ -5484,7 +5512,7 @@ checksum = "ced237d0bd84bbebb7c2cad4c073160dacb4fe40534963c32ed6d4c6bb7702a3" dependencies = [ "arrayvec 0.7.6", "asynchronous-codec 0.7.0", - "bytes 1.10.1", + "bytes 1.11.0", "either", "fnv", "futures 0.3.31", @@ -5521,7 +5549,7 @@ dependencies = [ "rand 0.8.5", "smallvec", "socket2 0.5.10", - "tokio 1.47.1", + "tokio 1.48.0", "tracing", "void", ] @@ -5550,7 +5578,7 @@ version = "0.45.10" source = "git+https://github.com/Quantus-Network/qp-libp2p-noise?tag=v0.45.10#901f09f30b32f910395270bba3a566191dc2f61f" dependencies = [ "asynchronous-codec 0.6.2", - "bytes 1.10.1", + "bytes 1.11.0", "clatter", "futures 0.3.31", "libp2p-core", @@ -5591,21 +5619,21 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46352ac5cd040c70e88e7ff8257a2ae2f891a4076abad2c439584a31c15fd24e" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.0", "futures 0.3.31", "futures-timer", "if-watch", "libp2p-core", "libp2p-identity", "libp2p-tls", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "quinn", "rand 0.8.5", "ring 0.17.14", "rustls", "socket2 0.5.10", "thiserror 1.0.69", - "tokio 1.47.1", + "tokio 1.48.0", "tracing", ] @@ -5647,7 +5675,7 @@ dependencies = [ "once_cell", "rand 0.8.5", "smallvec", - "tokio 1.47.1", + "tokio 1.48.0", "tracing", "void", "web-time", @@ -5662,7 +5690,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -5678,7 +5706,7 @@ dependencies = [ "libp2p-core", "libp2p-identity", "socket2 0.5.10", - "tokio 1.47.1", + "tokio 1.48.0", "tracing", ] @@ -5712,7 +5740,7 @@ dependencies = [ "igd-next", "libp2p-core", "libp2p-swarm", - "tokio 1.47.1", + "tokio 1.48.0", "tracing", "void", ] @@ -5728,7 +5756,7 @@ dependencies = [ "futures-rustls", "libp2p-core", "libp2p-identity", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pin-project-lite 0.2.16", "rw-stream-sink", "soketto", @@ -5750,7 +5778,7 @@ dependencies = [ "thiserror 1.0.69", "tracing", "yamux 0.12.1", - "yamux 0.13.6", + "yamux 0.13.8", ] [[package]] @@ -5759,9 +5787,9 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "libc", - "redox_syscall 0.5.17", + "redox_syscall 0.5.18", ] [[package]] @@ -5829,9 +5857,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.22" +version = "1.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" +checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" dependencies = [ "cc", "pkg-config", @@ -5855,9 +5883,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linked_hash_set" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bae85b5be22d9843c80e5fc80e9b64c8a3b1f98f867c709956eca3efff4e92e2" +checksum = "984fb35d06508d1e69fc91050cceba9c0b748f983e6739fa2c7a9237154c52c8" dependencies = [ "linked-hash-map", ] @@ -5897,9 +5925,9 @@ dependencies = [ [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "litep2p" @@ -5909,19 +5937,19 @@ checksum = "14fb10e63363204b89d91e1292df83322fd9de5d7fa76c3d5c78ddc2f8f3efa9" dependencies = [ "async-trait", "bs58", - "bytes 1.10.1", + "bytes 1.11.0", "cid 0.11.1", "ed25519-dalek", "futures 0.3.31", "futures-timer", "hickory-resolver 0.25.2", - "indexmap 2.11.4", + "indexmap 2.12.0", "libc", "mockall", "multiaddr 0.17.1", "multihash 0.17.0", "network-interface", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pin-project", "prost 0.13.5", "prost-build", @@ -5932,8 +5960,8 @@ dependencies = [ "smallvec", "snow", "socket2 0.5.10", - "thiserror 2.0.16", - "tokio 1.47.1", + "thiserror 2.0.17", + "tokio 1.48.0", "tokio-stream", "tokio-tungstenite", "tokio-util", @@ -5943,18 +5971,17 @@ dependencies = [ "url", "x25519-dalek", "x509-parser 0.17.0", - "yamux 0.13.6", + "yamux 0.13.8", "yasna", "zeroize", ] [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] @@ -5964,19 +5991,6 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" -[[package]] -name = "loom" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" -dependencies = [ - "cfg-if", - "generator", - "scoped-tls", - "tracing", - "tracing-subscriber", -] - [[package]] name = "lru" version = "0.12.5" @@ -6037,7 +6051,7 @@ checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -6049,7 +6063,7 @@ dependencies = [ "macro_magic_core", "macro_magic_macros", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -6063,7 +6077,7 @@ dependencies = [ "macro_magic_core_macros", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -6074,7 +6088,7 @@ checksum = "b02abfe41815b5bd98dbd4260173db2c116dda171dc0fe7838cb206333b83308" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -6085,16 +6099,27 @@ checksum = "73ea28ee64b88876bf45277ed9a5817c1817df061a74f2b988971a12570e5869" dependencies = [ "macro_magic_core", "quote", - "syn 2.0.106", + "syn 2.0.110", +] + +[[package]] +name = "match-lookup" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1265724d8cb29dbbc2b0f06fffb8bf1a8c0cf73a78eede9ba73a4a66c52a981e" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -6109,9 +6134,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memfd" @@ -6133,9 +6158,9 @@ dependencies = [ [[package]] name = "memmap2" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" dependencies = [ "libc", ] @@ -6209,13 +6234,13 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] @@ -6234,7 +6259,7 @@ dependencies = [ "hashlink", "lioness", "log", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "rand 0.8.5", "rand_chacha 0.3.1", "rand_distr", @@ -6279,25 +6304,24 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "moka" -version = "0.12.10" +version = "0.12.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926" +checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" dependencies = [ "crossbeam-channel", "crossbeam-epoch", "crossbeam-utils", - "loom", - "parking_lot 0.12.4", + "equivalent", + "parking_lot 0.12.5", "portable-atomic", "rustc_version 0.4.1", "smallvec", "tagptr", - "thiserror 1.0.69", "uuid", ] @@ -6347,11 +6371,12 @@ dependencies = [ [[package]] name = "multibase" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b3539ec3c1f04ac9748a260728e855f261b4977f5c3406612c884564f329404" +checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77" dependencies = [ "base-x", + "base256emoji", "data-encoding", "data-encoding-macro", ] @@ -6409,7 +6434,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea0df8e5eec2298a62b326ee4f0d7fe1a6b90a09dfcf9df37b38f947a8c42f19" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.0", "futures 0.3.31", "log", "pin-project", @@ -6503,12 +6528,12 @@ version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.0", "futures 0.3.31", "log", "netlink-packet-core", "netlink-sys", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -6517,11 +6542,11 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.0", "futures 0.3.31", "libc", "log", - "tokio 1.47.1", + "tokio 1.48.0", ] [[package]] @@ -6532,7 +6557,7 @@ checksum = "07709a6d4eba90ab10ec170a0530b3aafc81cb8a2d380e4423ae41fc55fe5745" dependencies = [ "cc", "libc", - "thiserror 2.0.16", + "thiserror 2.0.17", "winapi", ] @@ -6598,12 +6623,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "overload", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -6655,7 +6679,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -6731,6 +6755,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + [[package]] name = "object" version = "0.36.7" @@ -6740,6 +6773,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "oid-registry" version = "0.7.1" @@ -6770,9 +6812,9 @@ dependencies = [ [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "oorandom" @@ -6828,21 +6870,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43dfaf083aef571385fccfdc3a2f8ede8d0a1863160455d4f2b014d8f7d04a3f" dependencies = [ "expander", - "indexmap 2.11.4", + "indexmap 2.12.0", "itertools 0.11.0", - "petgraph", + "petgraph 0.6.5", "proc-macro-crate 3.4.0", "proc-macro2", "quote", "syn 1.0.109", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "p3-dft" version = "0.3.0" @@ -7251,9 +7287,9 @@ dependencies = [ [[package]] name = "pallet-ranked-collective" -version = "41.0.0" +version = "41.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf43c766b69c37ab964cf076f605d3993357124fcdd14a8ba3ecc169e2d0fc9c" +checksum = "aae595fd2ce7b0889680e173fe7faf743f127143faf5f2e716bae12cd8c0b354" dependencies = [ "frame-benchmarking", "frame-support", @@ -7325,9 +7361,9 @@ dependencies = [ [[package]] name = "pallet-revive" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "474840408264f98eea7f187839ff2157f83a92bec6f3f3503dbf855c38f4de6b" +checksum = "1459d118aeccd7fef1dfb7e3ea002a376c8f07c61053eb2875acc38b1ceee4d9" dependencies = [ "alloy-core", "derive_more 0.99.20", @@ -7393,7 +7429,7 @@ checksum = "63c2dc2fc6961da23fefc54689ce81a8e006f6988bc465dcc9ab9db905d31766" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -7618,14 +7654,23 @@ dependencies = [ "log", "pallet-balances 40.0.1", "parity-scale-codec", + "qp-dilithium-crypto", + "qp-header", + "qp-plonky2", + "qp-poseidon", + "qp-poseidon-core", "qp-wormhole", "qp-wormhole-circuit", + "qp-wormhole-circuit-builder", + "qp-wormhole-prover", "qp-wormhole-verifier", "qp-zk-circuits-common", "scale-info", "sp-core", "sp-io", "sp-runtime", + "sp-state-machine", + "sp-trie", ] [[package]] @@ -7655,7 +7700,7 @@ dependencies = [ "log", "lz4", "memmap2 0.5.10", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "rand 0.8.5", "siphasher 0.3.11", "snap", @@ -7671,7 +7716,7 @@ dependencies = [ "arrayvec 0.7.6", "bitvec", "byte-slice-cast", - "bytes 1.10.1", + "bytes 1.11.0", "const_format", "impl-trait-for-tuples", "parity-scale-codec-derive", @@ -7688,7 +7733,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -7716,12 +7761,12 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", - "parking_lot_core 0.9.11", + "parking_lot_core 0.9.12", ] [[package]] @@ -7740,15 +7785,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.17", + "redox_syscall 0.5.18", "smallvec", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -7793,12 +7838,12 @@ checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "pem" -version = "3.0.5" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ "base64 0.22.1", - "serde", + "serde_core", ] [[package]] @@ -7818,20 +7863,19 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" +checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" dependencies = [ "memchr", - "thiserror 2.0.16", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc58706f770acb1dbd0973e6530a3cff4746fb721207feb3a8a6064cd0b6c663" +checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de" dependencies = [ "pest", "pest_generator", @@ -7839,22 +7883,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d4f36811dfe07f7b8573462465d5cb8965fffc2e71ae377a33aecf14c2c9a2f" +checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "pest_meta" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42919b05089acbd0a5dcd5405fb304d17d1053847b81163d09c4ad18ce8e8420" +checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a" dependencies = [ "pest", "sha2 0.10.9", @@ -7866,8 +7910,18 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ - "fixedbitset", - "indexmap 2.11.4", + "fixedbitset 0.4.2", + "indexmap 2.12.0", +] + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset 0.5.7", + "indexmap 2.12.0", ] [[package]] @@ -7887,7 +7941,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -8046,9 +8100,9 @@ dependencies = [ [[package]] name = "polkadot-node-primitives" -version = "20.0.0" +version = "20.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "498dd752f08ff2a3f92fa67e5f5cbca65a342c924465ca8cc7e4b3902a6f8613" +checksum = "01b9c91614ec9e0502c547792dd72b00cf3c73d7290f8e611008129207124e4f" dependencies = [ "bitvec", "bounded-vec", @@ -8181,9 +8235,9 @@ dependencies = [ [[package]] name = "polkadot-runtime-parachains" -version = "20.0.2" +version = "20.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7692d2a6109ef7b4749c51d5c950fa15f8e38fed439d5d520ba49fa322466c45" +checksum = "d5eccffd0a38fc4cdbf9b3b8c0bc9d140fc2cb49e1f073208b88c9c9138ae039" dependencies = [ "bitflags 1.3.2", "bitvec", @@ -8367,7 +8421,7 @@ dependencies = [ "polkavm-common 0.21.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -8379,7 +8433,7 @@ dependencies = [ "polkavm-common 0.24.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -8389,7 +8443,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36837f6b7edfd6f4498f8d25d81da16cf03bd6992c3e56f3d477dfc90f4fefca" dependencies = [ "polkavm-derive-impl 0.21.0", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -8399,7 +8453,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba0ef0f17ad81413ea1ca5b1b67553aedf5650c88269b673d3ba015c83bc2651" dependencies = [ "polkavm-derive-impl 0.24.0", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -8457,7 +8511,7 @@ dependencies = [ "hermit-abi 0.5.2", "pin-project-lite 0.2.16", "rustix 1.1.2", - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -8500,9 +8554,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -8530,7 +8584,7 @@ checksum = "b4a326caf27cbf2ac291ca7fd56300497ba9e76a8cc6a7d95b7a18b57f22b61d" dependencies = [ "cc", "dunce", - "getrandom 0.3.3", + "getrandom 0.3.4", "libc", ] @@ -8596,7 +8650,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -8676,7 +8730,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.23.6", + "toml_edit 0.23.7", ] [[package]] @@ -8722,7 +8776,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -8733,14 +8787,14 @@ checksum = "75eea531cfcd120e0851a3f8aed42c4841f78c889eefafd96339c72677ae42c3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -8755,7 +8809,7 @@ dependencies = [ "fnv", "lazy_static", "memchr", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "thiserror 1.0.69", ] @@ -8767,7 +8821,7 @@ checksum = "504ee9ff529add891127c4827eb481bd69dc0ebc72e9a682e187db4caa60c3ca" dependencies = [ "dtoa", "itoa", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "prometheus-client-derive-encode", ] @@ -8779,24 +8833,23 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "proptest" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb0be07becd10686a0bb407298fb425360a5c44a663774406340c59a22de4ce" +checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.9.4", - "lazy_static", + "bitflags 2.10.0", "num-traits", "rand 0.9.2", "rand_chacha 0.9.0", "rand_xorshift", - "regex-syntax 0.8.6", + "regex-syntax", "rusty-fork", "tempfile", "unarray", @@ -8808,7 +8861,7 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.0", "prost-derive 0.12.6", ] @@ -8818,7 +8871,7 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.0", "prost-derive 0.13.5", ] @@ -8833,12 +8886,12 @@ dependencies = [ "log", "multimap", "once_cell", - "petgraph", + "petgraph 0.7.1", "prettyplease", "prost 0.13.5", "prost-types", "regex", - "syn 2.0.106", + "syn 2.0.110", "tempfile", ] @@ -8852,7 +8905,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -8865,7 +8918,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -8879,10 +8932,11 @@ dependencies = [ [[package]] name = "psm" -version = "0.1.26" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f" +checksum = "d11f2fedc3b7dafdc2851bc52f277377c5473d378859be234bc7ebb593144d01" dependencies = [ + "ar_archive_writer", "cc", ] @@ -8923,9 +8977,9 @@ dependencies = [ [[package]] name = "qp-plonky2" -version = "1.1.1" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "068162bd3730e381744eca219f98f0db22874afc8fa24631611e975c93d6cf68" +checksum = "39530b02faa85964bba211e030afa2d54995b403b0022f88e984c4c65679c4bc" dependencies = [ "ahash", "anyhow", @@ -8935,9 +8989,14 @@ dependencies = [ "keccak-hash 0.8.0", "log", "num", + "p3-field", + "p3-goldilocks", + "p3-poseidon2", + "p3-symmetric", "plonky2_maybe_rayon", "plonky2_util", "qp-plonky2-field", + "qp-poseidon-constants", "rand 0.8.5", "rand_chacha 0.3.1", "serde", @@ -8948,9 +9007,9 @@ dependencies = [ [[package]] name = "qp-plonky2-field" -version = "1.1.1" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b2f7e0854f9e14aa9b3f6d544ccf71087e751d6adbe6a7e50d2c75fb561d97" +checksum = "7e8d52dadf3bb92708c309922b62d7f3f2587d3047f9fe05a0c9f587e2890526" dependencies = [ "anyhow", "itertools 0.11.0", @@ -8965,9 +9024,9 @@ dependencies = [ [[package]] name = "qp-poseidon" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0353086f7af1df7d45a1ecb995cf84b583c8211d7122f542044b37388b5effcd" +checksum = "8ebc5e9fe1f91f5006aa2b45650d0049887d41d80c669ef5a78a45086895054d" dependencies = [ "log", "p3-field", @@ -8997,14 +9056,15 @@ dependencies = [ [[package]] name = "qp-poseidon-core" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e658a373a7fb22babeda9ffcc8af0a894e6e3c008272ed735509eccb7769ead3" +checksum = "f52c70df221c356b3ce63afabfae623aae632c27d3e078cd20eec4348096c2d7" dependencies = [ "p3-field", "p3-goldilocks", "p3-poseidon2", "p3-symmetric", + "qp-plonky2", "qp-poseidon-constants", "rand_chacha 0.9.0", ] @@ -9037,7 +9097,7 @@ dependencies = [ "rand_core 0.9.3", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -9056,21 +9116,34 @@ version = "0.1.0" [[package]] name = "qp-wormhole-circuit" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea70a3a1bf545450cb6320782389cfbfe58fcf0ecf724aa666383807bf5548b" +checksum = "10b7357bec091287185850a6f7b67eb45f899a655b2a86825eae7e39b3bc6c3c" dependencies = [ "anyhow", "hex", "qp-plonky2", + "qp-poseidon-core", "qp-zk-circuits-common", ] [[package]] name = "qp-wormhole-circuit-builder" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d6486fbfd79005d520caff6a32a8fde4eab978110101c9612d140f3e0ebdfd0" +checksum = "7faed51eaa66ba71526955b74aac2fdba9849905a405f74d5f4f26564a9eaa98" +dependencies = [ + "anyhow", + "qp-plonky2", + "qp-wormhole-circuit", + "qp-zk-circuits-common", +] + +[[package]] +name = "qp-wormhole-prover" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a56d84dd9bf64944b88cb0ec971f179a569d3bdf148132ab01bfe2d56c069d" dependencies = [ "anyhow", "qp-plonky2", @@ -9080,9 +9153,9 @@ dependencies = [ [[package]] name = "qp-wormhole-verifier" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f83ee7ce0fdb40f34eed7a31a575e857d24135a383cdfd6f2429a7ce048b665" +checksum = "40387c4c43fe47419cba58eb4f13c2a3c32ac0e381c98c3d77293ebf53298de7" dependencies = [ "anyhow", "qp-plonky2", @@ -9092,12 +9165,14 @@ dependencies = [ [[package]] name = "qp-zk-circuits-common" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a2a62915be0513d045f4d92829c0a26d6f3e8897353e8f8356a7eabef025329" +checksum = "366557f4c727379e2fd9c18b6667ae53c99bb241b973427df09a6b09584a11d4" dependencies = [ "anyhow", + "hex", "qp-plonky2", + "qp-poseidon-core", "serde", ] @@ -9123,7 +9198,7 @@ dependencies = [ "libc", "once_cell", "raw-cpuid", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "web-sys", "winapi", ] @@ -9231,6 +9306,7 @@ dependencies = [ "pallet-treasury", "pallet-utility", "pallet-vesting", + "pallet-wormhole", "parity-scale-codec", "primitive-types 0.13.1", "qp-dilithium-crypto", @@ -9281,7 +9357,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15a0580ab32b169745d7a39db2ba969226ca16738931be152a3209b409de2474" dependencies = [ "asynchronous-codec 0.7.0", - "bytes 1.10.1", + "bytes 1.11.0", "quick-protobuf", "thiserror 1.0.69", "unsigned-varint 0.8.0", @@ -9293,7 +9369,7 @@ version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.0", "cfg_aliases 0.2.1", "futures-io", "pin-project-lite 0.2.16", @@ -9301,9 +9377,9 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.1", "rustls", - "socket2 0.6.0", - "thiserror 2.0.16", - "tokio 1.47.1", + "socket2 0.6.1", + "thiserror 2.0.17", + "tokio 1.48.0", "tracing", "web-time", ] @@ -9314,8 +9390,8 @@ version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ - "bytes 1.10.1", - "getrandom 0.3.3", + "bytes 1.11.0", + "getrandom 0.3.4", "lru-slab", "rand 0.9.2", "ring 0.17.14", @@ -9323,7 +9399,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.16", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -9338,16 +9414,16 @@ dependencies = [ "cfg_aliases 0.2.1", "libc", "once_cell", - "socket2 0.6.0", + "socket2 0.6.1", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -9421,7 +9497,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "serde", ] @@ -9459,7 +9535,7 @@ version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] @@ -9511,11 +9587,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] @@ -9531,22 +9607,22 @@ dependencies = [ [[package]] name = "ref-cast" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -9576,47 +9652,32 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.2" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.10", - "regex-syntax 0.8.6", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.4.10" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.6", + "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" @@ -9625,7 +9686,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ "base64 0.21.7", - "bytes 1.10.1", + "bytes 1.11.0", "encoding_rs", "futures-core", "futures-util", @@ -9645,7 +9706,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "system-configuration 0.5.1", - "tokio 1.47.1", + "tokio 1.48.0", "tower-service", "url", "wasm-bindgen", @@ -9714,7 +9775,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.0", "rustc-hex", ] @@ -9724,7 +9785,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa24e92bb2a83198bb76d661a71df9f7076b8c420b8696e4d3d97d50d94479e3" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.0", "rustc-hex", ] @@ -9770,7 +9831,7 @@ dependencies = [ "netlink-sys", "nix", "thiserror 1.0.69", - "tokio 1.47.1", + "tokio 1.48.0", ] [[package]] @@ -9785,14 +9846,15 @@ dependencies = [ [[package]] name = "ruint" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecb38f82477f20c5c3d62ef52d7c4e536e38ea9b73fb570a20c5cae0e14bcf6" +checksum = "a68df0380e5c9d20ce49534f292a36a7514ae21350726efe1865bdb1fa91d278" dependencies = [ "alloy-rlp", "ark-ff 0.3.0", "ark-ff 0.4.2", - "bytes 1.10.1", + "ark-ff 0.5.0", + "bytes 1.11.0", "fastrlp 0.3.1", "fastrlp 0.4.0", "num-bigint", @@ -9805,7 +9867,7 @@ dependencies = [ "rand 0.9.2", "rlp 0.5.2", "ruint-macro", - "serde", + "serde_core", "valuable", "zeroize", ] @@ -9887,33 +9949,33 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.32" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "log", "once_cell", "ring 0.17.14", "rustls-pki-types", - "rustls-webpki 0.103.6", + "rustls-webpki 0.103.8", "subtle 2.6.1", "zeroize", ] [[package]] name = "rustls-native-certs" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -9923,9 +9985,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" dependencies = [ "web-time", "zeroize", @@ -9945,7 +10007,7 @@ dependencies = [ "rustls", "rustls-native-certs", "rustls-platform-verifier-android", - "rustls-webpki 0.103.6", + "rustls-webpki 0.103.8", "security-framework", "security-framework-sys", "webpki-root-certs 0.26.11", @@ -9970,9 +10032,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.6" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "ring 0.17.14", "rustls-pki-types", @@ -9987,9 +10049,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rusty-fork" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" dependencies = [ "fnv", "quick-error", @@ -10092,7 +10154,7 @@ dependencies = [ "sp-runtime", "substrate-prometheus-endpoint", "thiserror 1.0.69", - "tokio 1.47.1", + "tokio 1.48.0", ] [[package]] @@ -10141,7 +10203,7 @@ checksum = "c25df970b58c05e8381a1ead6f5708e3539aefa9212d311866fed64f141214e7" dependencies = [ "array-bytes 6.2.3", "docify", - "memmap2 0.9.8", + "memmap2 0.9.9", "parity-scale-codec", "sc-chain-spec-derive", "sc-client-api", @@ -10169,7 +10231,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -10217,7 +10279,7 @@ dependencies = [ "sp-version", "tempfile", "thiserror 1.0.69", - "tokio 1.47.1", + "tokio 1.48.0", ] [[package]] @@ -10230,7 +10292,7 @@ dependencies = [ "futures 0.3.31", "log", "parity-scale-codec", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "sc-executor", "sc-transaction-pool-api", "sc-utils", @@ -10261,7 +10323,7 @@ dependencies = [ "log", "parity-db", "parity-scale-codec", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "sc-client-api", "sc-state-db", "schnellru", @@ -10286,7 +10348,7 @@ dependencies = [ "futures 0.3.31", "log", "mockall", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "sc-client-api", "sc-network-types", "sc-utils", @@ -10314,7 +10376,7 @@ dependencies = [ "num-rational", "num-traits", "parity-scale-codec", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "sc-client-api", "sc-consensus", "sc-consensus-epochs", @@ -10361,7 +10423,7 @@ dependencies = [ "hex", "log", "parity-scale-codec", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "primitive-types 0.13.1", "sc-client-api", "sc-consensus", @@ -10413,7 +10475,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfd7a23eebd1fea90534994440f0ef516cbd8c0ef749e272c6c77fd729bbca71" dependencies = [ "parity-scale-codec", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "sc-executor-common", "sc-executor-polkavm", "sc-executor-wasmtime", @@ -10464,7 +10526,7 @@ checksum = "a5980897e2915ef027560886a2bb52f49a2cea4a9b9f5c75fead841201d03705" dependencies = [ "anyhow", "log", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "rustix 0.36.17", "sc-allocator", "sc-executor-common", @@ -10497,7 +10559,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08c28fc85c00ddf64f32f68111c61521b1f260f8dfa1eb98f9ed4401aa5d0ce0" dependencies = [ "array-bytes 6.2.3", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "serde_json", "sp-application-crypto", "sp-core", @@ -10514,13 +10576,13 @@ dependencies = [ "array-bytes 6.2.3", "arrayvec 0.7.6", "blake2 0.10.6", - "bytes 1.10.1", + "bytes 1.11.0", "futures 0.3.31", "futures-timer", "log", "mixnet", "parity-scale-codec", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "sc-client-api", "sc-network", "sc-network-types", @@ -10543,7 +10605,7 @@ dependencies = [ "async-channel 1.9.0", "async-trait", "asynchronous-codec 0.6.2", - "bytes 1.10.1", + "bytes 1.11.0", "cid 0.9.0", "criterion", "either", @@ -10559,7 +10621,7 @@ dependencies = [ "multistream-select", "once_cell", "parity-scale-codec", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "partial_sort", "pin-project", "prost 0.12.6", @@ -10585,7 +10647,7 @@ dependencies = [ "substrate-prometheus-endpoint", "tempfile", "thiserror 1.0.69", - "tokio 1.47.1", + "tokio 1.48.0", "tokio-stream", "tokio-test", "tokio-util", @@ -10660,7 +10722,7 @@ dependencies = [ "sp-runtime", "substrate-prometheus-endpoint", "thiserror 1.0.69", - "tokio 1.47.1", + "tokio 1.48.0", "tokio-stream", ] @@ -10691,7 +10753,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "441af5d0adf306ff745ccf23c7426ec2edf24f6fee678fb63994e1f8d2fcc890" dependencies = [ "bs58", - "bytes 1.10.1", + "bytes 1.11.0", "ed25519-dalek", "libp2p-identity", "libp2p-kad", @@ -10712,18 +10774,18 @@ version = "46.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "029f2eb16f9510a749201e7a2b405aafcc5afcc515509518d2efb17b164ca47e" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.0", "fnv", "futures 0.3.31", "futures-timer", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-rustls", "hyper-util", "num_cpus", "once_cell", "parity-scale-codec", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "rand 0.8.5", "rustls", "sc-client-api", @@ -10761,7 +10823,7 @@ dependencies = [ "jsonrpsee", "log", "parity-scale-codec", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "sc-block-builder", "sc-chain-spec", "sc-client-api", @@ -10781,7 +10843,7 @@ dependencies = [ "sp-session", "sp-statement-store", "sp-version", - "tokio 1.47.1", + "tokio 1.48.0", ] [[package]] @@ -10817,7 +10879,7 @@ dependencies = [ "governor", "http 1.3.1", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.1", "ip_network", "jsonrpsee", "log", @@ -10825,7 +10887,7 @@ dependencies = [ "serde", "serde_json", "substrate-prometheus-endpoint", - "tokio 1.47.1", + "tokio 1.48.0", "tower", "tower-http", ] @@ -10844,7 +10906,7 @@ dependencies = [ "jsonrpsee", "log", "parity-scale-codec", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "rand 0.8.5", "sc-chain-spec", "sc-client-api", @@ -10860,7 +10922,7 @@ dependencies = [ "sp-version", "substrate-prometheus-endpoint", "thiserror 1.0.69", - "tokio 1.47.1", + "tokio 1.48.0", "tokio-stream", ] @@ -10894,7 +10956,7 @@ dependencies = [ "jsonrpsee", "log", "parity-scale-codec", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pin-project", "rand 0.8.5", "sc-chain-spec", @@ -10940,7 +11002,7 @@ dependencies = [ "substrate-prometheus-endpoint", "tempfile", "thiserror 1.0.69", - "tokio 1.47.1", + "tokio 1.48.0", "tracing", "tracing-futures", ] @@ -10953,7 +11015,7 @@ checksum = "b81d0da325c141081336e8d8724f5342f2586bbb574fde81f716f7fab447325a" dependencies = [ "log", "parity-scale-codec", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "sp-core", ] @@ -10988,7 +11050,7 @@ dependencies = [ "futures 0.3.31", "libp2p", "log", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pin-project", "rand 0.8.5", "sc-utils", @@ -11000,17 +11062,18 @@ dependencies = [ [[package]] name = "sc-tracing" -version = "40.0.0" +version = "40.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d80e2558a0100794d5b4f4d75cb45cae65c061aaf386fd807d7a7e2c272251d2" +checksum = "b83b0097094cc643a224c093a50eae9b0a9cbfc00288e0544d0e9f0b229dd0bd" dependencies = [ "chrono", "console", + "frame-metadata 23.0.0", "is-terminal", "libc", "log", "parity-scale-codec", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "rustc-hash 1.1.0", "sc-client-api", "sc-tracing-proc-macro", @@ -11021,6 +11084,7 @@ dependencies = [ "sp-rpc", "sp-runtime", "sp-tracing", + "sp-trie", "thiserror 1.0.69", "tracing", "tracing-log", @@ -11036,7 +11100,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -11048,11 +11112,11 @@ dependencies = [ "async-trait", "futures 0.3.31", "futures-timer", - "indexmap 2.11.4", + "indexmap 2.12.0", "itertools 0.11.0", "linked-hash-map", "parity-scale-codec", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "sc-client-api", "sc-transaction-pool-api", "sc-utils", @@ -11066,7 +11130,7 @@ dependencies = [ "sp-transaction-pool", "substrate-prometheus-endpoint", "thiserror 1.0.69", - "tokio 1.47.1", + "tokio 1.48.0", "tokio-stream", "tracing", ] @@ -11079,7 +11143,7 @@ checksum = "c27a9cb54784cf7a1a607d4314f1a4437279ce6d4070eb810f3e4fbfff9b1ba7" dependencies = [ "async-trait", "futures 0.3.31", - "indexmap 2.11.4", + "indexmap 2.12.0", "log", "parity-scale-codec", "serde", @@ -11099,7 +11163,7 @@ dependencies = [ "futures 0.3.31", "futures-timer", "log", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "prometheus", "sp-arithmetic", ] @@ -11128,7 +11192,7 @@ dependencies = [ "scale-decode-derive", "scale-type-resolver", "smallvec", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -11140,7 +11204,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -11155,7 +11219,7 @@ dependencies = [ "scale-encode-derive", "scale-type-resolver", "smallvec", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -11168,7 +11232,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -11194,7 +11258,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -11216,15 +11280,15 @@ dependencies = [ "proc-macro2", "quote", "scale-info", - "syn 2.0.106", - "thiserror 2.0.16", + "syn 2.0.110", + "thiserror 2.0.17", ] [[package]] name = "scale-value" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca8b26b451ecb7fd7b62b259fa28add63d12ec49bbcac0e01fcb4b5ae0c09aa" +checksum = "884aab179aba344c67ddcd1d7dd8e3f8fee202f2e570d97ec34ec8688442a5b3" dependencies = [ "base58", "blake2 0.10.6", @@ -11235,7 +11299,7 @@ dependencies = [ "scale-encode", "scale-type-resolver", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", "yap", ] @@ -11245,7 +11309,7 @@ version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -11278,12 +11342,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - [[package]] name = "scopeguard" version = "1.2.0" @@ -11399,11 +11457,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.5.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc198e42d9b7510827939c9a15f5062a0c913f3371d765977e586d2fe6c16f4a" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -11465,9 +11523,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.226" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -11485,22 +11543,22 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.226" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.226" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -11539,15 +11597,14 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.14.1" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" +checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1" dependencies = [ "base64 0.22.1", "chrono", "hex", - "serde", - "serde_derive", + "serde_core", "serde_json", "serde_with_macros", "time", @@ -11555,14 +11612,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.14.1" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e" +checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b" dependencies = [ "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -11683,7 +11740,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dee851d0e5e7af3721faea1843e8015e820a234f81fda3dea9247e15bac9a86a" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] @@ -11816,7 +11873,7 @@ dependencies = [ "itertools 0.13.0", "log", "lru", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pin-project", "rand 0.8.5", "rand_chacha 0.3.1", @@ -11859,12 +11916,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -11874,7 +11931,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" dependencies = [ "base64 0.22.1", - "bytes 1.10.1", + "bytes 1.11.0", "futures 0.3.31", "http 1.3.1", "httparse", @@ -11918,7 +11975,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -11981,7 +12038,7 @@ checksum = "849f1cfcf170048d59c8d3d1175feea2a5cd41fe39742660b9ed542f0d1be7b0" dependencies = [ "futures 0.3.31", "parity-scale-codec", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "schnellru", "sp-api", "sp-consensus", @@ -12120,7 +12177,7 @@ dependencies = [ "merlin", "parity-bip39", "parity-scale-codec", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "paste", "primitive-types 0.13.1", "rand 0.8.5", @@ -12166,7 +12223,7 @@ checksum = "b85d0f1f1e44bd8617eb2a48203ee854981229e3e79e6f468c7175d5fd37489b" dependencies = [ "quote", "sp-crypto-hashing", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -12176,7 +12233,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "722cbecdbf5b94578137dbd07feb51e95f7de221be0c1ff4dcfe0bb4cd986929" dependencies = [ "kvdb", - "parking_lot 0.12.4", + "parking_lot 0.12.5", ] [[package]] @@ -12187,7 +12244,7 @@ checksum = "48d09fa0a5f7299fb81ee25ae3853d26200f7a348148aed6de76be905c007dbe" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -12234,7 +12291,7 @@ version = "41.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3f244e9a2818d21220ceb0915ac73a462814a92d0c354a124a818abdb7f4f66" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.0", "docify", "ed25519-dalek", "libsecp256k1", @@ -12273,16 +12330,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "269d0ee360f6d072f9203485afea35583ac151521a525cc48b2a107fc576c2d9" dependencies = [ "parity-scale-codec", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "sp-core", "sp-externalities", ] [[package]] name = "sp-maybe-compressed-blob" -version = "11.0.0" +version = "11.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c768c11afbe698a090386876911da4236af199cd38a5866748df4d8628aeff" +checksum = "c9d204064a17660455603ae152b02fc7ea4cfff2d14796f6483d7a35c4cca336" dependencies = [ "thiserror 1.0.69", "zstd 0.12.4", @@ -12411,7 +12468,7 @@ version = "30.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fcd9c219da8c85d45d5ae1ce80e73863a872ac27424880322903c6ac893c06e" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.0", "impl-trait-for-tuples", "parity-scale-codec", "polkavm-derive 0.24.0", @@ -12436,7 +12493,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -12478,7 +12535,7 @@ dependencies = [ "hash-db", "log", "parity-scale-codec", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pretty_assertions", "rand 0.8.5", "smallvec", @@ -12494,9 +12551,9 @@ dependencies = [ [[package]] name = "sp-statement-store" -version = "21.0.0" +version = "21.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d11b0753df3d68f5bb0f4d0d3975788c3a4dd2d0e479e28b7af17b52f15160" +checksum = "031e2366f3633aac66c223747f47db82d774e726e117776eab66937c0bf9f4a8" dependencies = [ "aes-gcm", "curve25519-dalek", @@ -12600,7 +12657,7 @@ dependencies = [ "memory-db", "nohash-hasher", "parity-scale-codec", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "rand 0.8.5", "scale-info", "schnellru", @@ -12644,7 +12701,7 @@ dependencies = [ "proc-macro-warning", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -12723,9 +12780,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "staging-xcm" @@ -12810,8 +12867,8 @@ dependencies = [ "bitflags 1.3.2", "cfg_aliases 0.2.1", "libc", - "parking_lot 0.12.4", - "parking_lot_core 0.9.11", + "parking_lot 0.12.5", + "parking_lot_core 0.9.12", "static_init_macro", "winapi", ] @@ -12889,7 +12946,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -12952,12 +13009,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d23e4bc8e910a312820d589047ab683928b761242dbe31dee081fbdb37cbe0be" dependencies = [ "http-body-util", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-util", "log", "prometheus", "thiserror 1.0.69", - "tokio 1.47.1", + "tokio 1.48.0", ] [[package]] @@ -13052,8 +13109,8 @@ dependencies = [ "subxt-macro", "subxt-metadata", "subxt-rpcs", - "thiserror 2.0.16", - "tokio 1.47.1", + "thiserror 2.0.17", + "tokio 1.48.0", "tokio-util", "tracing", "url", @@ -13073,8 +13130,8 @@ dependencies = [ "scale-info", "scale-typegen", "subxt-metadata", - "syn 2.0.106", - "thiserror 2.0.16", + "syn 2.0.110", + "thiserror 2.0.17", ] [[package]] @@ -13103,7 +13160,7 @@ dependencies = [ "serde_json", "sp-crypto-hashing", "subxt-metadata", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", ] @@ -13118,8 +13175,8 @@ dependencies = [ "serde", "serde_json", "smoldot-light", - "thiserror 2.0.16", - "tokio 1.47.1", + "thiserror 2.0.17", + "tokio 1.48.0", "tokio-stream", "tracing", ] @@ -13137,7 +13194,7 @@ dependencies = [ "scale-typegen", "subxt-codegen", "subxt-utils-fetchmetadata", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -13152,7 +13209,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-crypto-hashing", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -13173,7 +13230,7 @@ dependencies = [ "serde_json", "subxt-core", "subxt-lightclient", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "url", ] @@ -13204,7 +13261,7 @@ dependencies = [ "sha2 0.10.9", "sp-crypto-hashing", "subxt-core", - "thiserror 2.0.16", + "thiserror 2.0.17", "zeroize", ] @@ -13216,7 +13273,7 @@ checksum = "fc868b55fe2303788dc7703457af390111940c3da4714b510983284501780ed5" dependencies = [ "hex", "parity-scale-codec", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -13232,9 +13289,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", @@ -13243,14 +13300,14 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0b198d366dbec045acfcd97295eb653a7a2b40e4dc764ef1e79aafcad439d3c" +checksum = "ff790eb176cc81bb8936aed0f7b9f14fc4670069a2d371b3e3b0ecce908b2cb3" dependencies = [ "paste", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -13279,7 +13336,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -13314,7 +13371,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.9.4", "system-configuration-sys 0.6.0", ] @@ -13359,15 +13416,15 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" -version = "3.22.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "rustix 1.1.2", - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -13406,11 +13463,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.16", + "thiserror-impl 2.0.17", ] [[package]] @@ -13421,18 +13478,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -13542,9 +13599,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -13588,22 +13645,19 @@ dependencies = [ [[package]] name = "tokio" -version = "1.47.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", - "bytes 1.10.1", - "io-uring", + "bytes 1.11.0", "libc", "mio", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pin-project-lite 0.2.16", "signal-hook-registry", - "slab", - "socket2 0.6.0", - "tokio-macros 2.5.0", - "windows-sys 0.59.0", + "socket2 0.6.1", + "tokio-macros 2.6.0", + "windows-sys 0.61.2", ] [[package]] @@ -13619,23 +13673,23 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "tokio-rustls" -version = "0.26.3" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f63835928ca123f1bef57abbcd23bb2ba0ac9ae1235f1e65bda0d06e7786bd" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", - "tokio 1.47.1", + "tokio 1.48.0", ] [[package]] @@ -13646,7 +13700,7 @@ checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite 0.2.16", - "tokio 1.47.1", + "tokio 1.48.0", "tokio-util", ] @@ -13657,9 +13711,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" dependencies = [ "async-stream", - "bytes 1.10.1", + "bytes 1.11.0", "futures-core", - "tokio 1.47.1", + "tokio 1.48.0", "tokio-stream", ] @@ -13674,23 +13728,23 @@ dependencies = [ "rustls", "rustls-native-certs", "rustls-pki-types", - "tokio 1.47.1", + "tokio 1.48.0", "tokio-rustls", "tungstenite", ] [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.0", "futures-core", "futures-io", "futures-sink", "pin-project-lite 0.2.16", - "tokio 1.47.1", + "tokio 1.48.0", ] [[package]] @@ -13725,9 +13779,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" dependencies = [ "serde_core", ] @@ -13738,7 +13792,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.0", "serde", "serde_spanned", "toml_datetime 0.6.11", @@ -13748,21 +13802,21 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.6" +version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ - "indexmap 2.11.4", - "toml_datetime 0.7.2", + "indexmap 2.12.0", + "toml_datetime 0.7.3", "toml_parser", "winnow", ] [[package]] name = "toml_parser" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ "winnow", ] @@ -13794,8 +13848,8 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ - "bitflags 2.9.4", - "bytes 1.10.1", + "bitflags 2.10.0", + "bytes 1.11.0", "http 1.3.1", "http-body 1.0.1", "http-body-util", @@ -13836,7 +13890,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -13881,7 +13935,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -13897,15 +13951,15 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "parking_lot 0.12.4", - "regex", + "parking_lot 0.12.5", + "regex-automata", "sharded-slab", "smallvec", "thread_local", @@ -13990,7 +14044,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.0", "data-encoding", "http 1.3.1", "httparse", @@ -13999,7 +14053,7 @@ dependencies = [ "rustls", "rustls-pki-types", "sha1", - "thiserror 2.0.16", + "thiserror 2.0.17", "url", "utf-8", ] @@ -14024,9 +14078,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "ucd-trie" @@ -14066,9 +14120,9 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-normalization" @@ -14087,9 +14141,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unicode-xid" @@ -14124,7 +14178,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6889a77d49f1f013504cec6bf97a2c730394adedaeb1deb5ea08949a50541105" dependencies = [ "asynchronous-codec 0.6.2", - "bytes 1.10.1", + "bytes 1.11.0", "futures-io", "futures-util", ] @@ -14135,7 +14189,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.0", "tokio-util", ] @@ -14187,7 +14241,7 @@ version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "js-sys", "serde", "wasm-bindgen", @@ -14319,15 +14373,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -14343,14 +14388,14 @@ version = "0.12.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1fbb4ef9bbca0c1170e0b00dd28abc9e3b68669821600cad1caaed606583c6d" dependencies = [ - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "wasm-bindgen" -version = "0.2.103" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", @@ -14359,25 +14404,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-futures" -version = "0.4.53" +version = "0.4.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0b221ff421256839509adbb55998214a70d829d3a28c69b4a6672e9d2a42f67" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" dependencies = [ "cfg-if", "js-sys", @@ -14388,9 +14419,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.103" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -14398,22 +14429,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.103" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.106", - "wasm-bindgen-backend", + "syn 2.0.110", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.103" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] @@ -14738,9 +14769,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.80" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" dependencies = [ "js-sys", "wasm-bindgen", @@ -14762,14 +14793,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" dependencies = [ - "webpki-root-certs 1.0.2", + "webpki-root-certs 1.0.4", ] [[package]] name = "webpki-root-certs" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4ffd8df1c57e87c325000a3d6ef93db75279dc3a231125aac571650f22b12a" +checksum = "ee3e3b5f5e80bc89f30ce8d0343bf4e5f12341c51f3e26cbeecbc7c85443e85b" dependencies = [ "rustls-pki-types", ] @@ -14792,9 +14823,9 @@ dependencies = [ [[package]] name = "widestring" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" [[package]] name = "winapi" @@ -14818,7 +14849,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -14847,28 +14878,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections", - "windows-core 0.61.2", - "windows-future", - "windows-link 0.1.3", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core 0.61.2", -] - [[package]] name = "windows-core" version = "0.52.0" @@ -14890,84 +14899,44 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.61.2" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-core" -version = "0.62.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.2.0", - "windows-result 0.4.0", - "windows-strings 0.5.0", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", - "windows-threading", + "windows-link", + "windows-result 0.4.1", + "windows-strings", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-link" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" - -[[package]] -name = "windows-numerics" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", -] +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-result" @@ -14980,38 +14949,20 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" -dependencies = [ - "windows-link 0.2.0", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] name = "windows-strings" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.2.0", + "windows-link", ] [[package]] @@ -15056,16 +15007,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.3", + "windows-targets 0.53.5", ] [[package]] name = "windows-sys" -version = "0.61.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.0", + "windows-link", ] [[package]] @@ -15116,28 +15067,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" -dependencies = [ - "windows-link 0.1.3", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", -] - -[[package]] -name = "windows-threading" -version = "0.1.0" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link 0.1.3", + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -15160,9 +15102,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -15184,9 +15126,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -15208,9 +15150,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -15220,9 +15162,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -15244,9 +15186,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -15268,9 +15210,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -15292,9 +15234,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -15316,9 +15258,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" @@ -15347,9 +15289,9 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "wyz" @@ -15402,7 +15344,7 @@ dependencies = [ "nom", "oid-registry 0.8.1", "rusticata-macros", - "thiserror 2.0.16", + "thiserror 2.0.17", "time", ] @@ -15415,14 +15357,14 @@ dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "xml-rs" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" [[package]] name = "xmltree" @@ -15442,7 +15384,7 @@ dependencies = [ "futures 0.3.31", "log", "nohash-hasher", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pin-project", "rand 0.8.5", "static_assertions", @@ -15450,14 +15392,14 @@ dependencies = [ [[package]] name = "yamux" -version = "0.13.6" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2dd50a6d6115feb3e5d7d0efd45e8ca364b6c83722c1e9c602f5764e0e9597" +checksum = "deab71f2e20691b4728b349c6cee8fc7223880fa67b6b4f92225ec32225447e5" dependencies = [ "futures 0.3.31", "log", "nohash-hasher", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pin-project", "rand 0.9.2", "static_assertions", @@ -15487,11 +15429,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -15499,13 +15440,13 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", "synstructure 0.13.2", ] @@ -15526,7 +15467,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -15546,15 +15487,15 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", "synstructure 0.13.2", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" dependencies = [ "zeroize_derive", ] @@ -15567,14 +15508,14 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -15583,9 +15524,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -15594,13 +15535,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index fe5056a4..45a9c307 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -140,16 +140,20 @@ sp-consensus-pow = { path = "./primitives/consensus/pow", default-features = fal sp-consensus-qpow = { path = "./primitives/consensus/qpow", default-features = false } # Quantus network dependencies -qp-poseidon = { version = "1.0.1", default-features = false } -qp-poseidon-core = { version = "1.0.1", default-features = false, features = ["p3"] } +qp-plonky2 = { version = "1.1.3", default-features = false } +qp-poseidon = { version = "1.0.3", default-features = false } +qp-poseidon-core = { version = "1.0.3", default-features = false, features = ["p2", "p3"] } qp-rusty-crystals-dilithium = { version = "2.0.0", default-features = false } qp-rusty-crystals-hdwallet = { version = "1.0.0" } -qp-wormhole-circuit = { version = "0.1.2", default-features = false } -qp-wormhole-circuit-builder = { version = "0.1.2", default-features = false } -qp-wormhole-verifier = { version = "0.1.2", default-features = false, features = [ +qp-wormhole-circuit = { version = "0.1.3", default-features = false } +qp-wormhole-circuit-builder = { version = "0.1.4", default-features = false } +qp-wormhole-prover = { version = "0.1.4", default-features = false, features = [ "no_random", ] } -qp-zk-circuits-common = { version = "0.1.2", default-features = false, features = [ +qp-wormhole-verifier = { version = "0.1.4", default-features = false, features = [ + "no_random", +] } +qp-zk-circuits-common = { version = "0.1.4", default-features = false, features = [ "no_random", ] } diff --git a/node/build.rs b/node/build.rs index 2de7de2a..6b47474f 100644 --- a/node/build.rs +++ b/node/build.rs @@ -33,8 +33,9 @@ fn generate_circuit_binaries() { // Call the circuit-builder to generate binaries directly in the pallet directory // We don't need the prover binary for the chain, only verifier and common - qp_wormhole_circuit_builder::generate_circuit_binaries("../pallets/wormhole", false) - .expect("Failed to generate circuit binaries"); + // TODO: uncomment this once `no_random` issue is fixed in zk-circuits + // qp_wormhole_circuit_builder::generate_circuit_binaries("../pallets/wormhole", false) + // .expect("Failed to generate circuit binaries"); println!("cargo:trace=✅ Circuit binaries generated successfully"); } diff --git a/pallets/balances/src/lib.rs b/pallets/balances/src/lib.rs index 1ead5bcf..20165bea 100644 --- a/pallets/balances/src/lib.rs +++ b/pallets/balances/src/lib.rs @@ -401,6 +401,13 @@ pub mod pallet { Thawed { who: T::AccountId, amount: T::Balance }, /// The `TotalIssuance` was forcefully changed. TotalIssuanceForced { old: T::Balance, new: T::Balance }, + /// Transfer proof was stored. + TransferProofStored { + transfer_count: u64, + source: T::AccountId, + dest: T::AccountId, + funding_amount: T::Balance, + }, } #[pallet::error] @@ -874,9 +881,15 @@ pub mod pallet { ) { if from != to { let current_count = Self::transfer_count(); + TransferProof::::insert((current_count, from.clone(), to.clone(), value), ()); TransferCount::::put(current_count.saturating_add(One::one())); - TransferProof::::insert((current_count, from.clone(), to.clone(), value), ()); + Self::deposit_event(Event::TransferProofStored { + transfer_count: current_count, + source: from.clone(), + dest: to.clone(), + funding_amount: value, + }); } } diff --git a/pallets/balances/src/tests/reentrancy_tests.rs b/pallets/balances/src/tests/reentrancy_tests.rs index 89b27528..767f3ddf 100644 --- a/pallets/balances/src/tests/reentrancy_tests.rs +++ b/pallets/balances/src/tests/reentrancy_tests.rs @@ -56,7 +56,7 @@ fn transfer_dust_removal_tst1_should_work() { assert_eq!(Balances::free_balance(account_id(1)), 1050); // Verify the events - assert_eq!(System::events().len(), 14); + assert_eq!(System::events().len(), 15); System::assert_has_event(RuntimeEvent::Balances(crate::Event::Transfer { from: account_id(2), @@ -99,7 +99,7 @@ fn transfer_dust_removal_tst2_should_work() { // Dust balance is deposited to account 1 assert_eq!(Balances::free_balance(account_id(1)), 1000 + 450 + 50); // Verify the events - assert_eq!(System::events().len(), 12); + assert_eq!(System::events().len(), 13); System::assert_has_event(RuntimeEvent::Balances(crate::Event::Transfer { from: account_id(2), @@ -149,7 +149,7 @@ fn repatriating_reserved_balance_dust_removal_should_work() { assert_eq!(Balances::free_balance(account_id(1)), 1500); // Verify the events - assert_eq!(System::events().len(), 12); + assert_eq!(System::events().len(), 13); System::assert_has_event(RuntimeEvent::Balances(crate::Event::Transfer { from: account_id(2), diff --git a/pallets/mining-rewards/src/mock.rs b/pallets/mining-rewards/src/mock.rs index a4bbd286..1b4529a3 100644 --- a/pallets/mining-rewards/src/mock.rs +++ b/pallets/mining-rewards/src/mock.rs @@ -11,7 +11,7 @@ use sp_runtime::{ app_crypto::sp_core, testing::H256, traits::{BlakeTwo256, IdentityLookup}, - BuildStorage, Digest, DigestItem, + BuildStorage, DigestItem, }; // Configure a mock runtime to test the pallet @@ -127,13 +127,11 @@ pub fn new_test_ext() -> sp_io::TestExternalities { // Helper function to create a block digest with a miner pre-runtime digest pub fn set_miner_digest(miner: sp_core::crypto::AccountId32) { + // reset logs let miner_bytes = miner.encode(); let pre_digest = DigestItem::PreRuntime(POW_ENGINE_ID, miner_bytes); - let digest = Digest { logs: vec![pre_digest] }; - // Set the digest in the system - System::reset_events(); - System::initialize(&1, &sp_core::H256::default(), &digest); + System::deposit_log(pre_digest); } // Helper function to run a block diff --git a/pallets/mining-rewards/src/tests.rs b/pallets/mining-rewards/src/tests.rs index 04d9ef13..bd4604d4 100644 --- a/pallets/mining-rewards/src/tests.rs +++ b/pallets/mining-rewards/src/tests.rs @@ -1,6 +1,6 @@ use crate::{mock::*, weights::WeightInfo, Event}; use frame_support::traits::{Currency, Hooks}; -use sp_runtime::traits::AccountIdConversion; +use sp_runtime::{testing::Digest, traits::AccountIdConversion}; const UNIT: u128 = 1_000_000_000_000; @@ -153,11 +153,15 @@ fn different_miners_get_different_rewards() { assert_eq!(Balances::free_balance(miner()), balance_after_block_1); // Block 2 - Second miner - System::set_block_number(2); + let block_1 = System::finalize(); + // reset logs and go to block 2 + System::initialize(&2, &block_1.hash(), &Digest { logs: vec![] }); set_miner_digest(miner2()); MiningRewards::collect_transaction_fees(20); MiningRewards::on_finalize(2); + println!("Balnce {}", Balances::free_balance(miner())); + // Check second miner balance // Current implementation: miner gets base reward (50) + all fees (20) assert_eq!( diff --git a/pallets/wormhole/Cargo.toml b/pallets/wormhole/Cargo.toml index 54dba6aa..78e624e7 100644 --- a/pallets/wormhole/Cargo.toml +++ b/pallets/wormhole/Cargo.toml @@ -16,6 +16,8 @@ hex = { workspace = true, features = ["alloc"], optional = true } lazy_static.workspace = true log.workspace = true pallet-balances.workspace = true +qp-header = { workspace = true, features = ["serde"] } +qp-poseidon.workspace = true qp-wormhole.workspace = true qp-wormhole-circuit = { workspace = true, default-features = false } qp-wormhole-verifier = { workspace = true, default-features = false } @@ -29,6 +31,15 @@ sp-runtime.workspace = true [dev-dependencies] hex = { workspace = true, features = ["alloc"] } +qp-dilithium-crypto = { workspace = true, features = ["std"] } +qp-plonky2 = { workspace = true, default-features = false } +qp-poseidon-core.workspace = true +qp-wormhole-circuit.workspace = true +qp-wormhole-circuit-builder = { workspace = true, default-features = false } +qp-wormhole-prover.workspace = true +qp-zk-circuits-common.workspace = true +sp-state-machine = { path = "../../primitives/state-machine" } +sp-trie.workspace = true [features] default = ["std"] @@ -48,6 +59,8 @@ std = [ "lazy_static/spin_no_std", "log/std", "pallet-balances/std", + "qp-header/std", + "qp-poseidon/std", "qp-wormhole-circuit/std", "qp-wormhole-verifier/std", "qp-wormhole/std", diff --git a/pallets/wormhole/common.bin b/pallets/wormhole/common.bin index 81c43fbdb560a05e0ae97102c2d9c88e9ce85afd..9232df9c3bdbf92f77f73c7a28db999bdfafe660 100644 GIT binary patch delta 939 zcmZ9LO-{ow7zLdMN@$@i(9()OQAAm>;zHbl4GUj@N?2^U143MYi*N-b7HkoD&Kt>U zRCV6ijuZQH*3Hl8`6=B`(reTH29w}xmELdD=P-RW%gRl?4c7KLt>@`?5&QO?4K{YT zYlj0n9NOWY9q!xVsPntv5XK$=S=52sK|lS?BX7miAy0!$4w)1ldR92K!(%&KbRL64 zIPm~@W``Gccxi`Mc6e=vD?1B*%-(MDkvnD|H`QD3oN+3TN=(_c5#yKd`ZTif`My#r z3CAUrl6#mb_aM24qm+A)imldjlwF#bh0~#qvPQg*+L=J)Fep Vec { hex::decode(hex_proof.trim()).expect("Failed to decode hex proof") } -#[benchmarks( - where - T: Send + Sync, - T: Config, - BalanceOf: Into<<::Currency as Inspect>::Balance>, -)] +#[benchmarks] mod benchmarks { use super::*; @@ -27,6 +26,7 @@ mod benchmarks { fn verify_wormhole_proof() -> Result<(), BenchmarkError> { let proof_bytes = get_benchmark_proof(); + // Parse the proof to get public inputs let verifier = crate::get_wormhole_verifier() .map_err(|_| BenchmarkError::Stop("Verifier not available"))?; @@ -39,28 +39,43 @@ mod benchmarks { let public_inputs = PublicCircuitInputs::try_from(&proof) .map_err(|_| BenchmarkError::Stop("Invalid public inputs"))?; + // Extract values from public inputs let nullifier_bytes = *public_inputs.nullifier; + let block_number_u32 = public_inputs.block_number; + let block_hash_bytes: [u8; 32] = (*public_inputs.block_hash) + .try_into() + .map_err(|_| BenchmarkError::Stop("Invalid block hash length"))?; + // Ensure nullifier hasn't been used ensure!( !UsedNullifiers::::contains_key(nullifier_bytes), BenchmarkError::Stop("Nullifier already used") ); + // Verify the proof is valid (sanity check) verifier .verify(proof) .map_err(|_| BenchmarkError::Stop("Proof verification failed"))?; - let block_number = frame_system::Pallet::::block_number(); + // Set up storage to match the proof's public inputs: + // Set current block number to be >= proof's block_number + let block_number: BlockNumberFor = block_number_u32.into(); + frame_system::Pallet::::set_block_number(block_number + 1u32.into()); + + // Override block hash to match proof's block_hash + let block_hash = T::Hash::decode(&mut &block_hash_bytes[..]) + .map_err(|_| BenchmarkError::Stop("Failed to decode block hash"))?; + frame_system::BlockHash::::insert(block_number, block_hash); #[extrinsic_call] - verify_wormhole_proof(RawOrigin::None, proof_bytes, block_number); + verify_wormhole_proof(RawOrigin::None, proof_bytes); - Ok(()) - } + // Verify nullifier was marked as used + ensure!( + UsedNullifiers::::contains_key(nullifier_bytes), + BenchmarkError::Stop("Nullifier should be marked as used after verification") + ); - impl_benchmark_test_suite! { - Pallet, - crate::mock::new_test_ext(), - crate::mock::Test, + Ok(()) } } diff --git a/pallets/wormhole/src/lib.rs b/pallets/wormhole/src/lib.rs index 36457741..86d8281a 100644 --- a/pallets/wormhole/src/lib.rs +++ b/pallets/wormhole/src/lib.rs @@ -105,11 +105,7 @@ pub mod pallet { impl Pallet { #[pallet::call_index(0)] #[pallet::weight(::WeightInfo::verify_wormhole_proof())] - pub fn verify_wormhole_proof( - origin: OriginFor, - proof_bytes: Vec, - block_number: BlockNumberFor, - ) -> DispatchResult { + pub fn verify_wormhole_proof(origin: OriginFor, proof_bytes: Vec) -> DispatchResult { ensure_none(origin)?; let verifier = @@ -133,6 +129,10 @@ pub mod pallet { Error::::NullifierAlreadyUsed ); + // Extract the block number from public inputs + let block_number = BlockNumberFor::::try_from(public_inputs.block_number) + .map_err(|_| Error::::InvalidPublicInputs)?; + // Get the block hash for the specified block number let block_hash = frame_system::Pallet::::block_hash(block_number); @@ -145,36 +145,11 @@ pub mod pallet { let default_hash = T::Hash::default(); ensure!(block_hash != default_hash, Error::::BlockNotFound); - // Get the storage root for the specified block - let storage_root = sp_io::storage::root(sp_runtime::StateVersion::V1); - - let root_hash = public_inputs.root_hash; - let storage_root_bytes = storage_root.as_slice(); - - // Compare the root_hash from the proof with the actual storage root - // Skip storage root validation in test and benchmark environments since proofs - // may have been generated with different state - #[cfg(not(any(test, feature = "runtime-benchmarks")))] - if root_hash.as_ref() != storage_root_bytes { - log::warn!( - target: "wormhole", - "Storage root mismatch for block {:?}: expected {:?}, got {:?}", - block_number, - root_hash.as_ref(), - storage_root_bytes - ); - return Err(Error::::StorageRootMismatch.into()); - } - - #[cfg(any(test, feature = "runtime-benchmarks"))] - { - let _root_hash = root_hash; - let _storage_root_bytes = storage_root_bytes; - log::debug!( - target: "wormhole", - "Skipping storage root validation in test/benchmark environment" - ); - } + // Ensure that the block hash from storage matches the one in public inputs + ensure!( + block_hash.as_ref() == public_inputs.block_hash.as_ref(), + Error::::InvalidPublicInputs + ); verifier.verify(proof.clone()).map_err(|_| Error::::VerificationFailed)?; diff --git a/pallets/wormhole/src/mock.rs b/pallets/wormhole/src/mock.rs index 4c764f26..c902099c 100644 --- a/pallets/wormhole/src/mock.rs +++ b/pallets/wormhole/src/mock.rs @@ -4,11 +4,10 @@ use frame_support::{ traits::{ConstU32, Everything}, weights::IdentityFee, }; +use frame_system::mocking::MockUncheckedExtrinsic; +use qp_poseidon::PoseidonHasher; use sp_core::H256; -use sp_runtime::{ - traits::{BlakeTwo256, IdentityLookup}, - BuildStorage, -}; +use sp_runtime::{traits::IdentityLookup, BuildStorage}; // --- MOCK RUNTIME --- construct_runtime!( @@ -21,7 +20,10 @@ construct_runtime!( pub type Balance = u128; pub type AccountId = sp_core::crypto::AccountId32; -pub type Block = frame_system::mocking::MockBlock; +pub type Block = sp_runtime::generic::Block< + qp_header::Header, + MockUncheckedExtrinsic, +>; /// Helper function to convert a u64 to an AccountId32 pub fn account_id(id: u64) -> AccountId { @@ -46,10 +48,10 @@ impl frame_system::Config for Test { type RuntimeTask = (); type Nonce = u64; type Hash = H256; - type Hashing = BlakeTwo256; + type Hashing = PoseidonHasher; type AccountId = AccountId; type Lookup = IdentityLookup; - type Block = Block; + type Block = Block; type BlockHashCount = BlockHashCount; type DbWeight = (); type Version = (); @@ -108,12 +110,15 @@ impl pallet_wormhole::Config for Test { } // Helper function to build a genesis configuration -pub fn new_test_ext() -> sp_io::TestExternalities { +pub fn new_test_ext() -> sp_state_machine::TestExternalities { let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); - pallet_balances::GenesisConfig:: { balances: vec![] } - .assimilate_storage(&mut t) - .unwrap(); + let endowment = 1e18 as Balance; + pallet_balances::GenesisConfig:: { + balances: vec![(account_id(1), endowment), (account_id(2), endowment)], + } + .assimilate_storage(&mut t) + .unwrap(); t.into() } diff --git a/pallets/wormhole/src/tests.rs b/pallets/wormhole/src/tests.rs index a96fca74..ae2a7de1 100644 --- a/pallets/wormhole/src/tests.rs +++ b/pallets/wormhole/src/tests.rs @@ -1,211 +1,262 @@ #[cfg(test)] mod wormhole_tests { - use crate::{get_wormhole_verifier, mock::*, weights, Config, Error, WeightInfo}; - use frame_support::{assert_noop, assert_ok, weights::WeightToFee}; - use qp_wormhole_circuit::inputs::PublicCircuitInputs; + use crate::{get_wormhole_verifier, mock::*}; + use codec::Encode; + use frame_support::{ + assert_ok, + traits::fungible::{Inspect, Mutate}, + }; + use plonky2::plonk::circuit_data::CircuitConfig; + use qp_poseidon::PoseidonHasher; + use qp_wormhole_circuit::{ + inputs::{CircuitInputs, PrivateCircuitInputs, PublicCircuitInputs}, + nullifier::Nullifier, + }; + use qp_wormhole_prover::WormholeProver; use qp_wormhole_verifier::ProofWithPublicInputs; - use sp_runtime::Perbill; + use qp_zk_circuits_common::{ + circuit::{C, F}, + storage_proof::prepare_proof_for_circuit, + utils::{digest_felts_to_bytes, BytesDigest, Digest}, + }; + use sp_runtime::{traits::Header, DigestItem}; // Helper function to generate proof and inputs for - fn get_test_proof() -> Vec { - let hex_proof = include_str!("../proof_from_bins.hex"); - hex::decode(hex_proof.trim()).expect("Failed to decode hex proof") + fn generate_proof(inputs: CircuitInputs) -> ProofWithPublicInputs { + let config = CircuitConfig::standard_recursion_config(); + let prover = WormholeProver::new(config); + let prover_next = prover.commit(&inputs).expect("proof failed"); + let proof = prover_next.prove().expect("valid proof"); + proof } #[test] - fn test_verifier_availability() { - new_test_ext().execute_with(|| { - let verifier = get_wormhole_verifier(); - assert!(verifier.is_ok(), "Verifier should be available in tests"); + fn test_wormhole_transfer_proof_generation() { + // Setup accounts + let alice = account_id(1); + let secret: BytesDigest = [1u8; 32].try_into().expect("valid secret"); + let unspendable_account = + qp_wormhole_circuit::unspendable_account::UnspendableAccount::from_secret(secret) + .account_id; + let unspendable_account_bytes_digest = digest_felts_to_bytes(unspendable_account); + let unspendable_account_bytes: [u8; 32] = unspendable_account_bytes_digest + .as_ref() + .try_into() + .expect("BytesDigest is always 32 bytes"); + let unspendable_account_id = AccountId::new(unspendable_account_bytes); + let exit_account_id = AccountId::new([42u8; 32]); + let funding_amount = 1_000_000_000_001u128; - // Verify the verifier can be used - let verifier = verifier.unwrap(); - // Check that the circuit data is valid by checking gates - assert!(!verifier.circuit_data.common.gates.is_empty(), "Circuit should have gates"); - }); - } + let mut ext = new_test_ext(); - #[test] - fn test_verify_empty_proof_fails() { - new_test_ext().execute_with(|| { - let empty_proof = vec![]; - let block_number = frame_system::Pallet::::block_number(); - assert_noop!( - Wormhole::verify_wormhole_proof(RuntimeOrigin::none(), empty_proof, block_number), - Error::::ProofDeserializationFailed - ); - }); - } + // Execute the transfer and get the header + let (storage_key, state_root, leaf_hash, event_transfer_count, header) = + ext.execute_with(|| { + System::set_block_number(1); - #[test] - fn test_verify_invalid_proof_data_fails() { - new_test_ext().execute_with(|| { - // Create some random bytes that will fail deserialization - let invalid_proof = vec![1u8; 100]; - let block_number = frame_system::Pallet::::block_number(); - assert_noop!( - Wormhole::verify_wormhole_proof(RuntimeOrigin::none(), invalid_proof, block_number), - Error::::ProofDeserializationFailed - ); - }); - } + // Add dummy digest items to match expected format + let pre_runtime_data = vec![ + 233, 182, 183, 107, 158, 1, 115, 19, 219, 126, 253, 86, 30, 208, 176, 70, 21, + 45, 180, 229, 9, 62, 91, 4, 6, 53, 245, 52, 48, 38, 123, 225, + ]; + let seal_data = vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 77, 142, + ]; - #[test] - fn test_verify_valid_proof() { - new_test_ext().execute_with(|| { - let proof = get_test_proof(); - let block_number = frame_system::Pallet::::block_number(); - assert_ok!(Wormhole::verify_wormhole_proof(RuntimeOrigin::none(), proof, block_number)); - }); - } + System::deposit_log(DigestItem::PreRuntime(*b"pow_", pre_runtime_data)); + System::deposit_log(DigestItem::Seal(*b"pow_", seal_data)); - #[test] - fn test_verify_invalid_inputs() { - new_test_ext().execute_with(|| { - let mut proof = get_test_proof(); - let block_number = frame_system::Pallet::::block_number(); + assert_ok!(Balances::mint_into(&unspendable_account_id, funding_amount)); + assert_ok!(Balances::transfer_keep_alive( + frame_system::RawOrigin::Signed(alice.clone()).into(), + unspendable_account_id.clone(), + funding_amount, + )); - if let Some(byte) = proof.get_mut(0) { - *byte = !*byte; // Flip bits to make proof invalid - } + let transfer_count = pallet_balances::TransferCount::::get(); + let event_transfer_count = transfer_count - 1; - assert_noop!( - Wormhole::verify_wormhole_proof(RuntimeOrigin::none(), proof, block_number), - Error::::VerificationFailed - ); - }); - } + let leaf_hash = PoseidonHasher::hash_storage::( + &( + event_transfer_count, + alice.clone(), + unspendable_account_id.clone(), + funding_amount, + ) + .encode(), + ); - #[test] - fn test_wormhole_exit_balance_and_fees() { - new_test_ext().execute_with(|| { - let proof = get_test_proof(); - let expected_exit_account = account_id(8226349481601990196u64); - - // Parse the proof to get expected funding amount - let verifier = get_wormhole_verifier().expect("Verifier should be available"); - let proof_with_inputs = ProofWithPublicInputs::from_bytes(proof.clone(), &verifier.circuit_data.common) - .expect("Should be able to parse test proof"); - - let public_inputs = PublicCircuitInputs::try_from(&proof_with_inputs) - .expect("Should be able to parse public inputs"); - - let expected_funding_amount = public_inputs.funding_amount; - - // Calculate expected fees (matching lib.rs logic exactly) - let weight = as WeightInfo>::verify_wormhole_proof(); - let weight_fee: u128 = ::WeightToFee::weight_to_fee(&weight); - let volume_fee = Perbill::from_rational(1u32, 1000u32) * expected_funding_amount; - let expected_total_fee = weight_fee.saturating_add(volume_fee); - let expected_net_balance_increase = expected_funding_amount.saturating_sub(expected_total_fee); - - let initial_exit_balance = - pallet_balances::Pallet::::free_balance(&expected_exit_account); - - let block_number = frame_system::Pallet::::block_number(); - let result = - Wormhole::verify_wormhole_proof(RuntimeOrigin::none(), proof, block_number); - assert_ok!(result); - - let final_exit_balance = - pallet_balances::Pallet::::free_balance(&expected_exit_account); - - let balance_increase = final_exit_balance - initial_exit_balance; - - // Assert the exact expected balance increase - assert_eq!( - balance_increase - , expected_net_balance_increase, - "Balance increase should equal funding amount minus fees. Funding: {}, Fees: {}, Expected net: {}, Actual: {}" - , expected_funding_amount - , expected_total_fee - , expected_net_balance_increase - , balance_increase - ); - - // NOTE: In this mock/test context, the OnUnbalanced handler is not triggered for this withdrawal. - // In production, the fee will be routed to the handler as expected. - }); - } + let proof_address = pallet_balances::TransferProof::::hashed_key_for(&( + event_transfer_count, + alice.clone(), + unspendable_account_id.clone(), + funding_amount, + )); + let mut storage_key = proof_address; + storage_key.extend_from_slice(&leaf_hash); - #[test] - fn test_nullifier_already_used() { + let header = System::finalize(); + let state_root = *header.state_root(); + + (storage_key, state_root, leaf_hash, event_transfer_count, header) + }); + + // Generate a storage proof for the specific storage key + use sp_state_machine::prove_read; + let proof = prove_read(ext.as_backend(), &[&storage_key]) + .expect("failed to generate storage proof"); + + let proof_nodes_vec: Vec> = proof.iter_nodes().map(|n| n.to_vec()).collect(); + + // Prepare the storage proof for the circuit + let processed_storage_proof = + prepare_proof_for_circuit(proof_nodes_vec, hex::encode(&state_root), leaf_hash) + .expect("failed to prepare proof for circuit"); + + // Build the header components + let parent_hash = *header.parent_hash(); + let extrinsics_root = *header.extrinsics_root(); + let digest = header.digest().encode(); + let digest_array: [u8; 110] = digest.try_into().expect("digest should be 110 bytes"); + let block_number: u32 = (*header.number()).try_into().expect("block number fits in u32"); + + // Compute block hash + let block_hash = header.hash(); + + // Assemble circuit inputs + let circuit_inputs = CircuitInputs { + private: PrivateCircuitInputs { + secret, + transfer_count: event_transfer_count, + funding_account: BytesDigest::try_from(alice.as_ref() as &[u8]) + .expect("account is 32 bytes"), + storage_proof: processed_storage_proof, + unspendable_account: Digest::from(unspendable_account).into(), + state_root: BytesDigest::try_from(state_root.as_ref()) + .expect("state root is 32 bytes"), + extrinsics_root: BytesDigest::try_from(extrinsics_root.as_ref()) + .expect("extrinsics root is 32 bytes"), + digest: digest_array, + }, + public: PublicCircuitInputs { + funding_amount, + nullifier: Nullifier::from_preimage(secret, event_transfer_count).hash.into(), + exit_account: BytesDigest::try_from(exit_account_id.as_ref() as &[u8]) + .expect("account is 32 bytes"), + block_hash: BytesDigest::try_from(block_hash.as_ref()) + .expect("block hash is 32 bytes"), + parent_hash: BytesDigest::try_from(parent_hash.as_ref()) + .expect("parent hash is 32 bytes"), + block_number, + }, + }; + + // Generate the ZK proof + let proof = generate_proof(circuit_inputs); + + // Verify the proof can be parsed + let public_inputs = + PublicCircuitInputs::try_from(&proof).expect("failed to parse public inputs"); + + // Verify that the public inputs match what we expect + assert_eq!(public_inputs.funding_amount, funding_amount); + assert_eq!( + public_inputs.exit_account, + BytesDigest::try_from(exit_account_id.as_ref() as &[u8]).unwrap() + ); + + // Verify the proof using the verifier + let verifier = get_wormhole_verifier().expect("verifier should be available"); + verifier.verify(proof.clone()).expect("proof should verify"); + + // Serialize the proof to bytes for extrinsic testing + let proof_bytes = proof.to_bytes(); + + // Now test the extrinsic in a new environment new_test_ext().execute_with(|| { - let proof = get_test_proof(); - let block_number = frame_system::Pallet::::block_number(); + // Set up the blockchain state to have block 1 + System::set_block_number(1); - // First verification should succeed + // Add the same digest items + let pre_runtime_data = vec![ + 233, 182, 183, 107, 158, 1, 115, 19, 219, 126, 253, 86, 30, 208, 176, 70, 21, 45, + 180, 229, 9, 62, 91, 4, 6, 53, 245, 52, 48, 38, 123, 225, + ]; + let seal_data = vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 30, 77, 142, + ]; + + System::deposit_log(DigestItem::PreRuntime(*b"pow_", pre_runtime_data)); + System::deposit_log(DigestItem::Seal(*b"pow_", seal_data)); + + // Execute the same transfer to recreate the exact state + assert_ok!(Balances::mint_into(&unspendable_account_id, funding_amount)); + assert_ok!(Balances::transfer_keep_alive( + frame_system::RawOrigin::Signed(alice.clone()).into(), + unspendable_account_id.clone(), + funding_amount, + )); + + // Finalize the block to get the same header and store the block hash + let block_1_header = System::finalize(); + + // Initialize block 2 to store block 1's hash + System::reset_events(); + System::initialize(&2, &block_1_header.hash(), block_1_header.digest()); + + // Check exit account balance before verification + let balance_before = Balances::balance(&exit_account_id); + assert_eq!(balance_before, 0); + + // Call the verify_wormhole_proof extrinsic assert_ok!(Wormhole::verify_wormhole_proof( - RuntimeOrigin::none(), - proof.clone(), - block_number + frame_system::RawOrigin::None.into(), + proof_bytes.clone() )); - // Second verification with same proof should fail due to nullifier reuse - assert_noop!( - Wormhole::verify_wormhole_proof(RuntimeOrigin::none(), proof, block_number), - Error::::NullifierAlreadyUsed + // Check that the exit account received the funds (minus fees) + let balance_after = Balances::balance(&exit_account_id); + + // The balance should be funding_amount minus fees + // Weight fee + 0.1% volume fee + assert!(balance_after > 0, "Exit account should have received funds"); + assert!( + balance_after < funding_amount, + "Exit account balance should be less than funding amount due to fees" ); }); - } - #[test] - fn test_verify_future_block_number_fails() { + // Test that proof fails when state doesn't match new_test_ext().execute_with(|| { - let proof = get_test_proof(); - let current_block = frame_system::Pallet::::block_number(); - let future_block = current_block + 1; + // Set up block 1 but DON'T recreate the exact same state + System::set_block_number(1); - assert_noop!( - Wormhole::verify_wormhole_proof(RuntimeOrigin::none(), proof, future_block), - Error::::InvalidBlockNumber - ); - }); - } + // Add different digest items with same 110-byte format but different content + let pre_runtime_data = vec![1u8; 32]; // Different data + let seal_data = vec![2u8; 64]; // Different data - #[test] - fn test_verify_storage_root_mismatch_fails() { - new_test_ext().execute_with(|| { - // This test would require a proof with a different root_hash than the current storage - // root - let proof = get_test_proof(); - let block_number = frame_system::Pallet::::block_number(); - - let result = - Wormhole::verify_wormhole_proof(RuntimeOrigin::none(), proof, block_number); - - // This should either succeed (if root_hash matches) or fail with StorageRootMismatch - // We can't easily create a proof with wrong root_hash in tests, so we just verify - // that the validation logic is executed - assert!(result.is_ok() || result.is_err()); - }); - } + System::deposit_log(DigestItem::PreRuntime(*b"pow_", pre_runtime_data)); + System::deposit_log(DigestItem::Seal(*b"pow_", seal_data)); - #[test] - fn test_verify_with_different_block_numbers() { - new_test_ext().execute_with(|| { - let proof = get_test_proof(); - let current_block = frame_system::Pallet::::block_number(); + // Finalize block 1 with different state + let different_header = System::finalize(); - // Test with current block (should succeed) - assert_ok!(Wormhole::verify_wormhole_proof( - RuntimeOrigin::none(), - proof.clone(), - current_block - )); + // Initialize block 2 + System::reset_events(); + System::initialize(&2, &different_header.hash(), different_header.digest()); - // Test with a recent block (should succeed if it exists) - if current_block > 1 { - let recent_block = current_block - 1; - let result = Wormhole::verify_wormhole_proof( - RuntimeOrigin::none(), - proof.clone(), - recent_block, - ); - // This might succeed or fail depending on whether the block exists - // and whether the storage root matches - assert!(result.is_ok() || result.is_err()); - } + // Try to use the proof with the original header (which has different block hash) + let result = Wormhole::verify_wormhole_proof( + frame_system::RawOrigin::None.into(), + proof_bytes.clone(), + ); + + // This should fail because the block hash in the proof doesn't match + assert!(result.is_err(), "Proof verification should fail with mismatched state"); }); } } diff --git a/pallets/wormhole/src/weights.rs b/pallets/wormhole/src/weights.rs index b1516fa8..8d06df5b 100644 --- a/pallets/wormhole/src/weights.rs +++ b/pallets/wormhole/src/weights.rs @@ -18,10 +18,10 @@ //! Autogenerated weights for `pallet_wormhole` //! -//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 46.2.0 -//! DATE: 2025-07-22, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 49.1.0 +//! DATE: 2025-11-27, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `Veronikas-MacBook-Air.local`, CPU: `` +//! HOSTNAME: `Dastans-MacBook-Pro.local`, CPU: `` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` // Executed Command: @@ -70,10 +70,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `Balances::TransferProof` (`max_values`: None, `max_size`: Some(32), added: 2507, mode: `MaxEncodedLen`) fn verify_wormhole_proof() -> Weight { // Proof Size summary in bytes: - // Measured: `424` + // Measured: `256` // Estimated: `3593` - // Minimum execution time: 17_162_000_000 picoseconds. - Weight::from_parts(17_576_000_000, 3593) + // Minimum execution time: 14_318_000_000 picoseconds. + Weight::from_parts(14_468_000_000, 3593) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } @@ -93,10 +93,10 @@ impl WeightInfo for () { /// Proof: `Balances::TransferProof` (`max_values`: None, `max_size`: Some(32), added: 2507, mode: `MaxEncodedLen`) fn verify_wormhole_proof() -> Weight { // Proof Size summary in bytes: - // Measured: `424` + // Measured: `256` // Estimated: `3593` - // Minimum execution time: 17_162_000_000 picoseconds. - Weight::from_parts(17_576_000_000, 3593) + // Minimum execution time: 14_318_000_000 picoseconds. + Weight::from_parts(14_468_000_000, 3593) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } diff --git a/pallets/wormhole/verifier.bin b/pallets/wormhole/verifier.bin index 6f7404b667329a8017f4474738e1864f35740b79..121eb2b02d5badfed2fbdcc2c29e9d7a7ffec909 100644 GIT binary patch literal 552 zcmV+@0@wWn000000001{fgdmlpg^a6wNw|p^n-O4(K!Freq^lh(_{C)mkWt!KDUW$ zE?PNjBmiDLoZ{RA6jg7PSxr{BizIHJW~f*HO{9pc&uXqZjrcId3x@z<4KDbJIfrOx z3FS>VcY)vNi24;Io?*Yn7P+jd*v`UX^%^YUdrQkHvuml_sF!}-T;4qK!GOD-UtYyb zZfVj=Uoi;ph0JLtv6@F>U9H=K?8W=9Um3#E&28RBK*imrCh|tLm``n>T0iw%B&g~U zLHCzUTEj;0(O=t!{eN>!%#+cA6Jn(-a7yeFbw!h6R3ADGoPx(lp`OJ9E9cB0yYUCvl07q z2E9l&(l;2u;gF8HIhzbxcX&Omio1+M9&4fZM%uPN(d_}gXv3BeaZh4RY>l>s?G9bk z&ekwT5?b=MPr;MuP0MkQ%(e&ddMhGQVUJ5L>W-xNqo|R;gFc|_#5f`v2^nITGT}?g z)0Gs3-u}JJUGbhZm!M0U++70}*uA7{Dveh_&fT$~77p?a*$|iW9q^4&?iN?J93InB z_r-UE{N^JNuC9aIJk+lv!L++*S;2N+gcngm8J6`v+%Xqm<$jET;|f!$tR5)JxbPf~ qKk68O>uJ)jvp@aiEmphF1 zt=EwEV3`!C%DOHzBcO`Nn}z?iC(n1J%E~RK{>@7be>23SGfkcp+}r6(aMku|TbmAU zXrvkRZ{VGl_&}lwx_azzDm4Q*Gdc-%leoNxAW=R=MBgK|-WN=A{TuL#eQa`NHwWXG zl?|0r;!mqt+Z>~oULuS3xo$NuM1my_ktngy@30kh)^C;PHLCwM6LD`H#8}24RErRi zenXR9((a41u@*5HO9_1Oa?B~~$@I(eA820>@lPW(Kqgl$_O z8ZgGiQ^L7iT)4ai5L7ua~{&D*+DfTc5EAyADHBW`zC`Z&G<3L;3+W+ ztfy#C=~AZ_jGRSU#(7p{k{q-A;z0H12G=@*vtvHf49ARUh_WGBEmm2P8 qXKRDCF!y7cTf!)t9w)1yPwkj)P{8vPStKjYr_ZO+O{pLv{XmN~s0O6~ diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 65ba6ce7..078a04d8 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -44,6 +44,7 @@ pallet-transaction-payment-rpc-runtime-api.workspace = true pallet-treasury.workspace = true pallet-utility.workspace = true pallet-vesting.workspace = true +pallet-wormhole.workspace = true primitive-types.workspace = true qp-dilithium-crypto.workspace = true qp-header = { workspace = true, features = ["serde"] } @@ -112,6 +113,7 @@ std = [ "pallet-treasury/std", "pallet-utility/std", "pallet-vesting/std", + "pallet-wormhole/std", "primitive-types/std", "qp-dilithium-crypto/full_crypto", "qp-dilithium-crypto/std", @@ -162,6 +164,7 @@ runtime-benchmarks = [ "pallet-transaction-payment/runtime-benchmarks", "pallet-treasury/runtime-benchmarks", "pallet-vesting/runtime-benchmarks", + "pallet-wormhole/runtime-benchmarks", "sp-runtime/runtime-benchmarks", ] @@ -181,6 +184,7 @@ try-runtime = [ "pallet-transaction-payment/try-runtime", "pallet-treasury/try-runtime", "pallet-vesting/try-runtime", + "pallet-wormhole/try-runtime", "sp-runtime/try-runtime", ] diff --git a/runtime/src/benchmarks.rs b/runtime/src/benchmarks.rs index c670981c..fe021308 100644 --- a/runtime/src/benchmarks.rs +++ b/runtime/src/benchmarks.rs @@ -34,4 +34,5 @@ frame_benchmarking::define_benchmarks!( [pallet_mining_rewards, MiningRewards] [pallet_scheduler, Scheduler] [pallet_qpow, QPoW] + [pallet_wormhole, Wormhole] ); diff --git a/runtime/src/configs/mod.rs b/runtime/src/configs/mod.rs index e76063a3..693319cf 100644 --- a/runtime/src/configs/mod.rs +++ b/runtime/src/configs/mod.rs @@ -621,3 +621,14 @@ impl TryFrom for pallet_assets::Call { } } } + +parameter_types! { + pub WormholeMintingAccount: AccountId = PalletId(*b"wormhole").into_account_truncating(); +} + +impl pallet_wormhole::Config for Runtime { + type MintingAccount = WormholeMintingAccount; + type WeightInfo = (); + type Currency = Balances; + type WeightToFee = IdentityFee; +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 7c54c2b5..2dbbb1bf 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -275,4 +275,7 @@ mod runtime { #[runtime::pallet_index(22)] pub type AssetsHolder = pallet_assets_holder; + + #[runtime::pallet_index(23)] + pub type Wormhole = pallet_wormhole; } From 36e47fd47e7e77fc433e695fc25e4d1222870058 Mon Sep 17 00:00:00 2001 From: Cezary Olborski Date: Fri, 12 Dec 2025 10:06:47 +0800 Subject: [PATCH 03/27] feat: qp-header for Planck release (#338) * no circuit padding hasher for block header * *use custom hasher for header that encodes the pre-image in a felt aligned manner. * *bespoke header hasher * *patch bug with hash header fall back * *replace custom poseidon header hasher on generic header with a fork of header that has a custom hasher that overrides default on the header trait. * *rmv commented out impl of prior hash method * Update primitives/header/src/lib.rs Co-authored-by: Dastan <88332432+dastansam@users.noreply.github.com> * fixed tests * Use inherent struct method * Update Cargo.toml --------- Co-authored-by: Ethan Co-authored-by: illuzen Co-authored-by: Dastan <88332432+dastansam@users.noreply.github.com> --- Cargo.lock | 19 ++ Cargo.toml | 4 + primitives/header/Cargo.toml | 40 +++ primitives/header/src/lib.rs | 346 ++++++++++++++++++++++++++ runtime/Cargo.toml | 3 +- runtime/src/genesis_config_presets.rs | 1 + runtime/src/lib.rs | 5 +- 7 files changed, 414 insertions(+), 4 deletions(-) create mode 100644 primitives/header/Cargo.toml create mode 100644 primitives/header/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 8c992bea..35560863 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8882,6 +8882,24 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "qp-header" +version = "0.1.0" +dependencies = [ + "hex", + "log", + "p3-field", + "p3-goldilocks", + "parity-scale-codec", + "qp-poseidon", + "qp-poseidon-core", + "scale-info", + "serde", + "serde_json", + "sp-core", + "sp-runtime", +] + [[package]] name = "qp-plonky2" version = "1.1.1" @@ -9192,6 +9210,7 @@ dependencies = [ "parity-scale-codec", "primitive-types 0.13.1", "qp-dilithium-crypto", + "qp-header", "qp-poseidon", "qp-scheduler", "scale-info", diff --git a/Cargo.toml b/Cargo.toml index f57e846a..b6c3abde 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ members = [ "primitives/consensus/pow", "primitives/consensus/qpow", "primitives/dilithium-crypto", + "primitives/header", "primitives/scheduler", "primitives/state-machine", "primitives/trie", @@ -84,6 +85,8 @@ names = { version = "0.14.0", default-features = false } nohash-hasher = { version = "0.2.0" } num-traits = { version = "0.2", default-features = false, features = ["libm"] } once_cell = { version = "1.21.3" } +p3-field = { version = "0.3.0" } +p3-goldilocks = { version = "0.3.0" } parking_lot = { version = "0.12.1", default-features = false } partial_sort = { version = "0.2.0" } paste = { version = "1.0.15", default-features = false } @@ -135,6 +138,7 @@ pallet-reversible-transfers = { path = "./pallets/reversible-transfers", default pallet-scheduler = { path = "./pallets/scheduler", default-features = false } pallet-wormhole = { path = "./pallets/wormhole", default-features = false } qp-dilithium-crypto = { path = "./primitives/dilithium-crypto", version = "0.2.0", default-features = false } +qp-header = { path = "./primitives/header", default-features = false } qp-scheduler = { path = "./primitives/scheduler", default-features = false } qp-wormhole = { path = "./primitives/wormhole", default-features = false } qpow-math = { path = "./qpow-math", default-features = false } diff --git a/primitives/header/Cargo.toml b/primitives/header/Cargo.toml new file mode 100644 index 00000000..a164947d --- /dev/null +++ b/primitives/header/Cargo.toml @@ -0,0 +1,40 @@ +[package] +authors.workspace = true +description = "Fork of sp-runtime's Header type with a custom hash function that's felt aligned for our wormhole circuits" +edition.workspace = true +homepage.workspace = true +license = "Apache-2.0" +name = "qp-header" +publish = false +repository.workspace = true +version = "0.1.0" + +[dependencies] +codec = { features = ["derive"], workspace = true } +log.workspace = true +p3-field = { workspace = true } +p3-goldilocks = { workspace = true } +qp-poseidon = { workspace = true, features = ["serde"] } +qp-poseidon-core = { workspace = true } +scale-info = { features = ["derive", "serde"], workspace = true } +serde = { workspace = true, features = ["derive"], optional = true } +sp-core = { features = ["serde"], workspace = true } +sp-runtime = { features = ["serde"], workspace = true } + +[dev-dependencies] +hex = { workspace = true } +serde_json = { workspace = true, default-features = false, features = [ + "alloc", + "std", +] } + + +[features] +default = ["serde", "std"] +std = [ + "codec/std", + "qp-poseidon/std", + "scale-info/std", + "sp-core/std", + "sp-runtime/std", +] diff --git a/primitives/header/src/lib.rs b/primitives/header/src/lib.rs new file mode 100644 index 00000000..bbe897fe --- /dev/null +++ b/primitives/header/src/lib.rs @@ -0,0 +1,346 @@ +//! Fork of sp-runtime's generic implementation of a block header. +//! We override the hashing function to ensure a felt aligned pre-image for the block hash. + +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::{Codec, Decode, DecodeWithMemTracking, Encode}; +use p3_field::integers::QuotientMap; +use p3_goldilocks::Goldilocks; +use qp_poseidon_core::{ + hash_variable_length, + serialization::{injective_bytes_to_felts, unsafe_digest_bytes_to_felts}, +}; +use scale_info::TypeInfo; +use sp_core::U256; +use sp_runtime::{ + generic::Digest, + traits::{AtLeast32BitUnsigned, BlockNumber, Hash as HashT, MaybeDisplay, Member}, + RuntimeDebug, +}; +extern crate alloc; + +use alloc::vec::Vec; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Custom block header that hashes itself with Poseidon over Goldilocks field elements. +#[derive(Encode, Decode, PartialEq, Eq, Clone, RuntimeDebug, TypeInfo, DecodeWithMemTracking)] +#[scale_info(skip_type_params(Hash))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +pub struct Header + TryFrom, Hash: HashT> { + pub parent_hash: Hash::Output, + #[cfg_attr( + feature = "serde", + serde(serialize_with = "serialize_number", deserialize_with = "deserialize_number") + )] + pub number: Number, + pub state_root: Hash::Output, + pub extrinsics_root: Hash::Output, + pub digest: Digest, +} + +#[cfg(feature = "serde")] +pub fn serialize_number + TryFrom>( + val: &T, + s: S, +) -> Result +where + S: serde::Serializer, +{ + let u256: U256 = (*val).into(); + serde::Serialize::serialize(&u256, s) +} + +#[cfg(feature = "serde")] +pub fn deserialize_number<'a, D, T: Copy + Into + TryFrom>(d: D) -> Result +where + D: serde::Deserializer<'a>, +{ + let u256: U256 = serde::Deserialize::deserialize(d)?; + TryFrom::try_from(u256).map_err(|_| serde::de::Error::custom("Try from failed")) +} + +impl sp_runtime::traits::Header for Header +where + Number: BlockNumber, + Hash: HashT, + Hash::Output: From<[u8; 32]>, +{ + type Number = Number; + type Hash = ::Output; + type Hashing = Hash; + + fn new( + number: Self::Number, + extrinsics_root: Self::Hash, + state_root: Self::Hash, + parent_hash: Self::Hash, + digest: Digest, + ) -> Self { + Self { number, extrinsics_root, state_root, parent_hash, digest } + } + fn number(&self) -> &Self::Number { + &self.number + } + + fn set_number(&mut self, num: Self::Number) { + self.number = num + } + fn extrinsics_root(&self) -> &Self::Hash { + &self.extrinsics_root + } + + fn set_extrinsics_root(&mut self, root: Self::Hash) { + self.extrinsics_root = root + } + fn state_root(&self) -> &Self::Hash { + &self.state_root + } + + fn set_state_root(&mut self, root: Self::Hash) { + self.state_root = root + } + fn parent_hash(&self) -> &Self::Hash { + &self.parent_hash + } + + fn set_parent_hash(&mut self, hash: Self::Hash) { + self.parent_hash = hash + } + + fn digest(&self) -> &Digest { + &self.digest + } + + fn digest_mut(&mut self) -> &mut Digest { + #[cfg(feature = "std")] + log::debug!(target: "header", "Retrieving mutable reference to digest"); + &mut self.digest + } + // We override the default hashing function to use + // a felt aligned pre-image for poseidon hashing. + fn hash(&self) -> Self::Hash { + Header::hash(&self) + } +} + +impl Header +where + Number: Member + + core::hash::Hash + + Copy + + MaybeDisplay + + AtLeast32BitUnsigned + + Codec + + Into + + TryFrom, + Hash: HashT, + Hash::Output: From<[u8; 32]>, +{ + /// Convenience helper for computing the hash of the header without having + /// to import the trait. + pub fn hash(&self) -> Hash::Output { + let max_encoded_felts = 4 * 3 + 1 + 28; // 3 hashout fields + 1 u32 + 28 felts for injective digest encoding + let mut felts = Vec::with_capacity(max_encoded_felts); + + // parent_hash : 32 bytes → 4 felts + felts.extend(unsafe_digest_bytes_to_felts::( + self.parent_hash.as_ref().try_into().expect("hash is 32 bytes"), + )); + + // block number as u64 (compact encoded, but we only need the value) + // constrain the block number to be with u32 range for simplicity + let number = self.number.into(); + felts.push(Goldilocks::from_int(number.as_u32() as u64)); + + // state_root : 32 bytes → 4 felts + felts.extend(unsafe_digest_bytes_to_felts::( + self.state_root.as_ref().try_into().expect("hash is 32 bytes"), + )); + + // extrinsics_root : 32 bytes → 4 felts + felts.extend(unsafe_digest_bytes_to_felts::( + self.extrinsics_root.as_ref().try_into().expect("hash is 32 bytes"), + )); + + // digest – injective encoding + felts.extend(injective_bytes_to_felts::(&self.digest.encode())); + + let poseidon_hash: [u8; 32] = hash_variable_length(felts); + poseidon_hash.into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use qp_poseidon::PoseidonHasher; + use sp_core::H256; + use sp_runtime::{traits::BlakeTwo256, DigestItem}; + + #[test] + fn should_serialize_numbers() { + fn serialize(num: u128) -> String { + let mut v = vec![]; + { + let mut ser = serde_json::Serializer::new(std::io::Cursor::new(&mut v)); + serialize_number(&num, &mut ser).unwrap(); + } + String::from_utf8(v).unwrap() + } + + assert_eq!(serialize(0), "\"0x0\"".to_owned()); + assert_eq!(serialize(1), "\"0x1\"".to_owned()); + assert_eq!(serialize(u64::MAX as u128), "\"0xffffffffffffffff\"".to_owned()); + assert_eq!(serialize(u64::MAX as u128 + 1), "\"0x10000000000000000\"".to_owned()); + } + + #[test] + fn should_deserialize_number() { + fn deserialize(num: &str) -> u128 { + let mut der = serde_json::Deserializer::from_str(num); + deserialize_number(&mut der).unwrap() + } + + assert_eq!(deserialize("\"0x0\""), 0); + assert_eq!(deserialize("\"0x1\""), 1); + assert_eq!(deserialize("\"0xffffffffffffffff\""), u64::MAX as u128); + assert_eq!(deserialize("\"0x10000000000000000\""), u64::MAX as u128 + 1); + } + + #[test] + fn ensure_format_is_unchanged() { + let header = Header:: { + parent_hash: BlakeTwo256::hash(b"1"), + number: 2, + state_root: BlakeTwo256::hash(b"3"), + extrinsics_root: BlakeTwo256::hash(b"4"), + digest: Digest { logs: vec![sp_runtime::generic::DigestItem::Other(b"6".to_vec())] }, + }; + + let header_encoded = header.encode(); + assert_eq!( + header_encoded, + vec![ + 146, 205, 245, 120, 196, 112, 133, 165, 153, 34, 86, 240, 220, 249, 125, 11, 25, + 241, 241, 201, 222, 77, 95, 227, 12, 58, 206, 97, 145, 182, 229, 219, 2, 0, 0, 0, + 88, 19, 72, 51, 123, 15, 62, 20, 134, 32, 23, 61, 170, 165, 249, 77, 0, 216, 129, + 112, 93, 203, 240, 170, 131, 239, 218, 186, 97, 210, 237, 225, 235, 134, 73, 33, + 73, 151, 87, 78, 32, 196, 100, 56, 138, 23, 36, 32, 210, 84, 3, 104, 43, 187, 184, + 12, 73, 104, 49, 200, 204, 31, 143, 13, 4, 0, 4, 54 + ], + ); + assert_eq!(Header::::decode(&mut &header_encoded[..]).unwrap(), header); + + let header = Header:: { + parent_hash: BlakeTwo256::hash(b"1000"), + number: 2000, + state_root: BlakeTwo256::hash(b"3000"), + extrinsics_root: BlakeTwo256::hash(b"4000"), + digest: Digest { logs: vec![sp_runtime::generic::DigestItem::Other(b"5000".to_vec())] }, + }; + + let header_encoded = header.encode(); + assert_eq!( + header_encoded, + vec![ + 197, 243, 254, 225, 31, 117, 21, 218, 179, 213, 92, 6, 247, 164, 230, 25, 47, 166, + 140, 117, 142, 159, 195, 202, 67, 196, 238, 26, 44, 18, 33, 92, 208, 7, 0, 0, 219, + 225, 47, 12, 107, 88, 153, 146, 55, 21, 226, 186, 110, 48, 167, 187, 67, 183, 228, + 232, 118, 136, 30, 254, 11, 87, 48, 112, 7, 97, 31, 82, 146, 110, 96, 87, 152, 68, + 98, 162, 227, 222, 78, 14, 244, 194, 120, 154, 112, 97, 222, 144, 174, 101, 220, + 44, 111, 126, 54, 34, 155, 220, 253, 124, 4, 0, 16, 53, 48, 48, 48 + ], + ); + assert_eq!(Header::::decode(&mut &header_encoded[..]).unwrap(), header); + } + + fn hash_header(x: &[u8]) -> [u8; 32] { + let mut y = x; + if let Ok(header) = Header::::decode(&mut y) { + // Only treat this as a header if we consumed the entire input. + if y.is_empty() { + let max_encoded_felts = 4 * 3 + 1 + 28; // 3 hashout fields + 1 u32 + 28 felts + let mut felts = Vec::with_capacity(max_encoded_felts); + + let parent_hash = header.parent_hash.as_bytes(); + let number = header.number; + let state_root = header.state_root.as_bytes(); + let extrinsics_root = header.extrinsics_root.as_bytes(); + let digest = header.digest.encode(); + + felts.extend(unsafe_digest_bytes_to_felts::( + parent_hash.try_into().expect("Parent hash expected to equal 32 bytes"), + )); + felts.push(Goldilocks::from_int(number as u64)); + felts.extend(unsafe_digest_bytes_to_felts::( + state_root.try_into().expect("State root expected to equal 32 bytes"), + )); + felts.extend(unsafe_digest_bytes_to_felts::( + extrinsics_root.try_into().expect("Extrinsics root expected to equal 32 bytes"), + )); + felts.extend(injective_bytes_to_felts::(&digest)); + + return hash_variable_length(felts); + } + } + // Fallback: canonical bytes hashing for non-header data + PoseidonHasher::hash_padded(x) + } + + #[test] + fn poseidon_header_hash_matches_old_path() { + use codec::Encode; + + // Example header from a real block on devnet + let parent_hash = "839b2d2ac0bf4aa71b18ad1ba5e2880b4ef06452cefacd255cfd76f6ad2c7966"; + let number = 4; + let state_root = "1688817041c572d6c971681465f401f06d0fdcfaed61d28c06d42dc2d07816d5"; + let extrinsics_root = "7c6cace2e91b6314e05410b91224c11f5dd4a4a2dbf0e39081fddbe4ac9ad252"; + let digest = Digest { + logs: vec![ + DigestItem::PreRuntime( + [112, 111, 119, 95], + [ + 233, 182, 183, 107, 158, 1, 115, 19, 219, 126, 253, 86, 30, 208, 176, 70, + 21, 45, 180, 229, 9, 62, 91, 4, 6, 53, 245, 52, 48, 38, 123, 225, + ] + .to_vec(), + ), + DigestItem::Seal( + [112, 111, 119, 95], + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 77, 142, + ] + .to_vec(), + ), + ], + }; + let header = Header:: { + parent_hash: H256::from_slice( + hex::decode(parent_hash).expect("valid hex parent hash").as_slice(), + ), + number, + state_root: H256::from_slice( + hex::decode(state_root).expect("valid hex state root").as_slice(), + ), + extrinsics_root: H256::from_slice( + hex::decode(extrinsics_root).expect("valid hex extrinsics root").as_slice(), + ), + digest, + }; + + let encoded = header.encode(); + + let old = hash_header(&encoded); // old path + let new: [u8; 32] = header.hash().into(); + println!("Old hash: 0x{}", hex::encode(old)); + + assert_eq!(old, new); + } +} diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 2c1ad93c..51c7b67f 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -49,6 +49,7 @@ pallet-utility.workspace = true pallet-vesting.workspace = true primitive-types.workspace = true qp-dilithium-crypto.workspace = true +qp-header = { workspace = true, features = ["serde"] } qp-poseidon = { workspace = true, features = ["serde"] } qp-scheduler.workspace = true scale-info = { features = ["derive", "serde"], workspace = true } @@ -114,10 +115,10 @@ std = [ "primitive-types/std", "qp-dilithium-crypto/full_crypto", "qp-dilithium-crypto/std", + "qp-header/std", "qp-poseidon/std", "qp-scheduler/std", "scale-info/std", - "scale-info/std", "serde_json/std", "sp-api/std", "sp-block-builder/std", diff --git a/runtime/src/genesis_config_presets.rs b/runtime/src/genesis_config_presets.rs index d7e835b4..57eb5182 100644 --- a/runtime/src/genesis_config_presets.rs +++ b/runtime/src/genesis_config_presets.rs @@ -77,6 +77,7 @@ pub fn development_config_genesis() -> Value { let ss58_version = sp_core::crypto::Ss58AddressFormat::custom(189); for account in endowed_accounts.iter() { log::info!("🍆 Endowed account: {:?}", account.to_ss58check_with_version(ss58_version)); + log::info!("🍆 Endowed account raw: {:?}", account); } genesis_template(endowed_accounts, crystal_alice().into_account()) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 28df014f..447ac474 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -35,7 +35,6 @@ pub mod transaction_extensions; use crate::governance::pallet_custom_origins; use qp_poseidon::PoseidonHasher; - /// Opaque types. These are used by the CLI to instantiate machinery that don't need to know /// the specifics of the runtime. They can then be made to be agnostic over specific formats /// of data like extrinsics, allowing for them to continue syncing the network through upgrades @@ -52,7 +51,7 @@ pub mod opaque { // However, some internal checks in dev build expect extrinsics_root to be computed with same // Hash function, so we change the configs/mod.rs Hashing type as well // Opaque block header type. - pub type Header = generic::Header; + pub type Header = qp_header::Header; // Opaque block type. pub type Block = generic::Block; @@ -134,7 +133,7 @@ pub type BlockNumber = u32; pub type Address = MultiAddress; /// Block header type as expected by this runtime. -pub type Header = generic::Header; +pub type Header = qp_header::Header; /// Block type as expected by this runtime. pub type Block = generic::Block; From cd916c2e8857608aadf0afe38d5af02db0a68d91 Mon Sep 17 00:00:00 2001 From: Dastan <88332432+dastansam@users.noreply.github.com> Date: Mon, 22 Dec 2025 13:47:33 +0600 Subject: [PATCH 04/27] Use canonical balances pallet and add support for assets in wormhole (#333) * Use canonical balances pallet, add assets support to wormhole * Ignore old tests * Remove tests * Override native asset id * Use poseidon hasher * Use poseidon storage hasher * Passing wormhole proof tests * Update binaries * Update binaries * Update zk-circuits crates * Use crates.io dep versions --- Cargo.lock | 69 +- Cargo.toml | 18 +- node/src/benchmarking.rs | 4 + pallets/balances/Cargo.toml | 65 - pallets/balances/README.md | 127 -- pallets/balances/src/benchmarking.rs | 350 ---- pallets/balances/src/impl_currency.rs | 952 ---------- pallets/balances/src/impl_fungible.rs | 385 ---- pallets/balances/src/impl_proofs.rs | 28 - pallets/balances/src/lib.rs | 1338 -------------- pallets/balances/src/migration.rs | 103 -- pallets/balances/src/tests/currency_tests.rs | 1643 ----------------- .../balances/src/tests/dispatchable_tests.rs | 410 ---- .../src/tests/fungible_conformance_tests.rs | 141 -- pallets/balances/src/tests/fungible_tests.rs | 883 --------- pallets/balances/src/tests/general_tests.rs | 143 -- pallets/balances/src/tests/mod.rs | 330 ---- .../balances/src/tests/reentrancy_tests.rs | 212 --- .../src/tests/transfer_counter_tests.rs | 336 ---- pallets/balances/src/types.rs | 164 -- pallets/balances/src/weights.rs | 300 --- pallets/merkle-airdrop/src/mock.rs | 2 + pallets/mining-rewards/src/lib.rs | 31 +- pallets/mining-rewards/src/mock.rs | 21 + .../reversible-transfers/src/tests/mock.rs | 1 + pallets/wormhole/Cargo.toml | 2 + pallets/wormhole/common.bin | Bin 1905 -> 1905 bytes pallets/wormhole/src/lib.rs | 252 ++- pallets/wormhole/src/mock.rs | 44 +- pallets/wormhole/src/tests.rs | 188 +- pallets/wormhole/verifier.bin | Bin 552 -> 552 bytes primitives/wormhole/src/lib.rs | 31 +- runtime/Cargo.toml | 4 + runtime/src/configs/mod.rs | 13 +- runtime/src/genesis_config_presets.rs | 15 +- runtime/src/lib.rs | 1 + runtime/src/transaction_extensions.rs | 251 ++- runtime/tests/governance/treasury.rs | 9 +- 38 files changed, 775 insertions(+), 8091 deletions(-) delete mode 100644 pallets/balances/Cargo.toml delete mode 100644 pallets/balances/README.md delete mode 100644 pallets/balances/src/benchmarking.rs delete mode 100644 pallets/balances/src/impl_currency.rs delete mode 100644 pallets/balances/src/impl_fungible.rs delete mode 100644 pallets/balances/src/impl_proofs.rs delete mode 100644 pallets/balances/src/lib.rs delete mode 100644 pallets/balances/src/migration.rs delete mode 100644 pallets/balances/src/tests/currency_tests.rs delete mode 100644 pallets/balances/src/tests/dispatchable_tests.rs delete mode 100644 pallets/balances/src/tests/fungible_conformance_tests.rs delete mode 100644 pallets/balances/src/tests/fungible_tests.rs delete mode 100644 pallets/balances/src/tests/general_tests.rs delete mode 100644 pallets/balances/src/tests/mod.rs delete mode 100644 pallets/balances/src/tests/reentrancy_tests.rs delete mode 100644 pallets/balances/src/tests/transfer_counter_tests.rs delete mode 100644 pallets/balances/src/types.rs delete mode 100644 pallets/balances/src/weights.rs diff --git a/Cargo.lock b/Cargo.lock index b98c66c9..9bfabe60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7081,27 +7081,6 @@ dependencies = [ "sp-staking", ] -[[package]] -name = "pallet-balances" -version = "40.0.1" -dependencies = [ - "docify", - "frame-benchmarking", - "frame-support", - "frame-system", - "log", - "pallet-transaction-payment", - "parity-scale-codec", - "paste", - "qp-poseidon", - "qp-wormhole", - "scale-info", - "sp-core", - "sp-io", - "sp-metadata-ir", - "sp-runtime", -] - [[package]] name = "pallet-balances" version = "42.0.0" @@ -7164,7 +7143,7 @@ dependencies = [ "frame-support", "frame-system", "log", - "pallet-balances 40.0.1", + "pallet-balances", "pallet-vesting", "parity-scale-codec", "scale-info", @@ -7202,7 +7181,7 @@ dependencies = [ "frame-support", "frame-system", "log", - "pallet-balances 40.0.1", + "pallet-balances", "parity-scale-codec", "qp-wormhole", "scale-info", @@ -7323,7 +7302,7 @@ dependencies = [ "log", "pallet-assets", "pallet-assets-holder", - "pallet-balances 40.0.1", + "pallet-balances", "pallet-preimage", "pallet-recovery", "pallet-scheduler", @@ -7582,7 +7561,7 @@ dependencies = [ "frame-system", "impl-trait-for-tuples", "log", - "pallet-balances 42.0.0", + "pallet-balances", "parity-scale-codec", "scale-info", "serde", @@ -7631,7 +7610,8 @@ dependencies = [ "hex", "lazy_static", "log", - "pallet-balances 40.0.1", + "pallet-assets", + "pallet-balances", "parity-scale-codec", "qp-dilithium-crypto", "qp-header", @@ -7647,6 +7627,7 @@ dependencies = [ "scale-info", "sp-core", "sp-io", + "sp-metadata-ir", "sp-runtime", "sp-state-machine", "sp-trie", @@ -8229,7 +8210,7 @@ dependencies = [ "pallet-authority-discovery", "pallet-authorship", "pallet-babe", - "pallet-balances 42.0.0", + "pallet-balances", "pallet-broker", "pallet-message-queue", "pallet-mmr", @@ -9003,9 +8984,9 @@ dependencies = [ [[package]] name = "qp-poseidon" -version = "1.0.3" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ebc5e9fe1f91f5006aa2b45650d0049887d41d80c669ef5a78a45086895054d" +checksum = "5029d1c8223c0312a0247ebc745c5b09622dcbebe104f0fdb9de358b87ef032a" dependencies = [ "log", "p3-field", @@ -9035,9 +9016,9 @@ dependencies = [ [[package]] name = "qp-poseidon-core" -version = "1.0.3" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52c70df221c356b3ce63afabfae623aae632c27d3e078cd20eec4348096c2d7" +checksum = "71dd1bf5d2997abf70247fcd23c8f04d7093b1faf33b775a42fb00c07e0a0e05" dependencies = [ "p3-field", "p3-goldilocks", @@ -9095,9 +9076,9 @@ version = "0.1.0" [[package]] name = "qp-wormhole-circuit" -version = "0.1.4" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10b7357bec091287185850a6f7b67eb45f899a655b2a86825eae7e39b3bc6c3c" +checksum = "bcbccee20e314a1c52f36d8e78b1fa8205da46050c18da60d4964f41875bd4f6" dependencies = [ "anyhow", "hex", @@ -9108,9 +9089,9 @@ dependencies = [ [[package]] name = "qp-wormhole-circuit-builder" -version = "0.1.4" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7faed51eaa66ba71526955b74aac2fdba9849905a405f74d5f4f26564a9eaa98" +checksum = "aec473c32d69e2e0941d192ab8cf8712761ee2e84d3829fc211087f53be15bf3" dependencies = [ "anyhow", "qp-plonky2", @@ -9120,9 +9101,9 @@ dependencies = [ [[package]] name = "qp-wormhole-prover" -version = "0.1.4" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a56d84dd9bf64944b88cb0ec971f179a569d3bdf148132ab01bfe2d56c069d" +checksum = "9fd377a2fa936e6edb069ffecbf41afe10803b342e5cda8ff3aab199f77fde5d" dependencies = [ "anyhow", "qp-plonky2", @@ -9132,9 +9113,9 @@ dependencies = [ [[package]] name = "qp-wormhole-verifier" -version = "0.1.4" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40387c4c43fe47419cba58eb4f13c2a3c32ac0e381c98c3d77293ebf53298de7" +checksum = "08e114bc7ad7a7589c502960abc570685b726993ccce05443577a9ddd8dc4ee1" dependencies = [ "anyhow", "qp-plonky2", @@ -9144,9 +9125,9 @@ dependencies = [ [[package]] name = "qp-zk-circuits-common" -version = "0.1.4" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "366557f4c727379e2fd9c18b6667ae53c99bb241b973427df09a6b09584a11d4" +checksum = "445cc21c39959d1b553c4d8ea94d058ceab84cd70e5d47ec82b11535494cec46" dependencies = [ "anyhow", "hex", @@ -9264,7 +9245,7 @@ dependencies = [ "log", "pallet-assets", "pallet-assets-holder", - "pallet-balances 40.0.1", + "pallet-balances", "pallet-conviction-voting", "pallet-merkle-airdrop", "pallet-mining-rewards", @@ -9289,6 +9270,10 @@ dependencies = [ "qp-header", "qp-poseidon", "qp-scheduler", + "qp-wormhole", + "qp-wormhole-circuit", + "qp-wormhole-verifier", + "qp-zk-circuits-common", "scale-info", "serde_json", "sp-api", diff --git a/Cargo.toml b/Cargo.toml index b4d891f1..4878ed36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,6 @@ members = [ "client/network", "miner-api", "node", - "pallets/balances", "pallets/merkle-airdrop", "pallets/mining-rewards", "pallets/qpow", @@ -130,7 +129,7 @@ wasm-timer = { version = "0.2.5" } zeroize = { version = "1.7.0", default-features = false } # Own dependencies -pallet-balances = { path = "./pallets/balances", default-features = false } +pallet-balances = { version = "42.0.0", default-features = false } pallet-merkle-airdrop = { path = "./pallets/merkle-airdrop", default-features = false } pallet-mining-rewards = { path = "./pallets/mining-rewards", default-features = false } pallet-qpow = { path = "./pallets/qpow", default-features = false } @@ -150,19 +149,19 @@ sp-consensus-qpow = { path = "./primitives/consensus/qpow", default-features = f # Quantus network dependencies qp-plonky2 = { version = "1.1.3", default-features = false } -qp-poseidon = { version = "1.0.3", default-features = false } -qp-poseidon-core = { version = "1.0.3", default-features = false, features = ["p2", "p3"] } +qp-poseidon = { version = "1.0.5", default-features = false } +qp-poseidon-core = { version = "1.0.5", default-features = false, features = ["p2", "p3"] } qp-rusty-crystals-dilithium = { version = "2.0.0", default-features = false } qp-rusty-crystals-hdwallet = { version = "1.0.0" } -qp-wormhole-circuit = { version = "0.1.3", default-features = false } -qp-wormhole-circuit-builder = { version = "0.1.4", default-features = false } -qp-wormhole-prover = { version = "0.1.4", default-features = false, features = [ +qp-wormhole-circuit = { version = "0.1.7", default-features = false } +qp-wormhole-circuit-builder = { version = "0.1.7", default-features = false } +qp-wormhole-prover = { version = "0.1.7", default-features = false, features = [ "no_random", ] } -qp-wormhole-verifier = { version = "0.1.4", default-features = false, features = [ +qp-wormhole-verifier = { version = "0.1.7", default-features = false, features = [ "no_random", ] } -qp-zk-circuits-common = { version = "0.1.4", default-features = false, features = [ +qp-zk-circuits-common = { version = "0.1.7", default-features = false, features = [ "no_random", ] } @@ -244,7 +243,6 @@ sc-network = { path = "client/network" } sp-state-machine = { path = "./primitives/state-machine" } sp-trie = { path = "./primitives/trie" } - [profile.release] opt-level = 3 panic = "unwind" diff --git a/node/src/benchmarking.rs b/node/src/benchmarking.rs index 2d3d33eb..2dd9a8ef 100644 --- a/node/src/benchmarking.rs +++ b/node/src/benchmarking.rs @@ -127,6 +127,9 @@ pub fn create_benchmark_extrinsic( quantus_runtime::transaction_extensions::ReversibleTransactionExtension::< runtime::Runtime, >::new(), + quantus_runtime::transaction_extensions::WormholeProofRecorderExtension::< + runtime::Runtime, + >::new(), ); let raw_payload = runtime::SignedPayload::from_raw( @@ -143,6 +146,7 @@ pub fn create_benchmark_extrinsic( (), None, (), + (), ), ); let signature = raw_payload.using_encoded(|e| sender.sign(e)); diff --git a/pallets/balances/Cargo.toml b/pallets/balances/Cargo.toml deleted file mode 100644 index 686b4e18..00000000 --- a/pallets/balances/Cargo.toml +++ /dev/null @@ -1,65 +0,0 @@ -[package] -authors.workspace = true -description = "FRAME pallet to manage balances" -edition.workspace = true -homepage.workspace = true -license = "Apache-2.0" -name = "pallet-balances" -readme = "README.md" -repository.workspace = true -version = "40.0.1" - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] - -[dependencies] -codec = { features = ["derive", "max-encoded-len"], workspace = true } -docify = { workspace = true } -frame-benchmarking = { optional = true, workspace = true } -frame-support.workspace = true -frame-system.workspace = true -log.workspace = true -qp-poseidon = { workspace = true, features = ["serde"] } -qp-wormhole = { workspace = true } -scale-info = { features = ["derive"], workspace = true } -sp-metadata-ir = { workspace = true } -sp-runtime.workspace = true - -[dev-dependencies] -frame-support = { workspace = true, features = ["experimental"], default-features = true } -pallet-transaction-payment.features = ["std"] -pallet-transaction-payment.workspace = true -paste.workspace = true -sp-core.workspace = true -sp-io.workspace = true - -[features] -default = ["std"] -std = [ - "codec/std", - "frame-benchmarking?/std", - "frame-support/std", - "frame-system/std", - "log/std", - "pallet-transaction-payment/std", - "qp-poseidon/std", - "qp-wormhole/std", - "scale-info/std", - "sp-core/std", - "sp-io/std", - "sp-metadata-ir/std", - "sp-runtime/std", -] -# Enable support for setting the existential deposit to zero. -insecure_zero_ed = [] -runtime-benchmarks = [ - "frame-benchmarking/runtime-benchmarks", - "frame-support/runtime-benchmarks", - "frame-system/runtime-benchmarks", - "sp-runtime/runtime-benchmarks", -] -try-runtime = [ - "frame-support/try-runtime", - "frame-system/try-runtime", - "sp-runtime/try-runtime", -] diff --git a/pallets/balances/README.md b/pallets/balances/README.md deleted file mode 100644 index b1e9c82f..00000000 --- a/pallets/balances/README.md +++ /dev/null @@ -1,127 +0,0 @@ -# Balances Module - -The Balances module provides functionality for handling accounts and balances. - -- [`Config`](https://docs.rs/pallet-balances/latest/pallet_balances/pallet/trait.Config.html) -- [`Call`](https://docs.rs/pallet-balances/latest/pallet_balances/pallet/enum.Call.html) -- [`Pallet`](https://docs.rs/pallet-balances/latest/pallet_balances/pallet/struct.Pallet.html) - -## Overview - -The Balances module provides functions for: - -- Getting and setting free balances. -- Retrieving total, reserved and unreserved balances. -- Repatriating a reserved balance to a beneficiary account that exists. -- Transferring a balance between accounts (when not reserved). -- Slashing an account balance. -- Account creation and removal. -- Managing total issuance. -- Setting and managing locks. - -### Terminology - -- **Existential Deposit:** The minimum balance required to create or keep an account open. This prevents "dust accounts" -from filling storage. When the free plus the reserved balance (i.e. the total balance) fall below this, then the account - is said to be dead; and it loses its functionality as well as any prior history and all information on it is removed - from the chain's state. No account should ever have a total balance that is strictly between 0 and the existential - deposit (exclusive). If this ever happens, it indicates either a bug in this module or an erroneous raw mutation of - storage. - -- **Total Issuance:** The total number of units in existence in a system. - -- **Reaping an account:** The act of removing an account by resetting its nonce. Happens after its total balance has -become zero (or, strictly speaking, less than the Existential Deposit). - -- **Free Balance:** The portion of a balance that is not reserved. The free balance is the only balance that matters for - most operations. - -- **Reserved Balance:** Reserved balance still belongs to the account holder, but is suspended. Reserved balance can - still be slashed, but only after all the free balance has been slashed. - -- **Imbalance:** A condition when some funds were credited or debited without equal and opposite accounting (i.e. a -difference between total issuance and account balances). Functions that result in an imbalance will return an object of -the `Imbalance` trait that can be managed within your runtime logic. (If an imbalance is simply dropped, it should -automatically maintain any book-keeping such as total issuance.) - -- **Lock:** A freeze on a specified amount of an account's free balance until a specified block number. Multiple locks -always operate over the same funds, so they "overlay" rather than "stack". - -### Implementations - -The Balances module provides implementations for the following traits. If these traits provide the functionality that -you need, then you can avoid coupling with the Balances module. - -- [`Currency`](https://docs.rs/frame-support/latest/frame_support/traits/trait.Currency.html): Functions for dealing -with a fungible assets system. -- [`ReservableCurrency`](https://docs.rs/frame-support/latest/frame_support/traits/trait.ReservableCurrency.html): -Functions for dealing with assets that can be reserved from an account. -- [`LockableCurrency`](https://docs.rs/frame-support/latest/frame_support/traits/trait.LockableCurrency.html): Functions -for dealing with accounts that allow liquidity restrictions. -- [`Imbalance`](https://docs.rs/frame-support/latest/frame_support/traits/trait.Imbalance.html): Functions for handling -imbalances between total issuance in the system and account balances. Must be used when a function creates new funds -(e.g. a reward) or destroys some funds (e.g. a system fee). -- [`IsDeadAccount`](https://docs.rs/frame-support/latest/frame_support/traits/trait.IsDeadAccount.html): Determiner to -say whether a given account is unused. - -## Interface - -### Dispatchable Functions - -- `transfer` - Transfer some liquid free balance to another account. -- `force_set_balance` - Set the balances of a given account. The origin of this call must be root. - -## Usage - -The following examples show how to use the Balances module in your custom module. - -### Examples from the FRAME - -The Contract module uses the `Currency` trait to handle gas payment, and its types inherit from `Currency`: - -```rust -use frame_support::traits::Currency; - -pub type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; -pub type NegativeImbalanceOf = <::Currency as Currency<::AccountId>>::NegativeImbalance; - -``` - -The Staking module uses the `LockableCurrency` trait to lock a stash account's funds: - -```rust -use frame_support::traits::{WithdrawReasons, LockableCurrency}; -use sp_runtime::traits::Bounded; -pub trait Config: frame_system::Config { - type Currency: LockableCurrency>; -} - -fn update_ledger( - controller: &T::AccountId, - ledger: &StakingLedger -) { - T::Currency::set_lock( - STAKING_ID, - &ledger.stash, - ledger.total, - WithdrawReasons::all() - ); - // >::insert(controller, ledger); // Commented out as we don't have access to Staking's storage here. -} -``` - -## Genesis config - -The Balances module depends on the -[`GenesisConfig`](https://docs.rs/pallet-balances/latest/pallet_balances/pallet/struct.GenesisConfig.html). - -## Assumptions - -- Total issued balanced of all accounts should be less than `Config::Balance::max_value()`. - -License: Apache-2.0 - - -## Release - -Polkadot SDK Stable 2412 diff --git a/pallets/balances/src/benchmarking.rs b/pallets/balances/src/benchmarking.rs deleted file mode 100644 index 06f3d24d..00000000 --- a/pallets/balances/src/benchmarking.rs +++ /dev/null @@ -1,350 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Balances pallet benchmarking. - -#![cfg(feature = "runtime-benchmarks")] - -use super::*; -use crate::Pallet as Balances; - -use frame_benchmarking::v2::*; -use frame_system::RawOrigin; -use sp_runtime::traits::Bounded; -use types::ExtraFlags; - -const SEED: u32 = 0; -// existential deposit multiplier -const ED_MULTIPLIER: u32 = 10; - -fn minimum_balance, I: 'static>() -> T::Balance { - if cfg!(feature = "insecure_zero_ed") { - 100u32.into() - } else { - T::ExistentialDeposit::get() - } -} - -#[instance_benchmarks] -mod benchmarks { - use super::*; - - // Benchmark `transfer` extrinsic with the worst possible conditions: - // * Transfer will kill the sender account. - // * Transfer will create the recipient account. - #[benchmark] - fn transfer_allow_death() { - let existential_deposit: T::Balance = minimum_balance::(); - let caller = whitelisted_caller(); - - // Give some multiple of the existential deposit - let balance = existential_deposit.saturating_mul(ED_MULTIPLIER.into()).max(1u32.into()); - let _ = as Currency<_>>::make_free_balance_be(&caller, balance); - - // Transfer `e - 1` existential deposits + 1 unit, which guarantees to create one account, - // and reap this user. - let recipient: T::AccountId = account("recipient", 0, SEED); - let recipient_lookup = T::Lookup::unlookup(recipient.clone()); - let transfer_amount = - existential_deposit.saturating_mul((ED_MULTIPLIER - 1).into()) + 1u32.into(); - - #[extrinsic_call] - _(RawOrigin::Signed(caller.clone()), recipient_lookup, transfer_amount); - - assert_eq!(Balances::::free_balance(&caller), Zero::zero()); - assert_eq!(Balances::::free_balance(&recipient), transfer_amount); - } - - // Benchmark `transfer` with the best possible condition: - // * Both accounts exist and will continue to exist. - #[benchmark(extra)] - fn transfer_best_case() { - let caller = whitelisted_caller(); - let recipient: T::AccountId = account("recipient", 0, SEED); - let recipient_lookup = T::Lookup::unlookup(recipient.clone()); - - // Give the sender account max funds for transfer (their account will never reasonably be - // killed). - let _ = - as Currency<_>>::make_free_balance_be(&caller, T::Balance::max_value()); - - // Give the recipient account existential deposit (thus their account already exists). - let existential_deposit: T::Balance = minimum_balance::(); - let _ = - as Currency<_>>::make_free_balance_be(&recipient, existential_deposit); - let transfer_amount = existential_deposit.saturating_mul(ED_MULTIPLIER.into()); - - #[extrinsic_call] - transfer_allow_death(RawOrigin::Signed(caller.clone()), recipient_lookup, transfer_amount); - - assert!(!Balances::::free_balance(&caller).is_zero()); - assert!(!Balances::::free_balance(&recipient).is_zero()); - } - - // Benchmark `transfer_keep_alive` with the worst possible condition: - // * The recipient account is created. - #[benchmark] - fn transfer_keep_alive() { - let caller = whitelisted_caller(); - let recipient: T::AccountId = account("recipient", 0, SEED); - let recipient_lookup = T::Lookup::unlookup(recipient.clone()); - - // Give the sender account max funds, thus a transfer will not kill account. - let _ = - as Currency<_>>::make_free_balance_be(&caller, T::Balance::max_value()); - let existential_deposit: T::Balance = minimum_balance::(); - let transfer_amount = existential_deposit.saturating_mul(ED_MULTIPLIER.into()); - - #[extrinsic_call] - _(RawOrigin::Signed(caller.clone()), recipient_lookup, transfer_amount); - - assert!(!Balances::::free_balance(&caller).is_zero()); - assert_eq!(Balances::::free_balance(&recipient), transfer_amount); - } - - // Benchmark `force_set_balance` coming from ROOT account. This always creates an account. - #[benchmark] - fn force_set_balance_creating() { - let user: T::AccountId = account("user", 0, SEED); - let user_lookup = T::Lookup::unlookup(user.clone()); - - // Give the user some initial balance. - let existential_deposit: T::Balance = minimum_balance::(); - let balance_amount = existential_deposit.saturating_mul(ED_MULTIPLIER.into()); - let _ = as Currency<_>>::make_free_balance_be(&user, balance_amount); - - #[extrinsic_call] - force_set_balance(RawOrigin::Root, user_lookup, balance_amount); - - assert_eq!(Balances::::free_balance(&user), balance_amount); - } - - // Benchmark `force_set_balance` coming from ROOT account. This always kills an account. - #[benchmark] - fn force_set_balance_killing() { - let user: T::AccountId = account("user", 0, SEED); - let user_lookup = T::Lookup::unlookup(user.clone()); - - // Give the user some initial balance. - let existential_deposit: T::Balance = minimum_balance::(); - let balance_amount = existential_deposit.saturating_mul(ED_MULTIPLIER.into()); - let _ = as Currency<_>>::make_free_balance_be(&user, balance_amount); - - #[extrinsic_call] - force_set_balance(RawOrigin::Root, user_lookup, Zero::zero()); - - assert!(Balances::::free_balance(&user).is_zero()); - } - - // Benchmark `force_transfer` extrinsic with the worst possible conditions: - // * Transfer will kill the sender account. - // * Transfer will create the recipient account. - #[benchmark] - fn force_transfer() { - let existential_deposit: T::Balance = minimum_balance::(); - let source: T::AccountId = account("source", 0, SEED); - let source_lookup = T::Lookup::unlookup(source.clone()); - - // Give some multiple of the existential deposit - let balance = existential_deposit.saturating_mul(ED_MULTIPLIER.into()); - let _ = as Currency<_>>::make_free_balance_be(&source, balance); - - // Transfer `e - 1` existential deposits + 1 unit, which guarantees to create one account, - // and reap this user. - let recipient: T::AccountId = account("recipient", 0, SEED); - let recipient_lookup = T::Lookup::unlookup(recipient.clone()); - let transfer_amount = - existential_deposit.saturating_mul((ED_MULTIPLIER - 1).into()) + 1u32.into(); - - #[extrinsic_call] - _(RawOrigin::Root, source_lookup, recipient_lookup, transfer_amount); - - assert_eq!(Balances::::free_balance(&source), Zero::zero()); - assert_eq!(Balances::::free_balance(&recipient), transfer_amount); - } - - // This benchmark performs the same operation as `transfer` in the worst case scenario, - // but additionally introduces many new users into the storage, increasing the the merkle - // trie and PoV size. - #[benchmark(extra)] - fn transfer_increasing_users(u: Linear<0, 1_000>) { - // 1_000 is not very much, but this upper bound can be controlled by the CLI. - let existential_deposit: T::Balance = minimum_balance::(); - let caller = whitelisted_caller(); - - // Give some multiple of the existential deposit - let balance = existential_deposit.saturating_mul(ED_MULTIPLIER.into()); - let _ = as Currency<_>>::make_free_balance_be(&caller, balance); - - // Transfer `e - 1` existential deposits + 1 unit, which guarantees to create one account, - // and reap this user. - let recipient: T::AccountId = account("recipient", 0, SEED); - let recipient_lookup = T::Lookup::unlookup(recipient.clone()); - let transfer_amount = - existential_deposit.saturating_mul((ED_MULTIPLIER - 1).into()) + 1u32.into(); - - // Create a bunch of users in storage. - for i in 0..u { - // The `account` function uses `blake2_256` to generate unique accounts, so these - // should be quite random and evenly distributed in the trie. - let new_user: T::AccountId = account("new_user", i, SEED); - let _ = as Currency<_>>::make_free_balance_be(&new_user, balance); - } - - #[extrinsic_call] - transfer_allow_death(RawOrigin::Signed(caller.clone()), recipient_lookup, transfer_amount); - - assert_eq!(Balances::::free_balance(&caller), Zero::zero()); - assert_eq!(Balances::::free_balance(&recipient), transfer_amount); - } - - // Benchmark `transfer_all` with the worst possible condition: - // * The recipient account is created - // * The sender is killed - #[benchmark] - fn transfer_all() { - let caller = whitelisted_caller(); - let recipient: T::AccountId = account("recipient", 0, SEED); - let recipient_lookup = T::Lookup::unlookup(recipient.clone()); - - // Give some multiple of the existential deposit - let existential_deposit: T::Balance = minimum_balance::(); - let balance = existential_deposit.saturating_mul(ED_MULTIPLIER.into()); - let _ = as Currency<_>>::make_free_balance_be(&caller, balance); - - #[extrinsic_call] - _(RawOrigin::Signed(caller.clone()), recipient_lookup, false); - - assert!(Balances::::free_balance(&caller).is_zero()); - assert_eq!(Balances::::free_balance(&recipient), balance); - } - - #[benchmark] - fn force_unreserve() -> Result<(), BenchmarkError> { - let user: T::AccountId = account("user", 0, SEED); - let user_lookup = T::Lookup::unlookup(user.clone()); - - // Give some multiple of the existential deposit - let ed = minimum_balance::(); - let balance = ed + ed; - let _ = as Currency<_>>::make_free_balance_be(&user, balance); - - // Reserve the balance - as ReservableCurrency<_>>::reserve(&user, ed)?; - assert_eq!(Balances::::reserved_balance(&user), ed); - assert_eq!(Balances::::free_balance(&user), ed); - - #[extrinsic_call] - _(RawOrigin::Root, user_lookup, balance); - - assert!(Balances::::reserved_balance(&user).is_zero()); - assert_eq!(Balances::::free_balance(&user), ed + ed); - - Ok(()) - } - - #[benchmark] - fn upgrade_accounts(u: Linear<1, 1_000>) { - let caller: T::AccountId = whitelisted_caller(); - let who = (0..u) - .map(|i| -> T::AccountId { - let user = account("old_user", i, SEED); - let account = AccountData { - free: minimum_balance::(), - reserved: minimum_balance::(), - frozen: Zero::zero(), - flags: ExtraFlags::old_logic(), - }; - frame_system::Pallet::::inc_providers(&user); - assert!(T::AccountStore::try_mutate_exists(&user, |a| -> DispatchResult { - *a = Some(account); - Ok(()) - }) - .is_ok()); - assert!(!Balances::::account(&user).flags.is_new_logic()); - assert_eq!(frame_system::Pallet::::providers(&user), 1); - assert_eq!(frame_system::Pallet::::consumers(&user), 0); - user - }) - .collect(); - - #[extrinsic_call] - _(RawOrigin::Signed(caller.clone()), who); - - for i in 0..u { - let user: T::AccountId = account("old_user", i, SEED); - assert!(Balances::::account(&user).flags.is_new_logic()); - assert_eq!(frame_system::Pallet::::providers(&user), 1); - assert_eq!(frame_system::Pallet::::consumers(&user), 1); - } - } - - #[benchmark] - fn force_adjust_total_issuance() { - let ti = TotalIssuance::::get(); - let delta = 123u32.into(); - - #[extrinsic_call] - _(RawOrigin::Root, AdjustmentDirection::Increase, delta); - - assert_eq!(TotalIssuance::::get(), ti + delta); - } - - /// Benchmark `burn` extrinsic with the worst possible condition - burn kills the account. - #[benchmark] - fn burn_allow_death() { - let existential_deposit = T::ExistentialDeposit::get(); - let caller = whitelisted_caller(); - - // Give some multiple of the existential deposit - let balance = existential_deposit.saturating_mul(ED_MULTIPLIER.into()); - let _ = as Currency<_>>::make_free_balance_be(&caller, balance); - - // Burn enough to kill the account. - let burn_amount = balance - existential_deposit + 1u32.into(); - - #[extrinsic_call] - burn(RawOrigin::Signed(caller.clone()), burn_amount, false); - - assert_eq!(Balances::::free_balance(&caller), Zero::zero()); - } - - // Benchmark `burn` extrinsic with the case where account is kept alive. - #[benchmark] - fn burn_keep_alive() { - let existential_deposit = T::ExistentialDeposit::get(); - let caller = whitelisted_caller(); - - // Give some multiple of the existential deposit - let balance = existential_deposit.saturating_mul(ED_MULTIPLIER.into()); - let _ = as Currency<_>>::make_free_balance_be(&caller, balance); - - // Burn minimum possible amount which should not kill the account. - let burn_amount = 1u32.into(); - - #[extrinsic_call] - burn(RawOrigin::Signed(caller.clone()), burn_amount, true); - - assert_eq!(Balances::::free_balance(&caller), balance - burn_amount); - } - - impl_benchmark_test_suite! { - Balances, - crate::tests::ExtBuilder::default().build(), - crate::tests::Test, - } -} diff --git a/pallets/balances/src/impl_currency.rs b/pallets/balances/src/impl_currency.rs deleted file mode 100644 index 547cca75..00000000 --- a/pallets/balances/src/impl_currency.rs +++ /dev/null @@ -1,952 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Implementations for the `Currency` family of traits. -//! -//! Note that `WithdrawReasons` are intentionally not used for anything in this implementation and -//! are expected to be removed in the near future, once migration to `fungible::*` traits is done. - -use super::*; -use core::cmp::Ordering; -use frame_support::{ - ensure, - pallet_prelude::DispatchResult, - traits::{ - tokens::{ - fungible, Balance, BalanceStatus as Status, Fortitude::Polite, Precision::BestEffort, - }, - Currency, DefensiveSaturating, - ExistenceRequirement::{self, AllowDeath}, - Get, Imbalance, InspectLockableCurrency, LockIdentifier, LockableCurrency, - NamedReservableCurrency, ReservableCurrency, SignedImbalance, TryDrop, WithdrawReasons, - }, -}; -use frame_system::pallet_prelude::BlockNumberFor; -pub use imbalances::{NegativeImbalance, PositiveImbalance}; -use sp_runtime::traits::Bounded; - -// wrapping these imbalances in a private module is necessary to ensure absolute privacy -// of the inner member. -mod imbalances { - use super::*; - use core::mem; - use frame_support::traits::{tokens::imbalance::TryMerge, SameOrOther}; - - /// Opaque, move-only struct with private fields that serves as a token denoting that - /// funds have been created without any equal and opposite accounting. - #[must_use] - #[derive(RuntimeDebug, PartialEq, Eq)] - pub struct PositiveImbalance, I: 'static = ()>(T::Balance); - - impl, I: 'static> PositiveImbalance { - /// Create a new positive imbalance from a balance. - pub fn new(amount: T::Balance) -> Self { - PositiveImbalance(amount) - } - } - - /// Opaque, move-only struct with private fields that serves as a token denoting that - /// funds have been destroyed without any equal and opposite accounting. - #[must_use] - #[derive(RuntimeDebug, PartialEq, Eq)] - pub struct NegativeImbalance, I: 'static = ()>(T::Balance); - - impl, I: 'static> NegativeImbalance { - /// Create a new negative imbalance from a balance. - pub fn new(amount: T::Balance) -> Self { - NegativeImbalance(amount) - } - } - - impl, I: 'static> TryDrop for PositiveImbalance { - fn try_drop(self) -> result::Result<(), Self> { - self.drop_zero() - } - } - - impl, I: 'static> Default for PositiveImbalance { - fn default() -> Self { - Self::zero() - } - } - - impl, I: 'static> Imbalance for PositiveImbalance { - type Opposite = NegativeImbalance; - - fn zero() -> Self { - Self(Zero::zero()) - } - fn drop_zero(self) -> result::Result<(), Self> { - if self.0.is_zero() { - Ok(()) - } else { - Err(self) - } - } - fn split(self, amount: T::Balance) -> (Self, Self) { - let first = self.0.min(amount); - let second = self.0 - first; - - mem::forget(self); - (Self(first), Self(second)) - } - fn extract(&mut self, amount: T::Balance) -> Self { - let new = self.0.min(amount); - self.0 -= new; - Self(new) - } - fn merge(mut self, other: Self) -> Self { - self.0 = self.0.saturating_add(other.0); - mem::forget(other); - - self - } - fn subsume(&mut self, other: Self) { - self.0 = self.0.saturating_add(other.0); - mem::forget(other); - } - fn offset(self, other: Self::Opposite) -> SameOrOther { - let (a, b) = (self.0, other.0); - mem::forget((self, other)); - - match a.cmp(&b) { - Ordering::Greater => SameOrOther::Same(Self(a - b)), - Ordering::Less => SameOrOther::Other(NegativeImbalance::new(b - a)), - Ordering::Equal => SameOrOther::None, - } - } - fn peek(&self) -> T::Balance { - self.0 - } - } - - impl, I: 'static> TryMerge for PositiveImbalance { - fn try_merge(self, other: Self) -> Result { - Ok(self.merge(other)) - } - } - - impl, I: 'static> TryDrop for NegativeImbalance { - fn try_drop(self) -> result::Result<(), Self> { - self.drop_zero() - } - } - - impl, I: 'static> Default for NegativeImbalance { - fn default() -> Self { - Self::zero() - } - } - - impl, I: 'static> Imbalance for NegativeImbalance { - type Opposite = PositiveImbalance; - - fn zero() -> Self { - Self(Zero::zero()) - } - fn drop_zero(self) -> result::Result<(), Self> { - if self.0.is_zero() { - Ok(()) - } else { - Err(self) - } - } - fn split(self, amount: T::Balance) -> (Self, Self) { - let first = self.0.min(amount); - let second = self.0 - first; - - mem::forget(self); - (Self(first), Self(second)) - } - fn extract(&mut self, amount: T::Balance) -> Self { - let new = self.0.min(amount); - self.0 -= new; - Self(new) - } - fn merge(mut self, other: Self) -> Self { - self.0 = self.0.saturating_add(other.0); - mem::forget(other); - - self - } - fn subsume(&mut self, other: Self) { - self.0 = self.0.saturating_add(other.0); - mem::forget(other); - } - fn offset(self, other: Self::Opposite) -> SameOrOther { - let (a, b) = (self.0, other.0); - mem::forget((self, other)); - - match a.cmp(&b) { - Ordering::Greater => SameOrOther::Same(Self(a - b)), - Ordering::Less => SameOrOther::Other(PositiveImbalance::new(b - a)), - Ordering::Equal => SameOrOther::None, - } - } - fn peek(&self) -> T::Balance { - self.0 - } - } - - impl, I: 'static> TryMerge for NegativeImbalance { - fn try_merge(self, other: Self) -> Result { - Ok(self.merge(other)) - } - } - - impl, I: 'static> Drop for PositiveImbalance { - /// Basic drop handler will just square up the total issuance. - fn drop(&mut self) { - if !self.0.is_zero() { - >::mutate(|v| *v = v.saturating_add(self.0)); - Pallet::::deposit_event(Event::::Issued { amount: self.0 }); - } - } - } - - impl, I: 'static> Drop for NegativeImbalance { - /// Basic drop handler will just square up the total issuance. - fn drop(&mut self) { - if !self.0.is_zero() { - >::mutate(|v| *v = v.saturating_sub(self.0)); - Pallet::::deposit_event(Event::::Rescinded { amount: self.0 }); - } - } - } -} - -impl, I: 'static> Currency for Pallet -where - T::Balance: Balance, -{ - type Balance = T::Balance; - type PositiveImbalance = PositiveImbalance; - type NegativeImbalance = NegativeImbalance; - - fn total_balance(who: &T::AccountId) -> Self::Balance { - Self::account(who).total() - } - - // Check if `value` amount of free balance can be slashed from `who`. - fn can_slash(who: &T::AccountId, value: Self::Balance) -> bool { - if value.is_zero() { - return true; - } - Self::free_balance(who) >= value - } - - fn total_issuance() -> Self::Balance { - TotalIssuance::::get() - } - - fn active_issuance() -> Self::Balance { - >::active_issuance() - } - - fn deactivate(amount: Self::Balance) { - >::deactivate(amount); - } - - fn reactivate(amount: Self::Balance) { - >::reactivate(amount); - } - - fn minimum_balance() -> Self::Balance { - T::ExistentialDeposit::get() - } - - // Burn funds from the total issuance, returning a positive imbalance for the amount burned. - // Is a no-op if amount to be burned is zero. - fn burn(mut amount: Self::Balance) -> Self::PositiveImbalance { - if amount.is_zero() { - return PositiveImbalance::zero(); - } - >::mutate(|issued| { - *issued = issued.checked_sub(&amount).unwrap_or_else(|| { - amount = *issued; - Zero::zero() - }); - }); - - Pallet::::deposit_event(Event::::Rescinded { amount }); - PositiveImbalance::new(amount) - } - - // Create new funds into the total issuance, returning a negative imbalance - // for the amount issued. - // Is a no-op if amount to be issued it zero. - fn issue(mut amount: Self::Balance) -> Self::NegativeImbalance { - if amount.is_zero() { - return NegativeImbalance::zero(); - } - >::mutate(|issued| { - *issued = issued.checked_add(&amount).unwrap_or_else(|| { - amount = Self::Balance::max_value() - *issued; - Self::Balance::max_value() - }) - }); - - Pallet::::deposit_event(Event::::Issued { amount }); - NegativeImbalance::new(amount) - } - - fn free_balance(who: &T::AccountId) -> Self::Balance { - Self::account(who).free - } - - // Ensure that an account can withdraw from their free balance given any existing withdrawal - // restrictions like locks and vesting balance. - // Is a no-op if amount to be withdrawn is zero. - fn ensure_can_withdraw( - who: &T::AccountId, - amount: T::Balance, - _reasons: WithdrawReasons, - new_balance: T::Balance, - ) -> DispatchResult { - if amount.is_zero() { - return Ok(()); - } - ensure!(new_balance >= Self::account(who).frozen, Error::::LiquidityRestrictions); - Ok(()) - } - - // Transfer some free balance from `transactor` to `dest`, respecting existence requirements. - // Is a no-op if value to be transferred is zero or the `transactor` is the same as `dest`. - fn transfer( - transactor: &T::AccountId, - dest: &T::AccountId, - value: Self::Balance, - existence_requirement: ExistenceRequirement, - ) -> DispatchResult { - if value.is_zero() || transactor == dest { - return Ok(()); - } - let keep_alive = match existence_requirement { - ExistenceRequirement::KeepAlive => Preserve, - ExistenceRequirement::AllowDeath => Expendable, - }; - >::transfer(transactor, dest, value, keep_alive)?; - Ok(()) - } - - /// Slash a target account `who`, returning the negative imbalance created and any left over - /// amount that could not be slashed. - /// - /// Is a no-op if `value` to be slashed is zero or the account does not exist. - /// - /// NOTE: `slash()` prefers free balance, but assumes that reserve balance can be drawn - /// from in extreme circumstances. `can_slash()` should be used prior to `slash()` to avoid - /// having to draw from reserved funds, however we err on the side of punishment if things are - /// inconsistent or `can_slash` wasn't used appropriately. - fn slash(who: &T::AccountId, value: Self::Balance) -> (Self::NegativeImbalance, Self::Balance) { - if value.is_zero() { - return (NegativeImbalance::zero(), Zero::zero()); - } - if Self::total_balance(who).is_zero() { - return (NegativeImbalance::zero(), value); - } - - match Self::try_mutate_account_handling_dust( - who, - |account, _is_new| -> Result<(Self::NegativeImbalance, Self::Balance), DispatchError> { - // Best value is the most amount we can slash following liveness rules. - let ed = T::ExistentialDeposit::get(); - let actual = match system::Pallet::::can_dec_provider(who) { - true => value.min(account.free), - false => value.min(account.free.saturating_sub(ed)), - }; - account.free.saturating_reduce(actual); - let remaining = value.saturating_sub(actual); - Ok((NegativeImbalance::new(actual), remaining)) - }, - ) { - Ok((imbalance, remaining)) => { - Self::deposit_event(Event::Slashed { - who: who.clone(), - amount: value.saturating_sub(remaining), - }); - (imbalance, remaining) - }, - Err(_) => (Self::NegativeImbalance::zero(), value), - } - } - - /// Deposit some `value` into the free balance of an existing target account `who`. - /// - /// Is a no-op if the `value` to be deposited is zero. - fn deposit_into_existing( - who: &T::AccountId, - value: Self::Balance, - ) -> Result { - if value.is_zero() { - return Ok(PositiveImbalance::zero()); - } - - Self::try_mutate_account_handling_dust( - who, - |account, is_new| -> Result { - ensure!(!is_new, Error::::DeadAccount); - account.free = account.free.checked_add(&value).ok_or(ArithmeticError::Overflow)?; - Self::deposit_event(Event::Deposit { who: who.clone(), amount: value }); - Ok(PositiveImbalance::new(value)) - }, - ) - } - - /// Deposit some `value` into the free balance of `who`, possibly creating a new account. - /// - /// This function is a no-op if: - /// - the `value` to be deposited is zero; or - /// - the `value` to be deposited is less than the required ED and the account does not yet - /// exist; or - /// - the deposit would necessitate the account to exist and there are no provider references; - /// or - /// - `value` is so large it would cause the balance of `who` to overflow. - fn deposit_creating(who: &T::AccountId, value: Self::Balance) -> Self::PositiveImbalance { - if value.is_zero() { - return Self::PositiveImbalance::zero(); - } - - Self::try_mutate_account_handling_dust( - who, - |account, is_new| -> Result { - let ed = T::ExistentialDeposit::get(); - ensure!(value >= ed || !is_new, Error::::ExistentialDeposit); - - // defensive only: overflow should never happen, however in case it does, then this - // operation is a no-op. - account.free = match account.free.checked_add(&value) { - Some(x) => x, - None => return Ok(Self::PositiveImbalance::zero()), - }; - - Self::deposit_event(Event::Deposit { who: who.clone(), amount: value }); - Ok(PositiveImbalance::new(value)) - }, - ) - .unwrap_or_else(|_| Self::PositiveImbalance::zero()) - } - - /// Withdraw some free balance from an account, respecting existence requirements. - /// - /// Is a no-op if value to be withdrawn is zero. - fn withdraw( - who: &T::AccountId, - value: Self::Balance, - reasons: WithdrawReasons, - liveness: ExistenceRequirement, - ) -> result::Result { - if value.is_zero() { - return Ok(NegativeImbalance::zero()); - } - - Self::try_mutate_account_handling_dust( - who, - |account, _| -> Result { - let new_free_account = - account.free.checked_sub(&value).ok_or(Error::::InsufficientBalance)?; - - // bail if we need to keep the account alive and this would kill it. - let ed = T::ExistentialDeposit::get(); - let would_be_dead = new_free_account < ed; - let would_kill = would_be_dead && account.free >= ed; - ensure!(liveness == AllowDeath || !would_kill, Error::::Expendability); - - Self::ensure_can_withdraw(who, value, reasons, new_free_account)?; - - account.free = new_free_account; - - Self::deposit_event(Event::Withdraw { who: who.clone(), amount: value }); - Ok(NegativeImbalance::new(value)) - }, - ) - } - - /// Force the new free balance of a target account `who` to some new value `balance`. - fn make_free_balance_be( - who: &T::AccountId, - value: Self::Balance, - ) -> SignedImbalance { - Self::try_mutate_account_handling_dust( - who, - |account, - is_new| - -> Result, DispatchError> { - let ed = T::ExistentialDeposit::get(); - // If we're attempting to set an existing account to less than ED, then - // bypass the entire operation. It's a no-op if you follow it through, but - // since this is an instance where we might account for a negative imbalance - // (in the dust cleaner of set_account) before we account for its actual - // equal and opposite cause (returned as an Imbalance), then in the - // instance that there's no other accounts on the system at all, we might - // underflow the issuance and our arithmetic will be off. - ensure!(value >= ed || !is_new, Error::::ExistentialDeposit); - - let imbalance = if account.free <= value { - SignedImbalance::Positive(PositiveImbalance::new(value - account.free)) - } else { - SignedImbalance::Negative(NegativeImbalance::new(account.free - value)) - }; - account.free = value; - Self::deposit_event(Event::BalanceSet { who: who.clone(), free: account.free }); - Ok(imbalance) - }, - ) - .unwrap_or_else(|_| SignedImbalance::Positive(Self::PositiveImbalance::zero())) - } -} - -impl, I: 'static> ReservableCurrency for Pallet -where - T::Balance: Balance, -{ - /// Check if `who` can reserve `value` from their free balance. - /// - /// Always `true` if value to be reserved is zero. - fn can_reserve(who: &T::AccountId, value: Self::Balance) -> bool { - if value.is_zero() { - return true; - } - Self::account(who).free.checked_sub(&value).is_some_and(|new_balance| { - new_balance >= T::ExistentialDeposit::get() && - Self::ensure_can_withdraw(who, value, WithdrawReasons::RESERVE, new_balance) - .is_ok() - }) - } - - fn reserved_balance(who: &T::AccountId) -> Self::Balance { - Self::account(who).reserved - } - - /// Move `value` from the free balance from `who` to their reserved balance. - /// - /// Is a no-op if value to be reserved is zero. - fn reserve(who: &T::AccountId, value: Self::Balance) -> DispatchResult { - if value.is_zero() { - return Ok(()); - } - - Self::try_mutate_account_handling_dust(who, |account, _| -> DispatchResult { - account.free = - account.free.checked_sub(&value).ok_or(Error::::InsufficientBalance)?; - account.reserved = - account.reserved.checked_add(&value).ok_or(ArithmeticError::Overflow)?; - Self::ensure_can_withdraw(who, value, WithdrawReasons::RESERVE, account.free) - })?; - - Self::deposit_event(Event::Reserved { who: who.clone(), amount: value }); - Ok(()) - } - - /// Unreserve some funds, returning any amount that was unable to be unreserved. - /// - /// Is a no-op if the value to be unreserved is zero or the account does not exist. - /// - /// NOTE: returns amount value which wasn't successfully unreserved. - fn unreserve(who: &T::AccountId, value: Self::Balance) -> Self::Balance { - if value.is_zero() { - return Zero::zero(); - } - if Self::total_balance(who).is_zero() { - return value; - } - - let actual = match Self::mutate_account_handling_dust(who, |account| { - let actual = cmp::min(account.reserved, value); - account.reserved -= actual; - // defensive only: this can never fail since total issuance which is at least - // free+reserved fits into the same data type. - account.free = account.free.defensive_saturating_add(actual); - actual - }) { - Ok(x) => x, - Err(_) => { - // This should never happen since we don't alter the total amount in the account. - // If it ever does, then we should fail gracefully though, indicating that nothing - // could be done. - return value; - }, - }; - - Self::deposit_event(Event::Unreserved { who: who.clone(), amount: actual }); - value - actual - } - - /// Slash from reserved balance, returning the negative imbalance created, - /// and any amount that was unable to be slashed. - /// - /// Is a no-op if the value to be slashed is zero or the account does not exist. - fn slash_reserved( - who: &T::AccountId, - value: Self::Balance, - ) -> (Self::NegativeImbalance, Self::Balance) { - if value.is_zero() { - return (NegativeImbalance::zero(), Zero::zero()); - } - if Self::total_balance(who).is_zero() { - return (NegativeImbalance::zero(), value); - } - - // NOTE: `mutate_account` may fail if it attempts to reduce the balance to the point that an - // account is attempted to be illegally destroyed. - - match Self::mutate_account_handling_dust(who, |account| { - let actual = value.min(account.reserved); - account.reserved.saturating_reduce(actual); - - // underflow should never happen, but it if does, there's nothing to be done here. - (NegativeImbalance::new(actual), value.saturating_sub(actual)) - }) { - Ok((imbalance, not_slashed)) => { - Self::deposit_event(Event::Slashed { - who: who.clone(), - amount: value.saturating_sub(not_slashed), - }); - (imbalance, not_slashed) - }, - Err(_) => (Self::NegativeImbalance::zero(), value), - } - } - - /// Move the reserved balance of one account into the balance of another, according to `status`. - /// - /// Is a no-op if: - /// - the value to be moved is zero; or - /// - the `slashed` id equal to `beneficiary` and the `status` is `Reserved`. - /// - /// This is `Polite` and thus will not repatriate any funds which would lead the total balance - /// to be less than the frozen amount. Returns `Ok` with the actual amount of funds moved, - /// which may be less than `value` since the operation is done an a `BestEffort` basis. - fn repatriate_reserved( - slashed: &T::AccountId, - beneficiary: &T::AccountId, - value: Self::Balance, - status: Status, - ) -> Result { - let actual = - Self::do_transfer_reserved(slashed, beneficiary, value, BestEffort, Polite, status)?; - Ok(value.saturating_sub(actual)) - } -} - -impl, I: 'static> NamedReservableCurrency for Pallet -where - T::Balance: Balance, -{ - type ReserveIdentifier = T::ReserveIdentifier; - - fn reserved_balance_named(id: &Self::ReserveIdentifier, who: &T::AccountId) -> Self::Balance { - let reserves = Self::reserves(who); - reserves - .binary_search_by_key(id, |data| data.id) - .map(|index| reserves[index].amount) - .unwrap_or_default() - } - - /// Move `value` from the free balance from `who` to a named reserve balance. - /// - /// Is a no-op if value to be reserved is zero. - fn reserve_named( - id: &Self::ReserveIdentifier, - who: &T::AccountId, - value: Self::Balance, - ) -> DispatchResult { - if value.is_zero() { - return Ok(()); - } - - Reserves::::try_mutate(who, |reserves| -> DispatchResult { - match reserves.binary_search_by_key(id, |data| data.id) { - Ok(index) => { - reserves[index].amount = reserves[index] - .amount - .checked_add(&value) - .ok_or(ArithmeticError::Overflow)?; - }, - Err(index) => { - reserves - .try_insert(index, ReserveData { id: *id, amount: value }) - .map_err(|_| Error::::TooManyReserves)?; - }, - }; - >::reserve(who, value)?; - Ok(()) - }) - } - - /// Unreserve some funds, returning any amount that was unable to be unreserved. - /// - /// Is a no-op if the value to be unreserved is zero. - fn unreserve_named( - id: &Self::ReserveIdentifier, - who: &T::AccountId, - value: Self::Balance, - ) -> Self::Balance { - if value.is_zero() { - return Zero::zero(); - } - - Reserves::::mutate_exists(who, |maybe_reserves| -> Self::Balance { - if let Some(reserves) = maybe_reserves.as_mut() { - match reserves.binary_search_by_key(id, |data| data.id) { - Ok(index) => { - let to_change = cmp::min(reserves[index].amount, value); - - let remain = >::unreserve(who, to_change); - - // remain should always be zero but just to be defensive here. - let actual = to_change.defensive_saturating_sub(remain); - - // `actual <= to_change` and `to_change <= amount`; qed; - reserves[index].amount -= actual; - - if reserves[index].amount.is_zero() { - if reserves.len() == 1 { - // no more named reserves - *maybe_reserves = None; - } else { - // remove this named reserve - reserves.remove(index); - } - } - - value - actual - }, - Err(_) => value, - } - } else { - value - } - }) - } - - /// Slash from reserved balance, returning the negative imbalance created, - /// and any amount that was unable to be slashed. - /// - /// Is a no-op if the value to be slashed is zero. - fn slash_reserved_named( - id: &Self::ReserveIdentifier, - who: &T::AccountId, - value: Self::Balance, - ) -> (Self::NegativeImbalance, Self::Balance) { - if value.is_zero() { - return (NegativeImbalance::zero(), Zero::zero()); - } - - Reserves::::mutate(who, |reserves| -> (Self::NegativeImbalance, Self::Balance) { - match reserves.binary_search_by_key(id, |data| data.id) { - Ok(index) => { - let to_change = cmp::min(reserves[index].amount, value); - - let (imb, remain) = - >::slash_reserved(who, to_change); - - // remain should always be zero but just to be defensive here. - let actual = to_change.defensive_saturating_sub(remain); - - // `actual <= to_change` and `to_change <= amount`; qed; - reserves[index].amount -= actual; - - Self::deposit_event(Event::Slashed { who: who.clone(), amount: actual }); - (imb, value - actual) - }, - Err(_) => (NegativeImbalance::zero(), value), - } - }) - } - - /// Move the reserved balance of one account into the balance of another, according to `status`. - /// If `status` is `Reserved`, the balance will be reserved with given `id`. - /// - /// Is a no-op if: - /// - the value to be moved is zero; or - /// - the `slashed` id equal to `beneficiary` and the `status` is `Reserved`. - fn repatriate_reserved_named( - id: &Self::ReserveIdentifier, - slashed: &T::AccountId, - beneficiary: &T::AccountId, - value: Self::Balance, - status: Status, - ) -> Result { - if value.is_zero() { - return Ok(Zero::zero()); - } - - if slashed == beneficiary { - return match status { - Status::Free => Ok(Self::unreserve_named(id, slashed, value)), - Status::Reserved => - Ok(value.saturating_sub(Self::reserved_balance_named(id, slashed))), - }; - } - - Reserves::::try_mutate(slashed, |reserves| -> Result { - match reserves.binary_search_by_key(id, |data| data.id) { - Ok(index) => { - let to_change = cmp::min(reserves[index].amount, value); - - let actual = if status == Status::Reserved { - // make it the reserved under same identifier - Reserves::::try_mutate( - beneficiary, - |reserves| -> Result { - match reserves.binary_search_by_key(id, |data| data.id) { - Ok(index) => { - let remain = - >::repatriate_reserved( - slashed, - beneficiary, - to_change, - status, - )?; - - // remain should always be zero but just to be defensive - // here. - let actual = to_change.defensive_saturating_sub(remain); - - // this add can't overflow but just to be defensive. - reserves[index].amount = - reserves[index].amount.defensive_saturating_add(actual); - - Ok(actual) - }, - Err(index) => { - let remain = - >::repatriate_reserved( - slashed, - beneficiary, - to_change, - status, - )?; - - // remain should always be zero but just to be defensive - // here - let actual = to_change.defensive_saturating_sub(remain); - - reserves - .try_insert( - index, - ReserveData { id: *id, amount: actual }, - ) - .map_err(|_| Error::::TooManyReserves)?; - - Ok(actual) - }, - } - }, - )? - } else { - let remain = >::repatriate_reserved( - slashed, - beneficiary, - to_change, - status, - )?; - - // remain should always be zero but just to be defensive here - to_change.defensive_saturating_sub(remain) - }; - - // `actual <= to_change` and `to_change <= amount`; qed; - reserves[index].amount -= actual; - - Ok(value - actual) - }, - Err(_) => Ok(value), - } - }) - } -} - -impl, I: 'static> LockableCurrency for Pallet -where - T::Balance: Balance, -{ - type Moment = BlockNumberFor; - - type MaxLocks = T::MaxLocks; - - // Set or alter a lock on the balance of `who`. - fn set_lock( - id: LockIdentifier, - who: &T::AccountId, - amount: T::Balance, - reasons: WithdrawReasons, - ) { - if reasons.is_empty() || amount.is_zero() { - Self::remove_lock(id, who); - return; - } - - let mut new_lock = Some(BalanceLock { id, amount, reasons: reasons.into() }); - let mut locks = Self::locks(who) - .into_iter() - .filter_map(|l| if l.id == id { new_lock.take() } else { Some(l) }) - .collect::>(); - if let Some(lock) = new_lock { - locks.push(lock) - } - Self::update_locks(who, &locks[..]); - } - - // Extend a lock on the balance of `who`. - // Is a no-op if lock amount is zero or `reasons` `is_none()`. - fn extend_lock( - id: LockIdentifier, - who: &T::AccountId, - amount: T::Balance, - reasons: WithdrawReasons, - ) { - if amount.is_zero() || reasons.is_empty() { - return; - } - let mut new_lock = Some(BalanceLock { id, amount, reasons: reasons.into() }); - let mut locks = Self::locks(who) - .into_iter() - .filter_map(|l| { - if l.id == id { - new_lock.take().map(|nl| BalanceLock { - id: l.id, - amount: l.amount.max(nl.amount), - reasons: l.reasons | nl.reasons, - }) - } else { - Some(l) - } - }) - .collect::>(); - if let Some(lock) = new_lock { - locks.push(lock) - } - Self::update_locks(who, &locks[..]); - } - - fn remove_lock(id: LockIdentifier, who: &T::AccountId) { - let mut locks = Self::locks(who); - locks.retain(|l| l.id != id); - Self::update_locks(who, &locks[..]); - } -} - -impl, I: 'static> InspectLockableCurrency for Pallet { - fn balance_locked(id: LockIdentifier, who: &T::AccountId) -> Self::Balance { - Self::locks(who) - .into_iter() - .filter(|l| l.id == id) - .fold(Zero::zero(), |acc, l| acc + l.amount) - } -} diff --git a/pallets/balances/src/impl_fungible.rs b/pallets/balances/src/impl_fungible.rs deleted file mode 100644 index e5e43e04..00000000 --- a/pallets/balances/src/impl_fungible.rs +++ /dev/null @@ -1,385 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Implementation of `fungible` traits for Balances pallet. -use super::*; -use frame_support::traits::{ - tokens::{ - Fortitude, - Preservation::{self, Preserve, Protect}, - Provenance::{self, Minted}, - }, - AccountTouch, -}; - -impl, I: 'static> fungible::Inspect for Pallet { - type Balance = T::Balance; - - fn total_issuance() -> Self::Balance { - TotalIssuance::::get() - } - fn active_issuance() -> Self::Balance { - TotalIssuance::::get().saturating_sub(InactiveIssuance::::get()) - } - fn minimum_balance() -> Self::Balance { - T::ExistentialDeposit::get() - } - fn total_balance(who: &T::AccountId) -> Self::Balance { - Self::account(who).total() - } - fn balance(who: &T::AccountId) -> Self::Balance { - Self::account(who).free - } - fn reducible_balance( - who: &T::AccountId, - preservation: Preservation, - force: Fortitude, - ) -> Self::Balance { - let a = Self::account(who); - let mut untouchable = Zero::zero(); - if force == Polite { - // Frozen balance applies to total. Anything on hold therefore gets discounted from the - // limit given by the freezes. - untouchable = a.frozen.saturating_sub(a.reserved); - } - // If we want to keep our provider ref.. - if preservation == Preserve - // ..or we don't want the account to die and our provider ref is needed for it to live.. - || preservation == Protect && !a.free.is_zero() && - frame_system::Pallet::::providers(who) == 1 - // ..or we don't care about the account dying but our provider ref is required.. - || preservation == Expendable && !a.free.is_zero() && - !frame_system::Pallet::::can_dec_provider(who) - { - // ..then the ED needed.. - untouchable = untouchable.max(T::ExistentialDeposit::get()); - } - // Liquid balance is what is neither on hold nor frozen/required for provider. - a.free.saturating_sub(untouchable) - } - fn can_deposit( - who: &T::AccountId, - amount: Self::Balance, - provenance: Provenance, - ) -> DepositConsequence { - if amount.is_zero() { - return DepositConsequence::Success; - } - - if provenance == Minted && TotalIssuance::::get().checked_add(&amount).is_none() { - return DepositConsequence::Overflow; - } - - let account = Self::account(who); - let new_free = match account.free.checked_add(&amount) { - None => return DepositConsequence::Overflow, - Some(x) if x < T::ExistentialDeposit::get() => return DepositConsequence::BelowMinimum, - Some(x) => x, - }; - - match account.reserved.checked_add(&new_free) { - Some(_) => {}, - None => return DepositConsequence::Overflow, - }; - - // NOTE: We assume that we are a provider, so don't need to do any checks in the - // case of account creation. - - DepositConsequence::Success - } - fn can_withdraw( - who: &T::AccountId, - amount: Self::Balance, - ) -> WithdrawConsequence { - if amount.is_zero() { - return WithdrawConsequence::Success; - } - - if TotalIssuance::::get().checked_sub(&amount).is_none() { - return WithdrawConsequence::Underflow; - } - - let account = Self::account(who); - let new_free_balance = match account.free.checked_sub(&amount) { - Some(x) => x, - None => return WithdrawConsequence::BalanceLow, - }; - - let liquid = Self::reducible_balance(who, Expendable, Polite); - if amount > liquid { - return WithdrawConsequence::Frozen; - } - - // Provider restriction - total account balance cannot be reduced to zero if it cannot - // sustain the loss of a provider reference. - // NOTE: This assumes that the pallet is a provider (which is true). Is this ever changes, - // then this will need to adapt accordingly. - let ed = T::ExistentialDeposit::get(); - let success = if new_free_balance < ed { - if frame_system::Pallet::::can_dec_provider(who) { - WithdrawConsequence::ReducedToZero(new_free_balance) - } else { - return WithdrawConsequence::WouldDie; - } - } else { - WithdrawConsequence::Success - }; - - let new_total_balance = new_free_balance.saturating_add(account.reserved); - - // Eventual free funds must be no less than the frozen balance. - if new_total_balance < account.frozen { - return WithdrawConsequence::Frozen; - } - - success - } -} - -impl, I: 'static> fungible::Unbalanced for Pallet { - fn handle_dust(dust: fungible::Dust) { - T::DustRemoval::on_unbalanced(dust.into_credit()); - } - fn write_balance( - who: &T::AccountId, - amount: Self::Balance, - ) -> Result, DispatchError> { - let max_reduction = - >::reducible_balance(who, Expendable, Force); - let (result, maybe_dust) = Self::mutate_account(who, |account| -> DispatchResult { - // Make sure the reduction (if there is one) is no more than the maximum allowed. - let reduction = account.free.saturating_sub(amount); - ensure!(reduction <= max_reduction, Error::::InsufficientBalance); - - account.free = amount; - Ok(()) - })?; - result?; - Ok(maybe_dust) - } - - fn set_total_issuance(amount: Self::Balance) { - TotalIssuance::::mutate(|t| *t = amount); - } - - fn deactivate(amount: Self::Balance) { - InactiveIssuance::::mutate(|b| { - // InactiveIssuance cannot be greater than TotalIssuance. - *b = b.saturating_add(amount).min(TotalIssuance::::get()); - }); - } - - fn reactivate(amount: Self::Balance) { - InactiveIssuance::::mutate(|b| b.saturating_reduce(amount)); - } -} - -impl, I: 'static> fungible::Mutate for Pallet { - fn done_mint_into(who: &T::AccountId, amount: Self::Balance) { - Self::deposit_event(Event::::Minted { who: who.clone(), amount }); - } - fn done_burn_from(who: &T::AccountId, amount: Self::Balance) { - Self::deposit_event(Event::::Burned { who: who.clone(), amount }); - } - fn done_shelve(who: &T::AccountId, amount: Self::Balance) { - Self::deposit_event(Event::::Suspended { who: who.clone(), amount }); - } - fn done_restore(who: &T::AccountId, amount: Self::Balance) { - Self::deposit_event(Event::::Restored { who: who.clone(), amount }); - } - fn done_transfer(source: &T::AccountId, dest: &T::AccountId, amount: Self::Balance) { - Self::deposit_event(Event::::Transfer { - from: source.clone(), - to: dest.clone(), - amount, - }); - } -} - -impl, I: 'static> fungible::MutateHold for Pallet {} - -impl, I: 'static> fungible::InspectHold for Pallet { - type Reason = T::RuntimeHoldReason; - - fn total_balance_on_hold(who: &T::AccountId) -> T::Balance { - Self::account(who).reserved - } - fn reducible_total_balance_on_hold(who: &T::AccountId, force: Fortitude) -> Self::Balance { - // The total balance must never drop below the freeze requirements if we're not forcing: - let a = Self::account(who); - let unavailable = if force == Force { - Self::Balance::zero() - } else { - // The freeze lock applies to the total balance, so we can discount the free balance - // from the amount which the total reserved balance must provide to satisfy it. - a.frozen.saturating_sub(a.free) - }; - a.reserved.saturating_sub(unavailable) - } - fn balance_on_hold(reason: &Self::Reason, who: &T::AccountId) -> T::Balance { - Holds::::get(who) - .iter() - .find(|x| &x.id == reason) - .map_or_else(Zero::zero, |x| x.amount) - } - fn hold_available(reason: &Self::Reason, who: &T::AccountId) -> bool { - if frame_system::Pallet::::providers(who) == 0 { - return false; - } - let holds = Holds::::get(who); - if holds.is_full() && !holds.iter().any(|x| &x.id == reason) { - return false; - } - true - } -} - -impl, I: 'static> fungible::UnbalancedHold for Pallet { - fn set_balance_on_hold( - reason: &Self::Reason, - who: &T::AccountId, - amount: Self::Balance, - ) -> DispatchResult { - let mut new_account = Self::account(who); - let mut holds = Holds::::get(who); - let mut increase = true; - let mut delta = amount; - - if let Some(item) = holds.iter_mut().find(|x| &x.id == reason) { - delta = item.amount.max(amount) - item.amount.min(amount); - increase = amount > item.amount; - item.amount = amount; - holds.retain(|x| !x.amount.is_zero()); - } else if !amount.is_zero() { - holds - .try_push(IdAmount { id: *reason, amount }) - .map_err(|_| Error::::TooManyHolds)?; - } - - new_account.reserved = if increase { - new_account.reserved.checked_add(&delta).ok_or(ArithmeticError::Overflow)? - } else { - new_account.reserved.checked_sub(&delta).ok_or(ArithmeticError::Underflow)? - }; - - let (result, maybe_dust) = Self::try_mutate_account(who, |a, _| -> DispatchResult { - *a = new_account; - Ok(()) - })?; - debug_assert!( - maybe_dust.is_none(), - "Does not alter main balance; dust only happens when it is altered; qed" - ); - Holds::::insert(who, holds); - Ok(result) - } -} - -impl, I: 'static> fungible::InspectFreeze for Pallet { - type Id = T::FreezeIdentifier; - - fn balance_frozen(id: &Self::Id, who: &T::AccountId) -> Self::Balance { - let locks = Freezes::::get(who); - locks.into_iter().find(|l| &l.id == id).map_or(Zero::zero(), |l| l.amount) - } - - fn can_freeze(id: &Self::Id, who: &T::AccountId) -> bool { - let l = Freezes::::get(who); - !l.is_full() || l.iter().any(|x| &x.id == id) - } -} - -impl, I: 'static> fungible::MutateFreeze for Pallet { - fn set_freeze(id: &Self::Id, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { - if amount.is_zero() { - return Self::thaw(id, who); - } - let mut locks = Freezes::::get(who); - if let Some(i) = locks.iter_mut().find(|x| &x.id == id) { - i.amount = amount; - } else { - locks - .try_push(IdAmount { id: *id, amount }) - .map_err(|_| Error::::TooManyFreezes)?; - } - Self::update_freezes(who, locks.as_bounded_slice()) - } - - fn extend_freeze(id: &Self::Id, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { - if amount.is_zero() { - return Ok(()); - } - let mut locks = Freezes::::get(who); - if let Some(i) = locks.iter_mut().find(|x| &x.id == id) { - i.amount = i.amount.max(amount); - } else { - locks - .try_push(IdAmount { id: *id, amount }) - .map_err(|_| Error::::TooManyFreezes)?; - } - Self::update_freezes(who, locks.as_bounded_slice()) - } - - fn thaw(id: &Self::Id, who: &T::AccountId) -> DispatchResult { - let mut locks = Freezes::::get(who); - locks.retain(|l| &l.id != id); - Self::update_freezes(who, locks.as_bounded_slice()) - } -} - -impl, I: 'static> fungible::Balanced for Pallet { - type OnDropCredit = fungible::DecreaseIssuance; - type OnDropDebt = fungible::IncreaseIssuance; - - fn done_deposit(who: &T::AccountId, amount: Self::Balance) { - Self::deposit_event(Event::::Deposit { who: who.clone(), amount }); - } - fn done_withdraw(who: &T::AccountId, amount: Self::Balance) { - Self::deposit_event(Event::::Withdraw { who: who.clone(), amount }); - } - fn done_issue(amount: Self::Balance) { - if !amount.is_zero() { - Self::deposit_event(Event::::Issued { amount }); - } - } - fn done_rescind(amount: Self::Balance) { - Self::deposit_event(Event::::Rescinded { amount }); - } -} - -impl, I: 'static> fungible::BalancedHold for Pallet {} - -impl, I: 'static> - fungible::hold::DoneSlash for Pallet -{ - fn done_slash(reason: &T::RuntimeHoldReason, who: &T::AccountId, amount: T::Balance) { - T::DoneSlashHandler::done_slash(reason, who, amount); - } -} - -impl, I: 'static> AccountTouch<(), T::AccountId> for Pallet { - type Balance = T::Balance; - fn deposit_required(_: ()) -> Self::Balance { - Self::Balance::zero() - } - fn should_touch(_: (), _: &T::AccountId) -> bool { - false - } - fn touch(_: (), _: &T::AccountId, _: &T::AccountId) -> DispatchResult { - Ok(()) - } -} diff --git a/pallets/balances/src/impl_proofs.rs b/pallets/balances/src/impl_proofs.rs deleted file mode 100644 index 517225f5..00000000 --- a/pallets/balances/src/impl_proofs.rs +++ /dev/null @@ -1,28 +0,0 @@ -use super::*; -use qp_wormhole::TransferProofs; - -impl, I: 'static> TransferProofs - for Pallet -{ - fn transfer_proof_exists( - count: TransferCountType, - from: &T::AccountId, - to: &T::AccountId, - value: T::Balance, - ) -> bool { - TransferProof::::get((count, from, to, value)).is_some() - } - - fn store_transfer_proof(from: &T::AccountId, to: &T::AccountId, value: T::Balance) { - Pallet::::do_store_transfer_proof(from, to, value); - } - - fn transfer_proof_key( - transfer_count: u64, - from: T::AccountId, - to: T::AccountId, - value: T::Balance, - ) -> Vec { - Pallet::::transfer_proof_storage_key(transfer_count, from, to, value) - } -} diff --git a/pallets/balances/src/lib.rs b/pallets/balances/src/lib.rs deleted file mode 100644 index 583ea420..00000000 --- a/pallets/balances/src/lib.rs +++ /dev/null @@ -1,1338 +0,0 @@ -// This file is part of Substrate. - -#![allow(clippy::all)] -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! # Balances Pallet -//! -//! The Balances pallet provides functionality for handling accounts and balances for a single -//! token. -//! -//! It makes heavy use of concepts such as Holds and Freezes from the -//! [`frame_support::traits::fungible`] traits, therefore you should read and understand those docs -//! as a prerequisite to understanding this pallet. -//! -//! Also see the [`frame_tokens`] reference docs for higher level information regarding the -//! place of this palet in FRAME. -//! -//! ## Overview -//! -//! The Balances pallet provides functions for: -//! -//! - Getting and setting free balances. -//! - Retrieving total, reserved and unreserved balances. -//! - Repatriating a reserved balance to a beneficiary account that exists. -//! - Transferring a balance between accounts (when not reserved). -//! - Slashing an account balance. -//! - Account creation and removal. -//! - Managing total issuance. -//! - Setting and managing locks. -//! -//! ### Terminology -//! -//! - **Reaping an account:** The act of removing an account by resetting its nonce. Happens after -//! its total balance has become less than the Existential Deposit. -//! -//! ### Implementations -//! -//! The Balances pallet provides implementations for the following [`fungible`] traits. If these -//! traits provide the functionality that you need, then you should avoid tight coupling with the -//! Balances pallet. -//! -//! - [`fungible::Inspect`] -//! - [`fungible::Mutate`] -//! - [`fungible::Unbalanced`] -//! - [`fungible::Balanced`] -//! - [`fungible::BalancedHold`] -//! - [`fungible::InspectHold`] -//! - [`fungible::MutateHold`] -//! - [`fungible::InspectFreeze`] -//! - [`fungible::MutateFreeze`] -//! - [`fungible::Imbalance`] -//! -//! It also implements the following [`Currency`] related traits, however they are deprecated and -//! will eventually be removed. -//! -//! - [`Currency`]: Functions for dealing with a fungible assets system. -//! - [`ReservableCurrency`] -//! - [`NamedReservableCurrency`](frame_support::traits::NamedReservableCurrency): Functions for -//! dealing with assets that can be reserved from an account. -//! - [`LockableCurrency`](frame_support::traits::LockableCurrency): Functions for dealing with -//! accounts that allow liquidity restrictions. -//! - [`Imbalance`](frame_support::traits::Imbalance): Functions for handling imbalances between -//! total issuance in the system and account balances. Must be used when a function creates new -//! funds (e.g. a reward) or destroys some funds (e.g. a system fee). -//! -//! ## Usage -//! -//! The following examples show how to use the Balances pallet in your custom pallet. -//! -//! ### Examples from the FRAME -//! -//! The Contract pallet uses the `Currency` trait to handle gas payment, and its types inherit from -//! `Currency`: -//! -//! ``` -//! use frame_support::traits::Currency; -//! # pub trait Config: frame_system::Config { -//! # type Currency: Currency; -//! # } -//! -//! pub type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; -//! pub type NegativeImbalanceOf = <::Currency as Currency<::AccountId>>::NegativeImbalance; -//! -//! # fn main() {} -//! ``` -//! -//! The Staking pallet uses the `LockableCurrency` trait to lock a stash account's funds: -//! -//! ``` -//! use frame_support::traits::{WithdrawReasons, LockableCurrency}; -//! use sp_runtime::traits::Bounded; -//! pub trait Config: frame_system::Config { -//! type Currency: LockableCurrency>; -//! } -//! # struct StakingLedger { -//! # stash: ::AccountId, -//! # total: <::Currency as frame_support::traits::Currency<::AccountId>>::Balance, -//! # phantom: std::marker::PhantomData, -//! # } -//! # const STAKING_ID: [u8; 8] = *b"staking "; -//! -//! fn update_ledger( -//! controller: &T::AccountId, -//! ledger: &StakingLedger -//! ) { -//! T::Currency::set_lock( -//! STAKING_ID, -//! &ledger.stash, -//! ledger.total, -//! WithdrawReasons::all() -//! ); -//! // >::insert(controller, ledger); // Commented out as we don't have access to Staking's storage here. -//! } -//! # fn main() {} -//! ``` -//! -//! ## Genesis config -//! -//! The Balances pallet depends on the [`GenesisConfig`]. -//! -//! ## Assumptions -//! -//! * Total issued balanced of all accounts should be less than `Config::Balance::max_value()`. -//! * Existential Deposit is set to a value greater than zero. -//! -//! Note, you may find the Balances pallet still functions with an ED of zero when the -//! `insecure_zero_ed` cargo feature is enabled. However this is not a configuration which is -//! generally supported, nor will it be. -//! -//! [`frame_tokens`]: ../polkadot_sdk_docs/reference_docs/frame_tokens/index.html - -#![cfg_attr(not(feature = "std"), no_std)] -mod benchmarking; -mod impl_currency; -mod impl_fungible; -mod impl_proofs; -pub mod migration; -mod tests; -mod types; -pub mod weights; - -extern crate alloc; - -use alloc::vec::Vec; -use codec::{Codec, Decode, Encode, MaxEncodedLen}; -use core::{cmp, fmt::Debug, marker::PhantomData, mem, result}; -use frame_support::{ - ensure, - pallet_prelude::DispatchResult, - traits::{ - tokens::{ - fungible, Balance as BalanceT, BalanceStatus as Status, DepositConsequence, - Fortitude::{self, Force, Polite}, - IdAmount, - Preservation::{Expendable, Preserve, Protect}, - WithdrawConsequence, - }, - Currency, Defensive, Get, OnUnbalanced, ReservableCurrency, StoredMap, - }, - BoundedSlice, StorageHasher, WeakBoundedVec, -}; -use frame_system as system; -pub use impl_currency::{NegativeImbalance, PositiveImbalance}; -pub use pallet::*; -use qp_poseidon::PoseidonHasher as PoseidonCore; -use scale_info::TypeInfo; -use sp_metadata_ir::StorageHasherIR; -use sp_runtime::{ - traits::{ - AtLeast32BitUnsigned, CheckedAdd, CheckedSub, MaybeSerializeDeserialize, Saturating, - StaticLookup, Zero, - }, - ArithmeticError, DispatchError, FixedPointOperand, Perbill, RuntimeDebug, TokenError, -}; -pub use types::{ - AccountData, AdjustmentDirection, BalanceLock, DustCleaner, ExtraFlags, Reasons, ReserveData, -}; -pub use weights::WeightInfo; - -const LOG_TARGET: &str = "runtime::balances"; - -type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; - -pub struct PoseidonStorageHasher(PhantomData); - -impl StorageHasher - for PoseidonStorageHasher -{ - // We are lying here, but maybe it's ok because it's just metadata - const METADATA: StorageHasherIR = StorageHasherIR::Identity; - type Output = [u8; 32]; - - fn hash(x: &[u8]) -> Self::Output { - PoseidonCore::hash_storage::(x) - } - - fn max_len() -> usize { - 32 - } -} - -#[frame_support::pallet] -pub mod pallet { - use super::*; - use frame_support::{ - pallet_prelude::*, - traits::{fungible::Credit, tokens::Precision, VariantCount, VariantCountOf}, - }; - use frame_system::pallet_prelude::*; - - pub type CreditOf = Credit<::AccountId, Pallet>; - pub type TransferCountType = u64; - - /// Default implementations of [`DefaultConfig`], which can be used to implement [`Config`]. - pub mod config_preludes { - use super::*; - use frame_support::{derive_impl, traits::ConstU64}; - - pub struct TestDefaultConfig; - - #[derive_impl(frame_system::config_preludes::TestDefaultConfig, no_aggregated_types)] - impl frame_system::DefaultConfig for TestDefaultConfig {} - - #[frame_support::register_default_impl(TestDefaultConfig)] - impl DefaultConfig for TestDefaultConfig { - #[inject_runtime_type] - type RuntimeHoldReason = (); - #[inject_runtime_type] - type RuntimeFreezeReason = (); - - type Balance = u64; - type ExistentialDeposit = ConstU64<1>; - - type ReserveIdentifier = (); - type FreezeIdentifier = Self::RuntimeFreezeReason; - - type DustRemoval = (); - - type MaxLocks = ConstU32<100>; - type MaxReserves = ConstU32<100>; - type MaxFreezes = VariantCountOf; - - type WeightInfo = (); - type DoneSlashHandler = (); - } - } - - #[pallet::config(with_default)] - pub trait Config: frame_system::Config { - /// The overarching hold reason. - #[pallet::no_default_bounds] - type RuntimeHoldReason: Parameter + Member + MaxEncodedLen + Copy + VariantCount; - - /// The overarching freeze reason. - #[pallet::no_default_bounds] - type RuntimeFreezeReason: VariantCount; - - /// Weight information for extrinsics in this pallet. - type WeightInfo: WeightInfo; - - /// The balance of an account. - type Balance: Parameter - + Member - + AtLeast32BitUnsigned - + Codec - + Default - + Copy - + MaybeSerializeDeserialize - + Debug - + MaxEncodedLen - + TypeInfo - + FixedPointOperand - + BalanceT; - - /// Handler for the unbalanced reduction when removing a dust account. - #[pallet::no_default_bounds] - type DustRemoval: OnUnbalanced>; - - /// The minimum amount required to keep an account open. MUST BE GREATER THAN ZERO! - /// - /// If you *really* need it to be zero, you can enable the feature `insecure_zero_ed` for - /// this pallet. However, you do so at your own risk: this will open up a major DoS vector. - /// In case you have multiple sources of provider references, you may also get unexpected - /// behaviour if you set this to zero. - /// - /// Bottom line: Do yourself a favour and make it at least one! - #[pallet::constant] - #[pallet::no_default_bounds] - type ExistentialDeposit: Get; - - /// The means of storing the balances of an account. - #[pallet::no_default] - type AccountStore: StoredMap>; - - /// The ID type for reserves. - /// - /// Use of reserves is deprecated in favour of holds. See `https://github.com/paritytech/substrate/pull/12951/` - type ReserveIdentifier: Parameter + Member + MaxEncodedLen + Ord + Copy; - - /// The ID type for freezes. - type FreezeIdentifier: Parameter + Member + MaxEncodedLen + Copy; - - /// The maximum number of locks that should exist on an account. - /// Not strictly enforced, but used for weight estimation. - /// - /// Use of locks is deprecated in favour of freezes. See `https://github.com/paritytech/substrate/pull/12951/` - #[pallet::constant] - type MaxLocks: Get; - - /// The maximum number of named reserves that can exist on an account. - /// - /// Use of reserves is deprecated in favour of holds. See `https://github.com/paritytech/substrate/pull/12951/` - #[pallet::constant] - type MaxReserves: Get; - - /// The maximum number of individual freeze locks that can exist on an account at any time. - #[pallet::constant] - type MaxFreezes: Get; - - /// Allows callbacks to other pallets so they can update their bookkeeping when a slash - /// occurs. - type DoneSlashHandler: fungible::hold::DoneSlash< - Self::RuntimeHoldReason, - Self::AccountId, - Self::Balance, - >; - } - - /// The in-code storage version. - const STORAGE_VERSION: frame_support::traits::StorageVersion = - frame_support::traits::StorageVersion::new(1); - - #[pallet::pallet] - #[pallet::storage_version(STORAGE_VERSION)] - pub struct Pallet(PhantomData<(T, I)>); - - #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event, I: 'static = ()> { - /// An account was created with some free balance. - Endowed { account: T::AccountId, free_balance: T::Balance }, - /// An account was removed whose balance was non-zero but below ExistentialDeposit, - /// resulting in an outright loss. - DustLost { account: T::AccountId, amount: T::Balance }, - /// Transfer succeeded. - Transfer { from: T::AccountId, to: T::AccountId, amount: T::Balance }, - /// A balance was set by root. - BalanceSet { who: T::AccountId, free: T::Balance }, - /// Some balance was reserved (moved from free to reserved). - Reserved { who: T::AccountId, amount: T::Balance }, - /// Some balance was unreserved (moved from reserved to free). - Unreserved { who: T::AccountId, amount: T::Balance }, - /// Some balance was moved from the reserve of the first account to the second account. - /// Final argument indicates the destination balance type. - ReserveRepatriated { - from: T::AccountId, - to: T::AccountId, - amount: T::Balance, - destination_status: Status, - }, - /// Some amount was deposited (e.g. for transaction fees). - Deposit { who: T::AccountId, amount: T::Balance }, - /// Some amount was withdrawn from the account (e.g. for transaction fees). - Withdraw { who: T::AccountId, amount: T::Balance }, - /// Some amount was removed from the account (e.g. for misbehavior). - Slashed { who: T::AccountId, amount: T::Balance }, - /// Some amount was minted into an account. - Minted { who: T::AccountId, amount: T::Balance }, - /// Some amount was burned from an account. - Burned { who: T::AccountId, amount: T::Balance }, - /// Some amount was suspended from an account (it can be restored later). - Suspended { who: T::AccountId, amount: T::Balance }, - /// Some amount was restored into an account. - Restored { who: T::AccountId, amount: T::Balance }, - /// An account was upgraded. - Upgraded { who: T::AccountId }, - /// Total issuance was increased by `amount`, creating a credit to be balanced. - Issued { amount: T::Balance }, - /// Total issuance was decreased by `amount`, creating a debt to be balanced. - Rescinded { amount: T::Balance }, - /// Some balance was locked. - Locked { who: T::AccountId, amount: T::Balance }, - /// Some balance was unlocked. - Unlocked { who: T::AccountId, amount: T::Balance }, - /// Some balance was frozen. - Frozen { who: T::AccountId, amount: T::Balance }, - /// Some balance was thawed. - Thawed { who: T::AccountId, amount: T::Balance }, - /// The `TotalIssuance` was forcefully changed. - TotalIssuanceForced { old: T::Balance, new: T::Balance }, - /// Transfer proof was stored. - TransferProofStored { - transfer_count: u64, - source: T::AccountId, - dest: T::AccountId, - funding_amount: T::Balance, - }, - } - - #[pallet::error] - pub enum Error { - /// Vesting balance too high to send value. - VestingBalance, - /// Account liquidity restrictions prevent withdrawal. - LiquidityRestrictions, - /// Balance too low to send value. - InsufficientBalance, - /// Value too low to create account due to existential deposit. - ExistentialDeposit, - /// Transfer/payment would kill account. - Expendability, - /// A vesting schedule already exists for this account. - ExistingVestingSchedule, - /// Beneficiary account must pre-exist. - DeadAccount, - /// Number of named reserves exceed `MaxReserves`. - TooManyReserves, - /// Number of holds exceed `VariantCountOf`. - TooManyHolds, - /// Number of freezes exceed `MaxFreezes`. - TooManyFreezes, - /// The issuance cannot be modified since it is already deactivated. - IssuanceDeactivated, - /// The delta cannot be zero. - DeltaZero, - } - - /// The total units issued in the system. - #[pallet::storage] - #[pallet::whitelist_storage] - pub type TotalIssuance, I: 'static = ()> = StorageValue<_, T::Balance, ValueQuery>; - - /// The total units of outstanding deactivated balance in the system. - #[pallet::storage] - #[pallet::whitelist_storage] - pub type InactiveIssuance, I: 'static = ()> = - StorageValue<_, T::Balance, ValueQuery>; - - /// The Balances pallet example of storing the balance of an account. - /// - /// # Example - /// - /// ```nocompile - /// impl pallet_balances::Config for Runtime { - /// type AccountStore = StorageMapShim, frame_system::Provider, AccountId, Self::AccountData> - /// } - /// ``` - /// - /// You can also store the balance of an account in the `System` pallet. - /// - /// # Example - /// - /// ```nocompile - /// impl pallet_balances::Config for Runtime { - /// type AccountStore = System - /// } - /// ``` - /// - /// But this comes with tradeoffs, storing account balances in the system pallet stores - /// `frame_system` data alongside the account data contrary to storing account balances in the - /// `Balances` pallet, which uses a `StorageMap` to store balances data only. - /// NOTE: This is only used in the case that this pallet is used to store balances. - #[pallet::storage] - pub type Account, I: 'static = ()> = - StorageMap<_, Blake2_128Concat, T::AccountId, AccountData, ValueQuery>; - - /// Any liquidity locks on some account balances. - /// NOTE: Should only be accessed when setting, changing and freeing a lock. - /// - /// Use of locks is deprecated in favour of freezes. See `https://github.com/paritytech/substrate/pull/12951/` - #[pallet::storage] - pub type Locks, I: 'static = ()> = StorageMap< - _, - Blake2_128Concat, - T::AccountId, - WeakBoundedVec, T::MaxLocks>, - ValueQuery, - >; - - /// Named reserves on some account balances. - /// - /// Use of reserves is deprecated in favour of holds. See `https://github.com/paritytech/substrate/pull/12951/` - #[pallet::storage] - pub type Reserves, I: 'static = ()> = StorageMap< - _, - Blake2_128Concat, - T::AccountId, - BoundedVec, T::MaxReserves>, - ValueQuery, - >; - - /// Holds on account balances. - #[pallet::storage] - pub type Holds, I: 'static = ()> = StorageMap< - _, - Blake2_128Concat, - T::AccountId, - BoundedVec< - IdAmount, - VariantCountOf, - >, - ValueQuery, - >; - - /// Freeze locks on account balances. - #[pallet::storage] - pub type Freezes, I: 'static = ()> = StorageMap< - _, - Blake2_128Concat, - T::AccountId, - BoundedVec, T::MaxFreezes>, - ValueQuery, - >; - - /// Transfer proofs for a wormhole transfers - #[pallet::storage] - #[pallet::getter(fn transfer_proof)] - pub type TransferProof, I: 'static = ()> = StorageMap< - _, - PoseidonStorageHasher, - (u64, T::AccountId, T::AccountId, T::Balance), // (tx_count, from, to, amount) - (), - OptionQuery, // Returns None if not present - >; - - #[pallet::storage] - #[pallet::getter(fn transfer_count)] - pub type TransferCount, I: 'static = ()> = - StorageValue<_, TransferCountType, ValueQuery>; - - #[pallet::genesis_config] - pub struct GenesisConfig, I: 'static = ()> { - pub balances: Vec<(T::AccountId, T::Balance)>, - } - - impl, I: 'static> Default for GenesisConfig { - fn default() -> Self { - Self { balances: Default::default() } - } - } - - #[pallet::genesis_build] - impl, I: 'static> BuildGenesisConfig for GenesisConfig { - fn build(&self) { - let total = self.balances.iter().fold(Zero::zero(), |acc: T::Balance, &(_, n)| acc + n); - - >::put(total); - - for (_, balance) in &self.balances { - assert!( - *balance >= >::ExistentialDeposit::get(), - "the balance of any account should always be at least the existential deposit.", - ) - } - - // ensure no duplicates exist. - let endowed_accounts = self - .balances - .iter() - .map(|(x, _)| x) - .cloned() - .collect::>(); - - assert!( - endowed_accounts.len() == self.balances.len(), - "duplicate balances in genesis." - ); - - for &(ref who, free) in self.balances.iter() { - frame_system::Pallet::::inc_providers(who); - assert!(T::AccountStore::insert(who, AccountData { free, ..Default::default() }) - .is_ok()); - } - } - } - - #[pallet::hooks] - impl, I: 'static> Hooks> for Pallet { - fn integrity_test() { - #[cfg(not(feature = "insecure_zero_ed"))] - assert!( - !>::ExistentialDeposit::get().is_zero(), - "The existential deposit must be greater than zero!" - ); - - assert!( - T::MaxFreezes::get() >= ::VARIANT_COUNT, - "MaxFreezes should be greater than or equal to the number of freeze reasons: {} < {}", - T::MaxFreezes::get(), ::VARIANT_COUNT, - ); - } - - #[cfg(feature = "try-runtime")] - fn try_state(_n: BlockNumberFor) -> Result<(), sp_runtime::TryRuntimeError> { - Holds::::iter_keys().try_for_each(|k| { - if Holds::::decode_len(k).unwrap_or(0) > - T::RuntimeHoldReason::VARIANT_COUNT as usize - { - Err("Found `Hold` with too many elements") - } else { - Ok(()) - } - })?; - - Freezes::::iter_keys().try_for_each(|k| { - if Freezes::::decode_len(k).unwrap_or(0) > T::MaxFreezes::get() as usize { - Err("Found `Freeze` with too many elements") - } else { - Ok(()) - } - })?; - - Ok(()) - } - } - - #[pallet::call(weight(>::WeightInfo))] - impl, I: 'static> Pallet { - /// Transfer some liquid free balance to another account. - /// - /// `transfer_allow_death` will set the `FreeBalance` of the sender and receiver. - /// If the sender's account is below the existential deposit as a result - /// of the transfer, the account will be reaped. - /// - /// The dispatch origin for this call must be `Signed` by the transactor. - #[pallet::call_index(0)] - pub fn transfer_allow_death( - origin: OriginFor, - dest: AccountIdLookupOf, - #[pallet::compact] value: T::Balance, - ) -> DispatchResult { - let source = ensure_signed(origin)?; - let dest = T::Lookup::lookup(dest)?; - >::transfer(&source, &dest, value, Expendable)?; - Self::do_store_transfer_proof(&source, &dest, value); - Ok(()) - } - - /// Exactly as `transfer_allow_death`, except the origin must be root and the source account - /// may be specified. - #[pallet::call_index(2)] - pub fn force_transfer( - origin: OriginFor, - source: AccountIdLookupOf, - dest: AccountIdLookupOf, - #[pallet::compact] value: T::Balance, - ) -> DispatchResult { - ensure_root(origin)?; - let source = T::Lookup::lookup(source)?; - let dest = T::Lookup::lookup(dest)?; - >::transfer(&source, &dest, value, Expendable)?; - Self::do_store_transfer_proof(&source, &dest, value); - Ok(()) - } - - /// Same as the [`transfer_allow_death`] call, but with a check that the transfer will not - /// kill the origin account. - /// - /// 99% of the time you want [`transfer_allow_death`] instead. - /// - /// [`transfer_allow_death`]: struct.Pallet.html#method.transfer - #[pallet::call_index(3)] - pub fn transfer_keep_alive( - origin: OriginFor, - dest: AccountIdLookupOf, - #[pallet::compact] value: T::Balance, - ) -> DispatchResult { - let source = ensure_signed(origin)?; - let dest = T::Lookup::lookup(dest)?; - >::transfer(&source, &dest, value, Preserve)?; - Self::do_store_transfer_proof(&source, &dest, value); - Ok(()) - } - - /// Transfer the entire transferable balance from the caller account. - /// - /// NOTE: This function only attempts to transfer _transferable_ balances. This means that - /// any locked, reserved, or existential deposits (when `keep_alive` is `true`), will not be - /// transferred by this function. To ensure that this function results in a killed account, - /// you might need to prepare the account by removing any reference counters, storage - /// deposits, etc... - /// - /// The dispatch origin of this call must be Signed. - /// - /// - `dest`: The recipient of the transfer. - /// - `keep_alive`: A boolean to determine if the `transfer_all` operation should send all - /// of the funds the account has, causing the sender account to be killed (false), or - /// transfer everything except at least the existential deposit, which will guarantee to - /// keep the sender account alive (true). - #[pallet::call_index(4)] - pub fn transfer_all( - origin: OriginFor, - dest: AccountIdLookupOf, - keep_alive: bool, - ) -> DispatchResult { - let transactor = ensure_signed(origin)?; - let keep_alive = if keep_alive { Preserve } else { Expendable }; - let reducible_balance = >::reducible_balance( - &transactor, - keep_alive, - Fortitude::Polite, - ); - let dest = T::Lookup::lookup(dest)?; - >::transfer( - &transactor, - &dest, - reducible_balance, - keep_alive, - )?; - Self::do_store_transfer_proof(&transactor, &dest, reducible_balance); - Ok(()) - } - - /// Unreserve some balance from a user by force. - /// - /// Can only be called by ROOT. - #[pallet::call_index(5)] - pub fn force_unreserve( - origin: OriginFor, - who: AccountIdLookupOf, - amount: T::Balance, - ) -> DispatchResult { - ensure_root(origin)?; - let who = T::Lookup::lookup(who)?; - let _leftover = >::unreserve(&who, amount); - Ok(()) - } - - /// Upgrade a specified account. - /// - /// - `origin`: Must be `Signed`. - /// - `who`: The account to be upgraded. - /// - /// This will waive the transaction fee if at least all but 10% of the accounts needed to - /// be upgraded. (We let some not have to be upgraded just in order to allow for the - /// possibility of churn). - #[pallet::call_index(6)] - #[pallet::weight(T::WeightInfo::upgrade_accounts(who.len() as u32))] - pub fn upgrade_accounts( - origin: OriginFor, - who: Vec, - ) -> DispatchResultWithPostInfo { - ensure_signed(origin)?; - if who.is_empty() { - return Ok(Pays::Yes.into()); - } - let mut upgrade_count = 0; - for i in &who { - let upgraded = Self::ensure_upgraded(i); - if upgraded { - upgrade_count.saturating_inc(); - } - } - let proportion_upgraded = Perbill::from_rational(upgrade_count, who.len() as u32); - if proportion_upgraded >= Perbill::from_percent(90) { - Ok(Pays::No.into()) - } else { - Ok(Pays::Yes.into()) - } - } - - /// Set the regular balance of a given account. - /// - /// The dispatch origin for this call is `root`. - #[pallet::call_index(8)] - #[pallet::weight( - T::WeightInfo::force_set_balance_creating() // Creates a new account. - .max(T::WeightInfo::force_set_balance_killing()) // Kills an existing account. - )] - pub fn force_set_balance( - origin: OriginFor, - who: AccountIdLookupOf, - #[pallet::compact] new_free: T::Balance, - ) -> DispatchResult { - ensure_root(origin)?; - let who = T::Lookup::lookup(who)?; - let existential_deposit = Self::ed(); - - let wipeout = new_free < existential_deposit; - let new_free = if wipeout { Zero::zero() } else { new_free }; - - // First we try to modify the account's balance to the forced balance. - let old_free = Self::mutate_account_handling_dust(&who, |account| { - let old_free = account.free; - account.free = new_free; - old_free - })?; - - // This will adjust the total issuance, which was not done by the `mutate_account` - // above. - match new_free.cmp(&old_free) { - cmp::Ordering::Greater => { - mem::drop(PositiveImbalance::::new(new_free - old_free)); - }, - cmp::Ordering::Less => { - mem::drop(NegativeImbalance::::new(old_free - new_free)); - }, - cmp::Ordering::Equal => {}, - } - - Self::deposit_event(Event::BalanceSet { who, free: new_free }); - Ok(()) - } - - /// Adjust the total issuance in a saturating way. - /// - /// Can only be called by root and always needs a positive `delta`. - /// - /// # Example - #[doc = docify::embed!("./src/tests/dispatchable_tests.rs", force_adjust_total_issuance_example)] - #[pallet::call_index(9)] - #[pallet::weight(T::WeightInfo::force_adjust_total_issuance())] - pub fn force_adjust_total_issuance( - origin: OriginFor, - direction: AdjustmentDirection, - #[pallet::compact] delta: T::Balance, - ) -> DispatchResult { - ensure_root(origin)?; - - ensure!(delta > Zero::zero(), Error::::DeltaZero); - - let old = TotalIssuance::::get(); - let new = match direction { - AdjustmentDirection::Increase => old.saturating_add(delta), - AdjustmentDirection::Decrease => old.saturating_sub(delta), - }; - - ensure!(InactiveIssuance::::get() <= new, Error::::IssuanceDeactivated); - TotalIssuance::::set(new); - - Self::deposit_event(Event::::TotalIssuanceForced { old, new }); - - Ok(()) - } - - /// Burn the specified liquid free balance from the origin account. - /// - /// If the origin's account ends up below the existential deposit as a result - /// of the burn and `keep_alive` is false, the account will be reaped. - /// - /// Unlike sending funds to a _burn_ address, which merely makes the funds inaccessible, - /// this `burn` operation will reduce total issuance by the amount _burned_. - #[pallet::call_index(10)] - #[pallet::weight(if *keep_alive {T::WeightInfo::burn_allow_death() } else {T::WeightInfo::burn_keep_alive()})] - pub fn burn( - origin: OriginFor, - #[pallet::compact] value: T::Balance, - keep_alive: bool, - ) -> DispatchResult { - let source = ensure_signed(origin)?; - let preservation = if keep_alive { Preserve } else { Expendable }; - >::burn_from( - &source, - value, - preservation, - Precision::Exact, - Polite, - )?; - Ok(()) - } - } - - impl, I: 'static> Pallet { - pub(crate) fn do_store_transfer_proof( - from: &T::AccountId, - to: &T::AccountId, - value: T::Balance, - ) { - if from != to { - let current_count = Self::transfer_count(); - TransferProof::::insert((current_count, from.clone(), to.clone(), value), ()); - TransferCount::::put(current_count.saturating_add(One::one())); - - Self::deposit_event(Event::TransferProofStored { - transfer_count: current_count, - source: from.clone(), - dest: to.clone(), - funding_amount: value, - }); - } - } - - pub(crate) fn transfer_proof_storage_key( - transfer_count: u64, - from: T::AccountId, - to: T::AccountId, - amount: T::Balance, - ) -> Vec { - let key = (transfer_count, from, to, amount); - TransferProof::::hashed_key_for(&key) - } - } - - impl, I: 'static> Pallet { - /// Public function to get the total issuance. - pub fn total_issuance() -> T::Balance { - TotalIssuance::::get() - } - - /// Public function to get the inactive issuance. - pub fn inactive_issuance() -> T::Balance { - InactiveIssuance::::get() - } - - /// Public function to access the Locks storage. - pub fn locks(who: &T::AccountId) -> WeakBoundedVec, T::MaxLocks> { - Locks::::get(who) - } - - /// Public function to access the reserves storage. - pub fn reserves( - who: &T::AccountId, - ) -> BoundedVec, T::MaxReserves> { - Reserves::::get(who) - } - - fn ed() -> T::Balance { - T::ExistentialDeposit::get() - } - /// Ensure the account `who` is using the new logic. - /// - /// Returns `true` if the account did get upgraded, `false` if it didn't need upgrading. - pub fn ensure_upgraded(who: &T::AccountId) -> bool { - let mut a = T::AccountStore::get(who); - if a.flags.is_new_logic() { - return false; - } - a.flags.set_new_logic(); - if !a.reserved.is_zero() && a.frozen.is_zero() { - if system::Pallet::::providers(who) == 0 { - // Gah!! We have no provider refs :( - // This shouldn't practically happen, but we need a failsafe anyway: let's give - // them enough for an ED. - log::warn!( - target: LOG_TARGET, - "account with a non-zero reserve balance has no provider refs, account_id: '{:?}'.", - who - ); - a.free = a.free.max(Self::ed()); - system::Pallet::::inc_providers(who); - } - let _ = system::Pallet::::inc_consumers_without_limit(who).defensive(); - } - // Should never fail - we're only setting a bit. - let _ = T::AccountStore::try_mutate_exists(who, |account| -> DispatchResult { - *account = Some(a); - Ok(()) - }); - Self::deposit_event(Event::Upgraded { who: who.clone() }); - true - } - - /// Get the free balance of an account. - pub fn free_balance(who: impl core::borrow::Borrow) -> T::Balance { - Self::account(who.borrow()).free - } - - /// Get the balance of an account that can be used for transfers, reservations, or any other - /// non-locking, non-transaction-fee activity. Will be at most `free_balance`. - pub fn usable_balance(who: impl core::borrow::Borrow) -> T::Balance { - >::reducible_balance(who.borrow(), Expendable, Polite) - } - - /// Get the balance of an account that can be used for paying transaction fees (not tipping, - /// or any other kind of fees, though). Will be at most `free_balance`. - /// - /// This requires that the account stays alive. - pub fn usable_balance_for_fees(who: impl core::borrow::Borrow) -> T::Balance { - >::reducible_balance(who.borrow(), Protect, Polite) - } - - /// Get the reserved balance of an account. - pub fn reserved_balance(who: impl core::borrow::Borrow) -> T::Balance { - Self::account(who.borrow()).reserved - } - - /// Get both the free and reserved balances of an account. - pub(crate) fn account(who: &T::AccountId) -> AccountData { - T::AccountStore::get(who) - } - - /// Mutate an account to some new value, or delete it entirely with `None`. Will enforce - /// `ExistentialDeposit` law, annulling the account as needed. - /// - /// It returns the result from the closure. Any dust is handled through the low-level - /// `fungible::Unbalanced` trap-door for legacy dust management. - /// - /// NOTE: Doesn't do any preparatory work for creating a new account, so should only be used - /// when it is known that the account already exists. - /// - /// NOTE: LOW-LEVEL: This will not attempt to maintain total issuance. It is expected that - /// the caller will do this. - pub(crate) fn mutate_account_handling_dust( - who: &T::AccountId, - f: impl FnOnce(&mut AccountData) -> R, - ) -> Result { - let (r, maybe_dust) = Self::mutate_account(who, f)?; - if let Some(dust) = maybe_dust { - >::handle_raw_dust(dust); - } - Ok(r) - } - - /// Mutate an account to some new value, or delete it entirely with `None`. Will enforce - /// `ExistentialDeposit` law, annulling the account as needed. - /// - /// It returns the result from the closure. Any dust is handled through the low-level - /// `fungible::Unbalanced` trap-door for legacy dust management. - /// - /// NOTE: Doesn't do any preparatory work for creating a new account, so should only be used - /// when it is known that the account already exists. - /// - /// NOTE: LOW-LEVEL: This will not attempt to maintain total issuance. It is expected that - /// the caller will do this. - pub(crate) fn try_mutate_account_handling_dust>( - who: &T::AccountId, - f: impl FnOnce(&mut AccountData, bool) -> Result, - ) -> Result { - let (r, maybe_dust) = Self::try_mutate_account(who, f)?; - if let Some(dust) = maybe_dust { - >::handle_raw_dust(dust); - } - Ok(r) - } - - /// Mutate an account to some new value, or delete it entirely with `None`. Will enforce - /// `ExistentialDeposit` law, annulling the account as needed. - /// - /// It returns both the result from the closure, and an optional amount of dust - /// which should be handled once it is known that all nested mutates that could affect - /// storage items what the dust handler touches have completed. - /// - /// NOTE: Doesn't do any preparatory work for creating a new account, so should only be used - /// when it is known that the account already exists. - /// - /// NOTE: LOW-LEVEL: This will not attempt to maintain total issuance. It is expected that - /// the caller will do this. - pub(crate) fn mutate_account( - who: &T::AccountId, - f: impl FnOnce(&mut AccountData) -> R, - ) -> Result<(R, Option), DispatchError> { - Self::try_mutate_account(who, |a, _| -> Result { Ok(f(a)) }) - } - - /// Returns `true` when `who` has some providers or `insecure_zero_ed` feature is disabled. - /// Returns `false` otherwise. - #[cfg(not(feature = "insecure_zero_ed"))] - fn have_providers_or_no_zero_ed(_: &T::AccountId) -> bool { - true - } - - /// Returns `true` when `who` has some providers or `insecure_zero_ed` feature is disabled. - /// Returns `false` otherwise. - #[cfg(feature = "insecure_zero_ed")] - fn have_providers_or_no_zero_ed(who: &T::AccountId) -> bool { - frame_system::Pallet::::providers(who) > 0 - } - - /// Mutate an account to some new value, or delete it entirely with `None`. Will enforce - /// `ExistentialDeposit` law, annulling the account as needed. This will do nothing if the - /// result of `f` is an `Err`. - /// - /// It returns both the result from the closure, and an optional amount of dust - /// which should be handled once it is known that all nested mutates that could affect - /// storage items what the dust handler touches have completed. - /// - /// NOTE: Doesn't do any preparatory work for creating a new account, so should only be used - /// when it is known that the account already exists. - /// - /// NOTE: LOW-LEVEL: This will not attempt to maintain total issuance. It is expected that - /// the caller will do this. - pub(crate) fn try_mutate_account>( - who: &T::AccountId, - f: impl FnOnce(&mut AccountData, bool) -> Result, - ) -> Result<(R, Option), E> { - Self::ensure_upgraded(who); - let result = T::AccountStore::try_mutate_exists(who, |maybe_account| { - let is_new = maybe_account.is_none(); - let mut account = maybe_account.take().unwrap_or_default(); - let did_provide = - account.free >= Self::ed() && Self::have_providers_or_no_zero_ed(who); - let did_consume = - !is_new && (!account.reserved.is_zero() || !account.frozen.is_zero()); - - let result = f(&mut account, is_new)?; - - let does_provide = account.free >= Self::ed(); - let does_consume = !account.reserved.is_zero() || !account.frozen.is_zero(); - - if !did_provide && does_provide { - frame_system::Pallet::::inc_providers(who); - } - if did_consume && !does_consume { - frame_system::Pallet::::dec_consumers(who); - } - if !did_consume && does_consume { - frame_system::Pallet::::inc_consumers(who)?; - } - if does_consume && frame_system::Pallet::::consumers(who) == 0 { - // NOTE: This is a failsafe and should not happen for normal accounts. A normal - // account should have gotten a consumer ref in `!did_consume && does_consume` - // at some point. - log::error!(target: LOG_TARGET, "Defensively bumping a consumer ref."); - frame_system::Pallet::::inc_consumers(who)?; - } - if did_provide && !does_provide { - // This could reap the account so must go last. - frame_system::Pallet::::dec_providers(who).inspect_err(|_| { - // best-effort revert consumer change. - if did_consume && !does_consume { - let _ = frame_system::Pallet::::inc_consumers(who).defensive(); - } - if !did_consume && does_consume { - frame_system::Pallet::::dec_consumers(who); - } - })?; - } - - let maybe_endowed = if is_new { Some(account.free) } else { None }; - - // Handle any steps needed after mutating an account. - // - // This includes DustRemoval unbalancing, in the case than the `new` account's total - // balance is non-zero but below ED. - // - // Updates `maybe_account` to `Some` iff the account has sufficient balance. - // Evaluates `maybe_dust`, which is `Some` containing the dust to be dropped, iff - // some dust should be dropped. - // - // We should never be dropping if reserved is non-zero. Reserved being non-zero - // should imply that we have a consumer ref, so this is economically safe. - let ed = Self::ed(); - let maybe_dust = if account.free < ed && account.reserved.is_zero() { - if account.free.is_zero() { - None - } else { - Some(account.free) - } - } else { - assert!( - account.free.is_zero() || account.free >= ed || !account.reserved.is_zero() - ); - *maybe_account = Some(account); - None - }; - Ok((maybe_endowed, maybe_dust, result)) - }); - result.map(|(maybe_endowed, maybe_dust, result)| { - if let Some(endowed) = maybe_endowed { - Self::deposit_event(Event::Endowed { - account: who.clone(), - free_balance: endowed, - }); - } - if let Some(amount) = maybe_dust { - Pallet::::deposit_event(Event::DustLost { account: who.clone(), amount }); - } - (result, maybe_dust) - }) - } - - /// Update the account entry for `who`, given the locks. - pub(crate) fn update_locks(who: &T::AccountId, locks: &[BalanceLock]) { - let bounded_locks = WeakBoundedVec::<_, T::MaxLocks>::force_from( - locks.to_vec(), - Some("Balances Update Locks"), - ); - - if locks.len() as u32 > T::MaxLocks::get() { - log::warn!( - target: LOG_TARGET, - "Warning: A user has more currency locks than expected. \ - A runtime configuration adjustment may be needed." - ); - } - let freezes = Freezes::::get(who); - let mut prev_frozen = Zero::zero(); - let mut after_frozen = Zero::zero(); - // No way this can fail since we do not alter the existential balances. - // TODO: Revisit this assumption. - let res = Self::mutate_account(who, |b| { - prev_frozen = b.frozen; - b.frozen = Zero::zero(); - for l in locks.iter() { - b.frozen = b.frozen.max(l.amount); - } - for l in freezes.iter() { - b.frozen = b.frozen.max(l.amount); - } - after_frozen = b.frozen; - }); - debug_assert!(res.is_ok()); - if let Ok((_, maybe_dust)) = res { - debug_assert!(maybe_dust.is_none(), "Not altering main balance; qed"); - } - - match locks.is_empty() { - true => Locks::::remove(who), - false => Locks::::insert(who, bounded_locks), - } - - match prev_frozen.cmp(&after_frozen) { - cmp::Ordering::Greater => { - let amount = prev_frozen.saturating_sub(after_frozen); - Self::deposit_event(Event::Unlocked { who: who.clone(), amount }); - }, - cmp::Ordering::Less => { - let amount = after_frozen.saturating_sub(prev_frozen); - Self::deposit_event(Event::Locked { who: who.clone(), amount }); - }, - cmp::Ordering::Equal => {}, - } - } - - /// Update the account entry for `who`, given the locks. - pub(crate) fn update_freezes( - who: &T::AccountId, - freezes: BoundedSlice, T::MaxFreezes>, - ) -> DispatchResult { - let mut prev_frozen = Zero::zero(); - let mut after_frozen = Zero::zero(); - let (_, maybe_dust) = Self::mutate_account(who, |b| { - prev_frozen = b.frozen; - b.frozen = Zero::zero(); - for l in Locks::::get(who).iter() { - b.frozen = b.frozen.max(l.amount); - } - for l in freezes.iter() { - b.frozen = b.frozen.max(l.amount); - } - after_frozen = b.frozen; - })?; - debug_assert!(maybe_dust.is_none(), "Not altering main balance; qed"); - if freezes.is_empty() { - Freezes::::remove(who); - } else { - Freezes::::insert(who, freezes); - } - match prev_frozen.cmp(&after_frozen) { - cmp::Ordering::Greater => { - let amount = prev_frozen.saturating_sub(after_frozen); - Self::deposit_event(Event::Thawed { who: who.clone(), amount }); - }, - cmp::Ordering::Less => { - let amount = after_frozen.saturating_sub(prev_frozen); - Self::deposit_event(Event::Frozen { who: who.clone(), amount }); - }, - cmp::Ordering::Equal => {}, - } - Ok(()) - } - - /// Move the reserved balance of one account into the balance of another, according to - /// `status`. This will respect freezes/locks only if `fortitude` is `Polite`. - /// - /// Is a no-op if the value to be moved is zero. - /// - /// NOTE: returns actual amount of transferred value in `Ok` case. - pub(crate) fn do_transfer_reserved( - slashed: &T::AccountId, - beneficiary: &T::AccountId, - value: T::Balance, - precision: Precision, - fortitude: Fortitude, - status: Status, - ) -> Result { - if value.is_zero() { - return Ok(Zero::zero()); - } - - let max = >::reducible_total_balance_on_hold( - slashed, fortitude, - ); - let actual = match precision { - Precision::BestEffort => value.min(max), - Precision::Exact => value, - }; - ensure!(actual <= max, TokenError::FundsUnavailable); - if slashed == beneficiary { - return match status { - Status::Free => Ok(actual.saturating_sub(Self::unreserve(slashed, actual))), - Status::Reserved => Ok(actual), - }; - } - - let ((_, maybe_dust_1), maybe_dust_2) = Self::try_mutate_account( - beneficiary, - |to_account, is_new| -> Result<((), Option), DispatchError> { - ensure!(!is_new, Error::::DeadAccount); - Self::try_mutate_account(slashed, |from_account, _| -> DispatchResult { - match status { - Status::Free => - to_account.free = to_account - .free - .checked_add(&actual) - .ok_or(ArithmeticError::Overflow)?, - Status::Reserved => - to_account.reserved = to_account - .reserved - .checked_add(&actual) - .ok_or(ArithmeticError::Overflow)?, - } - from_account.reserved.saturating_reduce(actual); - Ok(()) - }) - }, - )?; - - if let Some(dust) = maybe_dust_1 { - >::handle_raw_dust(dust); - } - if let Some(dust) = maybe_dust_2 { - >::handle_raw_dust(dust); - } - - Self::deposit_event(Event::ReserveRepatriated { - from: slashed.clone(), - to: beneficiary.clone(), - amount: actual, - destination_status: status, - }); - Ok(actual) - } - } -} diff --git a/pallets/balances/src/migration.rs b/pallets/balances/src/migration.rs deleted file mode 100644 index ac2f7e2f..00000000 --- a/pallets/balances/src/migration.rs +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (C) Parity Technologies (UK) Ltd. -// This file is part of Substrate. - -// Substrate is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Substrate is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Substrate. If not, see . - -use super::*; -use frame_support::{ - pallet_prelude::*, - traits::{OnRuntimeUpgrade, PalletInfoAccess}, - weights::Weight, -}; - -fn migrate_v0_to_v1, I: 'static>(accounts: &[T::AccountId]) -> Weight { - let on_chain_version = Pallet::::on_chain_storage_version(); - - if on_chain_version == 0 { - let total = accounts - .iter() - .map(Pallet::::total_balance) - .fold(T::Balance::zero(), |a, e| a.saturating_add(e)); - Pallet::::deactivate(total); - - // Remove the old `StorageVersion` type. - frame_support::storage::unhashed::kill(&frame_support::storage::storage_prefix( - Pallet::::name().as_bytes(), - "StorageVersion".as_bytes(), - )); - - // Set storage version to `1`. - StorageVersion::new(1).put::>(); - - log::info!(target: LOG_TARGET, "Storage to version 1"); - T::DbWeight::get().reads_writes(2 + accounts.len() as u64, 3) - } else { - log::info!( - target: LOG_TARGET, - "Migration did not execute. This probably should be removed" - ); - T::DbWeight::get().reads(1) - } -} - -// NOTE: This must be used alongside the account whose balance is expected to be inactive. -// Generally this will be used for the XCM teleport checking account. -pub struct MigrateToTrackInactive(PhantomData<(T, A, I)>); -impl, A: Get, I: 'static> OnRuntimeUpgrade - for MigrateToTrackInactive -{ - fn on_runtime_upgrade() -> Weight { - migrate_v0_to_v1::(&[A::get()]) - } -} - -// NOTE: This must be used alongside the accounts whose balance is expected to be inactive. -// Generally this will be used for the XCM teleport checking accounts. -pub struct MigrateManyToTrackInactive(PhantomData<(T, A, I)>); -impl, A: Get>, I: 'static> OnRuntimeUpgrade - for MigrateManyToTrackInactive -{ - fn on_runtime_upgrade() -> Weight { - migrate_v0_to_v1::(&A::get()) - } -} - -pub struct ResetInactive(PhantomData<(T, I)>); -impl, I: 'static> OnRuntimeUpgrade for ResetInactive { - fn on_runtime_upgrade() -> Weight { - let on_chain_version = Pallet::::on_chain_storage_version(); - - if on_chain_version == 1 { - // Remove the old `StorageVersion` type. - frame_support::storage::unhashed::kill(&frame_support::storage::storage_prefix( - Pallet::::name().as_bytes(), - "StorageVersion".as_bytes(), - )); - - InactiveIssuance::::kill(); - - // Set storage version to `0`. - StorageVersion::new(0).put::>(); - - log::info!(target: LOG_TARGET, "Storage to version 0"); - T::DbWeight::get().reads_writes(1, 3) - } else { - log::info!( - target: LOG_TARGET, - "Migration did not execute. This probably should be removed" - ); - T::DbWeight::get().reads(1) - } - } -} diff --git a/pallets/balances/src/tests/currency_tests.rs b/pallets/balances/src/tests/currency_tests.rs deleted file mode 100644 index cf22405a..00000000 --- a/pallets/balances/src/tests/currency_tests.rs +++ /dev/null @@ -1,1643 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Tests regarding the functionality of the `Currency` trait set implementations. - -use super::*; -use crate::{Event, NegativeImbalance}; -use frame_support::{ - traits::{ - BalanceStatus::{Free, Reserved}, - Currency, - ExistenceRequirement::{self, AllowDeath, KeepAlive}, - Hooks, InspectLockableCurrency, LockIdentifier, LockableCurrency, NamedReservableCurrency, - ReservableCurrency, WithdrawReasons, - }, - StorageNoopGuard, -}; -use frame_system::Event as SysEvent; -use sp_runtime::traits::DispatchTransaction; - -const ID_1: LockIdentifier = *b"1 "; -const ID_2: LockIdentifier = *b"2 "; - -pub const CALL: &::RuntimeCall = - &RuntimeCall::Balances(crate::Call::transfer_allow_death { - dest: sp_core::crypto::AccountId32::new([0u8; 32]), - value: 0, - }); - -#[test] -fn ed_should_work() { - ExtBuilder::default().existential_deposit(1).build_and_execute_with(|| { - assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), account_id(1), 1000)); - assert_noop!( - >::transfer(&account_id(1), &account_id(10), 1000, KeepAlive), - TokenError::NotExpendable - ); - assert_ok!(>::transfer( - &account_id(1), - &account_id(10), - 1000, - AllowDeath - )); - }); -} - -#[test] -fn set_lock_with_amount_zero_removes_lock() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - Balances::set_lock(ID_1, &account_id(1), Balance::MAX, WithdrawReasons::all()); - Balances::set_lock(ID_1, &account_id(1), 0, WithdrawReasons::all()); - assert_ok!(>::transfer( - &account_id(1), - &account_id(2), - 1, - AllowDeath - )); - }); -} - -#[test] -fn set_lock_with_withdraw_reasons_empty_removes_lock() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - Balances::set_lock(ID_1, &account_id(1), Balance::MAX, WithdrawReasons::all()); - Balances::set_lock(ID_1, &account_id(1), Balance::MAX, WithdrawReasons::empty()); - assert_ok!(>::transfer( - &account_id(1), - &account_id(2), - 1, - AllowDeath - )); - }); -} - -#[test] -fn basic_locking_should_work() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - assert_eq!(Balances::free_balance(account_id(1)), 10); - Balances::set_lock(ID_1, &account_id(1), 9, WithdrawReasons::all()); - assert_noop!( - >::transfer(&account_id(1), &account_id(2), 5, AllowDeath), - TokenError::Frozen - ); - }); -} - -#[test] -fn inspect_lock_should_work() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - Balances::set_lock(ID_1, &account_id(1), 10, WithdrawReasons::all()); - Balances::set_lock(ID_2, &account_id(1), 10, WithdrawReasons::all()); - Balances::set_lock(ID_1, &account_id(2), 20, WithdrawReasons::all()); - - assert_eq!( - >::balance_locked(ID_1, &account_id(1)), - 10 - ); - assert_eq!( - >::balance_locked(ID_2, &account_id(1)), - 10 - ); - assert_eq!( - >::balance_locked(ID_1, &account_id(2)), - 20 - ); - assert_eq!( - >::balance_locked(ID_2, &account_id(2)), - 0 - ); - assert_eq!( - >::balance_locked(ID_1, &account_id(3)), - 0 - ); - }) -} - -#[test] -fn account_should_be_reaped() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - assert_eq!(Balances::free_balance(account_id(1)), 10); - assert_ok!(>::transfer( - &account_id(1), - &account_id(2), - 10, - AllowDeath - )); - assert_eq!(System::providers(&account_id(1)), 0); - assert_eq!(System::consumers(&account_id(1)), 0); - // Check that the account is dead. - assert!(!frame_system::Account::::contains_key(account_id(1))); - }); -} - -#[test] -fn reap_failed_due_to_provider_and_consumer() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - // SCENARIO: only one provider and there are remaining consumers. - assert_ok!(System::inc_consumers(&account_id(1))); - assert!(!System::can_dec_provider(&account_id(1))); - assert_noop!( - >::transfer(&account_id(1), &account_id(2), 10, AllowDeath), - TokenError::Frozen - ); - assert!(System::account_exists(&account_id(1))); - assert_eq!(Balances::free_balance(account_id(1)), 10); - - // SCENARIO: more than one provider, but will not kill account due to other provider. - assert_eq!(System::inc_providers(&account_id(1)), frame_system::IncRefStatus::Existed); - assert_eq!(System::providers(&account_id(1)), 2); - assert!(System::can_dec_provider(&account_id(1))); - assert_ok!(>::transfer( - &account_id(1), - &account_id(2), - 10, - AllowDeath - )); - assert_eq!(System::providers(&account_id(1)), 1); - assert!(System::account_exists(&account_id(1))); - assert_eq!(Balances::free_balance(account_id(1)), 0); - }); -} - -#[test] -fn partial_locking_should_work() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - Balances::set_lock(ID_1, &account_id(1), 5, WithdrawReasons::all()); - assert_ok!(>::transfer( - &account_id(1), - &account_id(2), - 1, - AllowDeath - )); - }); -} - -#[test] -fn lock_removal_should_work() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - Balances::set_lock(ID_1, &account_id(1), Balance::MAX, WithdrawReasons::all()); - assert_eq!(System::consumers(&account_id(1)), 1); - Balances::remove_lock(ID_1, &account_id(1)); - assert_eq!(System::consumers(&account_id(1)), 0); - assert_ok!(>::transfer( - &account_id(1), - &account_id(2), - 1, - AllowDeath - )); - }); -} - -#[test] -fn lock_replacement_should_work() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - Balances::set_lock(ID_1, &account_id(1), Balance::MAX, WithdrawReasons::all()); - assert_eq!(System::consumers(&account_id(1)), 1); - Balances::set_lock(ID_1, &account_id(1), 5, WithdrawReasons::all()); - assert_eq!(System::consumers(&account_id(1)), 1); - assert_ok!(>::transfer( - &account_id(1), - &account_id(2), - 1, - AllowDeath - )); - }); -} - -#[test] -fn double_locking_should_work() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - Balances::set_lock(ID_1, &account_id(1), 5, WithdrawReasons::all()); - assert_eq!(System::consumers(&account_id(1)), 1); - Balances::set_lock(ID_2, &account_id(1), 5, WithdrawReasons::all()); - assert_eq!(System::consumers(&account_id(1)), 1); - assert_ok!(>::transfer( - &account_id(1), - &account_id(2), - 1, - AllowDeath - )); - }); -} - -#[test] -fn combination_locking_should_work() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - assert_eq!(System::consumers(&account_id(1)), 0); - Balances::set_lock(ID_1, &account_id(1), Balance::MAX, WithdrawReasons::empty()); - assert_eq!(System::consumers(&account_id(1)), 0); - Balances::set_lock(ID_2, &account_id(1), 0, WithdrawReasons::all()); - assert_eq!(System::consumers(&account_id(1)), 0); - assert_ok!(>::transfer( - &account_id(1), - &account_id(2), - 1, - AllowDeath - )); - }); -} - -#[test] -fn lock_value_extension_should_work() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - Balances::set_lock(ID_1, &account_id(1), 5, WithdrawReasons::all()); - assert_eq!(System::consumers(&account_id(1)), 1); - assert_noop!( - >::transfer(&account_id(1), &account_id(2), 6, AllowDeath), - TokenError::Frozen - ); - Balances::extend_lock(ID_1, &account_id(1), 2, WithdrawReasons::all()); - assert_eq!(System::consumers(&account_id(1)), 1); - assert_noop!( - >::transfer(&account_id(1), &account_id(2), 6, AllowDeath), - TokenError::Frozen - ); - Balances::extend_lock(ID_1, &account_id(1), 8, WithdrawReasons::all()); - assert_eq!(System::consumers(&account_id(1)), 1); - assert_noop!( - >::transfer(&account_id(1), &account_id(2), 3, AllowDeath), - TokenError::Frozen - ); - }); -} - -#[test] -fn lock_should_work_reserve() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - pallet_transaction_payment::NextFeeMultiplier::::put( - Multiplier::saturating_from_integer(1), - ); - Balances::set_lock(ID_1, &account_id(1), 10, WithdrawReasons::RESERVE); - assert_noop!( - >::transfer(&account_id(1), &account_id(2), 1, AllowDeath), - TokenError::Frozen - ); - assert_noop!( - Balances::reserve(&account_id(1), 1), - Error::::LiquidityRestrictions, - ); - assert!(ChargeTransactionPayment::::validate_and_prepare( - ChargeTransactionPayment::from(1), - Some(account_id(1)).into(), - CALL, - &crate::tests::info_from_weight(Weight::from_parts(1, 0)), - 1, - 0, - ) - .is_err()); - assert!(ChargeTransactionPayment::::validate_and_prepare( - ChargeTransactionPayment::from(0), - Some(account_id(1)).into(), - CALL, - &crate::tests::info_from_weight(Weight::from_parts(1, 0)), - 1, - 0, - ) - .is_err()); - }); -} - -#[test] -fn lock_should_work_tx_fee() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - Balances::set_lock(ID_1, &account_id(1), 10, WithdrawReasons::TRANSACTION_PAYMENT); - assert_noop!( - >::transfer(&account_id(1), &account_id(2), 1, AllowDeath), - TokenError::Frozen - ); - assert_noop!( - Balances::reserve(&account_id(1), 1), - Error::::LiquidityRestrictions, - ); - assert!(ChargeTransactionPayment::::validate_and_prepare( - ChargeTransactionPayment::from(1), - Some(account_id(1)).into(), - CALL, - &crate::tests::info_from_weight(Weight::from_parts(1, 0)), - 1, - 0, - ) - .is_err()); - assert!(ChargeTransactionPayment::::validate_and_prepare( - ChargeTransactionPayment::from(0), - Some(account_id(1)).into(), - CALL, - &crate::tests::info_from_weight(Weight::from_parts(1, 0)), - 1, - 0, - ) - .is_err()); - }); -} - -#[test] -fn lock_block_number_extension_should_work() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - Balances::set_lock(ID_1, &account_id(1), 10, WithdrawReasons::all()); - assert_noop!( - >::transfer(&account_id(1), &account_id(2), 6, AllowDeath), - TokenError::Frozen - ); - Balances::extend_lock(ID_1, &account_id(1), 10, WithdrawReasons::all()); - assert_noop!( - >::transfer(&account_id(1), &account_id(2), 6, AllowDeath), - TokenError::Frozen - ); - System::set_block_number(2); - Balances::extend_lock(ID_1, &account_id(1), 10, WithdrawReasons::all()); - assert_noop!( - >::transfer(&account_id(1), &account_id(2), 3, AllowDeath), - TokenError::Frozen - ); - }); -} - -#[test] -fn lock_reasons_extension_should_work() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - Balances::set_lock(ID_1, &account_id(1), 10, WithdrawReasons::TRANSFER); - assert_noop!( - >::transfer(&account_id(1), &account_id(2), 6, AllowDeath), - TokenError::Frozen - ); - Balances::extend_lock(ID_1, &account_id(1), 10, WithdrawReasons::empty()); - assert_noop!( - >::transfer(&account_id(1), &account_id(2), 6, AllowDeath), - TokenError::Frozen - ); - Balances::extend_lock(ID_1, &account_id(1), 10, WithdrawReasons::RESERVE); - assert_noop!( - >::transfer(&account_id(1), &account_id(2), 6, AllowDeath), - TokenError::Frozen - ); - }); -} - -#[test] -fn reserved_balance_should_prevent_reclaim_count() { - ExtBuilder::default() - .existential_deposit(256) - .monied(true) - .build_and_execute_with(|| { - System::inc_account_nonce(account_id(2)); - assert_eq!(Balances::total_balance(&account_id(2)), 256 * 20); - assert_eq!(System::providers(&account_id(2)), 1); - System::inc_providers(&account_id(2)); - assert_eq!(System::providers(&account_id(2)), 2); - - assert_ok!(Balances::reserve(&account_id(2), 256 * 19 + 1)); // account 2 becomes mostly reserved - assert_eq!(System::providers(&account_id(2)), 1); - assert_eq!(Balances::free_balance(account_id(2)), 255); // "free" account would be deleted. - assert_eq!(Balances::total_balance(&account_id(2)), 256 * 20); // reserve still exists. - assert_eq!(System::account_nonce(account_id(2)), 1); - - // account 4 tries to take index 1 for account 5. - assert_ok!(Balances::transfer_allow_death( - Some(account_id(4)).into(), - account_id(5), - 256 + 0x69 - )); - assert_eq!(Balances::total_balance(&account_id(5)), 256 + 0x69); - - assert!(Balances::slash_reserved(&account_id(2), 256 * 19 + 1).1.is_zero()); // account 2 gets slashed - - // "reserve" account reduced to 255 (below ED) so account no longer consuming - assert_ok!(System::dec_providers(&account_id(2))); - assert_eq!(System::providers(&account_id(2)), 0); - // account deleted - assert_eq!(System::account_nonce(account_id(2)), 0); // nonce zero - assert_eq!(Balances::total_balance(&account_id(2)), 0); - - // account 4 tries to take index 1 again for account 6. - assert_ok!(Balances::transfer_allow_death( - Some(account_id(4)).into(), - account_id(6), - 256 + 0x69 - )); - assert_eq!(Balances::total_balance(&account_id(6)), 256 + 0x69); - }); -} - -#[test] -fn reward_should_work() { - ExtBuilder::default().monied(true).build_and_execute_with(|| { - assert_eq!(Balances::total_balance(&account_id(1)), 10); - assert_ok!(Balances::deposit_into_existing(&account_id(1), 10).map(drop)); - assert_eq!( - events(), - [ - RuntimeEvent::Balances(crate::Event::Deposit { who: account_id(1), amount: 10 }), - RuntimeEvent::Balances(crate::Event::Issued { amount: 10 }), - ] - ); - assert_eq!(Balances::total_balance(&account_id(1)), 20); - assert_eq!(pallet_balances::TotalIssuance::::get(), 120); - }); -} - -#[test] -fn balance_works() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::deposit_creating(&account_id(1), 42); - System::assert_has_event(RuntimeEvent::Balances(crate::Event::Deposit { - who: account_id(1), - amount: 42, - })); - assert_eq!(Balances::free_balance(account_id(1)), 42); - assert_eq!(Balances::reserved_balance(account_id(1)), 0); - assert_eq!(Balances::total_balance(&account_id(1)), 42); - assert_eq!(Balances::free_balance(account_id(2)), 0); - assert_eq!(Balances::reserved_balance(account_id(2)), 0); - assert_eq!(Balances::total_balance(&account_id(2)), 0); - }); -} - -#[test] -fn reserving_balance_should_work() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::deposit_creating(&account_id(1), 111); - - assert_eq!(Balances::total_balance(&account_id(1)), 111); - assert_eq!(Balances::free_balance(account_id(1)), 111); - assert_eq!(Balances::reserved_balance(account_id(1)), 0); - - assert_ok!(Balances::reserve(&account_id(1), 69)); - - assert_eq!(Balances::total_balance(&account_id(1)), 111); - assert_eq!(Balances::free_balance(account_id(1)), 42); - assert_eq!(Balances::reserved_balance(account_id(1)), 69); - }); -} - -#[test] -fn deducting_balance_should_work() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::deposit_creating(&account_id(1), 111); - assert_ok!(Balances::reserve(&account_id(1), 69)); - assert_eq!(Balances::free_balance(account_id(1)), 42); - }); -} - -#[test] -fn refunding_balance_should_work() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::deposit_creating(&account_id(1), 42); - System::set_block_number(2); - Balances::unreserve(&account_id(1), 69); - assert_eq!(Balances::free_balance(account_id(1)), 42); - assert_eq!(Balances::reserved_balance(account_id(1)), 0); - }); -} - -#[test] -fn slashing_balance_should_work() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::deposit_creating(&account_id(1), 111); - assert_ok!(Balances::reserve(&account_id(1), 69)); - assert_eq!(Balances::slash_reserved(&account_id(1), 69).1, 0); - assert_eq!(Balances::free_balance(account_id(1)), 42); - assert_eq!(Balances::reserved_balance(account_id(1)), 0); - assert_eq!(pallet_balances::TotalIssuance::::get(), 42); - }); -} - -#[test] -fn withdrawing_balance_should_work() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::deposit_creating(&account_id(2), 111); - let _ = Balances::withdraw( - &account_id(2), - 11, - WithdrawReasons::TRANSFER, - ExistenceRequirement::KeepAlive, - ); - System::assert_has_event(RuntimeEvent::Balances(crate::Event::Withdraw { - who: account_id(2), - amount: 11, - })); - assert_eq!(Balances::free_balance(account_id(2)), 100); - assert_eq!(pallet_balances::TotalIssuance::::get(), 100); - }); -} - -#[test] -fn withdrawing_balance_should_fail_when_not_expendable() { - ExtBuilder::default().build_and_execute_with(|| { - ExistentialDeposit::set(10); - let _ = Balances::deposit_creating(&account_id(2), 20); - assert_ok!(Balances::reserve(&account_id(2), 5)); - assert_noop!( - Balances::withdraw( - &account_id(2), - 6, - WithdrawReasons::TRANSFER, - ExistenceRequirement::KeepAlive - ), - Error::::Expendability, - ); - assert_ok!(Balances::withdraw( - &account_id(2), - 5, - WithdrawReasons::TRANSFER, - ExistenceRequirement::KeepAlive - ),); - }); -} - -#[test] -fn slashing_incomplete_balance_should_work() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::deposit_creating(&account_id(1), 42); - assert_ok!(Balances::reserve(&account_id(1), 21)); - assert_eq!(Balances::slash_reserved(&account_id(1), 69).1, 48); - assert_eq!(Balances::free_balance(account_id(1)), 21); - assert_eq!(Balances::reserved_balance(account_id(1)), 0); - assert_eq!(pallet_balances::TotalIssuance::::get(), 21); - }); -} - -#[test] -fn unreserving_balance_should_work() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::deposit_creating(&account_id(1), 111); - assert_ok!(Balances::reserve(&account_id(1), 110)); - Balances::unreserve(&account_id(1), 41); - assert_eq!(Balances::reserved_balance(account_id(1)), 69); - assert_eq!(Balances::free_balance(account_id(1)), 42); - }); -} - -#[test] -fn slashing_reserved_balance_should_work() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::deposit_creating(&account_id(1), 112); - assert_ok!(Balances::reserve(&account_id(1), 111)); - assert_eq!(Balances::slash_reserved(&account_id(1), 42).1, 0); - assert_eq!(Balances::reserved_balance(account_id(1)), 69); - assert_eq!(Balances::free_balance(account_id(1)), 1); - assert_eq!(pallet_balances::TotalIssuance::::get(), 70); - }); -} - -#[test] -fn slashing_incomplete_reserved_balance_should_work() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::deposit_creating(&account_id(1), 111); - assert_ok!(Balances::reserve(&account_id(1), 42)); - assert_eq!(Balances::slash_reserved(&account_id(1), 69).1, 27); - assert_eq!(Balances::free_balance(account_id(1)), 69); - assert_eq!(Balances::reserved_balance(account_id(1)), 0); - assert_eq!(pallet_balances::TotalIssuance::::get(), 69); - }); -} - -#[test] -fn repatriating_reserved_balance_should_work() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::deposit_creating(&account_id(1), 111); - let _ = Balances::deposit_creating(&account_id(2), 1); - assert_ok!(Balances::reserve(&account_id(1), 110)); - assert_ok!(Balances::repatriate_reserved(&account_id(1), &account_id(2), 41, Free), 0); - System::assert_last_event(RuntimeEvent::Balances(crate::Event::ReserveRepatriated { - from: account_id(1), - to: account_id(2), - amount: 41, - destination_status: Free, - })); - assert_eq!(Balances::reserved_balance(account_id(1)), 69); - assert_eq!(Balances::free_balance(account_id(1)), 1); - assert_eq!(Balances::reserved_balance(account_id(2)), 0); - assert_eq!(Balances::free_balance(account_id(2)), 42); - }); -} - -#[test] -fn transferring_reserved_balance_should_work() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::deposit_creating(&account_id(1), 111); - let _ = Balances::deposit_creating(&account_id(2), 1); - assert_ok!(Balances::reserve(&account_id(1), 110)); - assert_ok!(Balances::repatriate_reserved(&account_id(1), &account_id(2), 41, Reserved), 0); - assert_eq!(Balances::reserved_balance(account_id(1)), 69); - assert_eq!(Balances::free_balance(account_id(1)), 1); - assert_eq!(Balances::reserved_balance(account_id(2)), 41); - assert_eq!(Balances::free_balance(account_id(2)), 1); - }); -} - -#[test] -fn transferring_reserved_balance_to_yourself_should_work() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::deposit_creating(&account_id(1), 110); - assert_ok!(Balances::reserve(&account_id(1), 50)); - assert_ok!(Balances::repatriate_reserved(&account_id(1), &account_id(1), 50, Free), 0); - assert_eq!(Balances::free_balance(account_id(1)), 110); - assert_eq!(Balances::reserved_balance(account_id(1)), 0); - - assert_ok!(Balances::reserve(&account_id(1), 50)); - assert_ok!(Balances::repatriate_reserved(&account_id(1), &account_id(1), 60, Free), 10); - assert_eq!(Balances::free_balance(account_id(1)), 110); - assert_eq!(Balances::reserved_balance(account_id(1)), 0); - }); -} - -#[test] -fn transferring_reserved_balance_to_nonexistent_should_fail() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::deposit_creating(&account_id(1), 111); - assert_ok!(Balances::reserve(&account_id(1), 110)); - assert_noop!( - Balances::repatriate_reserved(&account_id(1), &account_id(2), 42, Free), - Error::::DeadAccount - ); - }); -} - -#[test] -fn transferring_incomplete_reserved_balance_should_work() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::deposit_creating(&account_id(1), 110); - let _ = Balances::deposit_creating(&account_id(2), 1); - assert_ok!(Balances::reserve(&account_id(1), 41)); - assert_ok!(Balances::repatriate_reserved(&account_id(1), &account_id(2), 69, Free), 28); - assert_eq!(Balances::reserved_balance(account_id(1)), 0); - assert_eq!(Balances::free_balance(account_id(1)), 69); - assert_eq!(Balances::reserved_balance(account_id(2)), 0); - assert_eq!(Balances::free_balance(account_id(2)), 42); - }); -} - -#[test] -fn transferring_too_high_value_should_not_panic() { - ExtBuilder::default().build_and_execute_with(|| { - Balances::make_free_balance_be(&account_id(1), Balance::MAX); - Balances::make_free_balance_be(&account_id(2), 1); - - assert_err!( - >::transfer( - &account_id(1), - &account_id(2), - Balance::MAX, - AllowDeath - ), - ArithmeticError::Overflow, - ); - - assert_eq!(Balances::free_balance(account_id(1)), Balance::MAX); - assert_eq!(Balances::free_balance(account_id(2)), 1); - }); -} - -#[test] -fn account_create_on_free_too_low_with_other() { - ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { - let _ = Balances::deposit_creating(&account_id(1), 100); - assert_eq!(pallet_balances::TotalIssuance::::get(), 100); - - // No-op. - let _ = Balances::deposit_creating(&account_id(2), 50); - assert_eq!(Balances::free_balance(account_id(2)), 0); - assert_eq!(pallet_balances::TotalIssuance::::get(), 100); - }) -} - -#[test] -fn account_create_on_free_too_low() { - ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { - // No-op. - let _ = Balances::deposit_creating(&account_id(2), 50); - assert_eq!(Balances::free_balance(account_id(2)), 0); - assert_eq!(pallet_balances::TotalIssuance::::get(), 0); - }) -} - -#[test] -fn account_removal_on_free_too_low() { - ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { - assert_eq!(pallet_balances::TotalIssuance::::get(), 0); - - // Setup two accounts with free balance above the existential threshold. - let _ = Balances::deposit_creating(&account_id(1), 110); - let _ = Balances::deposit_creating(&account_id(2), 110); - - assert_eq!(Balances::free_balance(account_id(1)), 110); - assert_eq!(Balances::free_balance(account_id(2)), 110); - assert_eq!(pallet_balances::TotalIssuance::::get(), 220); - - // Transfer funds from account 1 of such amount that after this transfer - // the balance of account 1 will be below the existential threshold. - // This should lead to the removal of all balance of this account. - assert_ok!(Balances::transfer_allow_death(Some(account_id(1)).into(), account_id(2), 20)); - - // Verify free balance removal of account 1. - assert_eq!(Balances::free_balance(account_id(1)), 0); - assert_eq!(Balances::free_balance(account_id(2)), 130); - - // Verify that TotalIssuance tracks balance removal when free balance is too low. - assert_eq!(pallet_balances::TotalIssuance::::get(), 130); - }); -} - -#[test] -fn burn_must_work() { - ExtBuilder::default().monied(true).build_and_execute_with(|| { - let init_total_issuance = pallet_balances::TotalIssuance::::get(); - let imbalance = >::burn(10); - assert_eq!(pallet_balances::TotalIssuance::::get(), init_total_issuance - 10); - drop(imbalance); - assert_eq!(pallet_balances::TotalIssuance::::get(), init_total_issuance); - }); -} - -#[test] -#[should_panic = "the balance of any account should always be at least the existential deposit."] -fn cannot_set_genesis_value_below_ed() { - EXISTENTIAL_DEPOSIT.with(|v| *v.borrow_mut() = 11); - let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); - crate::GenesisConfig:: { balances: vec![(account_id(1), 10)] } - .assimilate_storage(&mut t) - .unwrap(); -} - -#[test] -#[should_panic = "duplicate balances in genesis."] -fn cannot_set_genesis_value_twice() { - let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); - crate::GenesisConfig:: { - balances: vec![(account_id(1), 10), (account_id(2), 20), (account_id(1), 15)], - } - .assimilate_storage(&mut t) - .unwrap(); -} - -#[test] -fn existential_deposit_respected_when_reserving() { - ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { - // Set balance to free and reserved at the existential deposit - assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), account_id(1), 101)); - // Check balance - assert_eq!(Balances::free_balance(account_id(1)), 101); - assert_eq!(Balances::reserved_balance(account_id(1)), 0); - - // Reserve some free balance - assert_ok!(Balances::reserve(&account_id(1), 1)); - // Check balance, the account should be ok. - assert_eq!(Balances::free_balance(account_id(1)), 100); - assert_eq!(Balances::reserved_balance(account_id(1)), 1); - - // Cannot reserve any more of the free balance. - assert_noop!(Balances::reserve(&account_id(1), 1), DispatchError::ConsumerRemaining); - }); -} - -#[test] -fn slash_fails_when_account_needed() { - ExtBuilder::default().existential_deposit(50).build_and_execute_with(|| { - // Set balance to free and reserved at the existential deposit - assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), account_id(1), 52)); - assert_ok!(Balances::reserve(&account_id(1), 1)); - // Check balance - assert_eq!(Balances::free_balance(account_id(1)), 51); - assert_eq!(Balances::reserved_balance(account_id(1)), 1); - - // Slash a small amount - let res = Balances::slash(&account_id(1), 1); - assert_eq!(res, (NegativeImbalance::new(1), 0)); - - // The account should be dead. - assert_eq!(Balances::free_balance(account_id(1)), 50); - assert_eq!(Balances::reserved_balance(account_id(1)), 1); - - // Slashing again doesn't work since we require the ED - let res = Balances::slash(&account_id(1), 1); - assert_eq!(res, (NegativeImbalance::new(0), 1)); - - // The account should be dead. - assert_eq!(Balances::free_balance(account_id(1)), 50); - assert_eq!(Balances::reserved_balance(account_id(1)), 1); - }); -} - -#[test] -fn account_deleted_when_just_dust() { - ExtBuilder::default().existential_deposit(50).build_and_execute_with(|| { - // Set balance to free and reserved at the existential deposit - assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), account_id(1), 50)); - // Check balance - assert_eq!(Balances::free_balance(account_id(1)), 50); - - // Slash a small amount - let res = Balances::slash(&account_id(1), 1); - assert_eq!(res, (NegativeImbalance::new(1), 0)); - - // The account should be dead. - assert_eq!(Balances::free_balance(account_id(1)), 0); - }); -} - -#[test] -fn emit_events_with_reserve_and_unreserve() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::deposit_creating(&account_id(1), 100); - - System::set_block_number(2); - assert_ok!(Balances::reserve(&account_id(1), 10)); - - System::assert_last_event(RuntimeEvent::Balances(crate::Event::Reserved { - who: account_id(1), - amount: 10, - })); - - System::set_block_number(3); - assert!(Balances::unreserve(&account_id(1), 5).is_zero()); - - System::assert_last_event(RuntimeEvent::Balances(crate::Event::Unreserved { - who: account_id(1), - amount: 5, - })); - - System::set_block_number(4); - assert_eq!(Balances::unreserve(&account_id(1), 6), 1); - - // should only unreserve 5 - System::assert_last_event(RuntimeEvent::Balances(crate::Event::Unreserved { - who: account_id(1), - amount: 5, - })); - }); -} - -#[test] -fn emit_events_with_changing_locks() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::deposit_creating(&account_id(1), 100); - System::reset_events(); - - // Locks = [] --> [10] - Balances::set_lock(*b"LOCK_000", &account_id(1), 10, WithdrawReasons::TRANSFER); - assert_eq!( - events(), - [RuntimeEvent::Balances(crate::Event::Locked { who: account_id(1), amount: 10 })] - ); - - // Locks = [10] --> [15] - Balances::set_lock(*b"LOCK_000", &account_id(1), 15, WithdrawReasons::TRANSFER); - assert_eq!( - events(), - [RuntimeEvent::Balances(crate::Event::Locked { who: account_id(1), amount: 5 })] - ); - - // Locks = [15] --> [15, 20] - Balances::set_lock(*b"LOCK_001", &account_id(1), 20, WithdrawReasons::TRANSACTION_PAYMENT); - assert_eq!( - events(), - [RuntimeEvent::Balances(crate::Event::Locked { who: account_id(1), amount: 5 })] - ); - - // Locks = [15, 20] --> [17, 20] - Balances::set_lock(*b"LOCK_000", &account_id(1), 17, WithdrawReasons::TRANSACTION_PAYMENT); - for event in events() { - match event { - RuntimeEvent::Balances(crate::Event::Locked { .. }) => { - assert!(false, "unexpected lock event") - }, - RuntimeEvent::Balances(crate::Event::Unlocked { .. }) => { - assert!(false, "unexpected unlock event") - }, - _ => continue, - } - } - - // Locks = [17, 20] --> [17, 15] - Balances::set_lock(*b"LOCK_001", &account_id(1), 15, WithdrawReasons::TRANSFER); - assert_eq!( - events(), - [RuntimeEvent::Balances(crate::Event::Unlocked { who: account_id(1), amount: 3 })] - ); - - // Locks = [17, 15] --> [15] - Balances::remove_lock(*b"LOCK_000", &account_id(1)); - assert_eq!( - events(), - [RuntimeEvent::Balances(crate::Event::Unlocked { who: account_id(1), amount: 2 })] - ); - - // Locks = [15] --> [] - Balances::remove_lock(*b"LOCK_001", &account_id(1)); - assert_eq!( - events(), - [RuntimeEvent::Balances(crate::Event::Unlocked { who: account_id(1), amount: 15 })] - ); - }); -} - -#[test] -fn emit_events_with_existential_deposit() { - ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { - assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), account_id(1), 100)); - - assert_eq!( - events(), - [ - RuntimeEvent::System(system::Event::NewAccount { account: account_id(1) }), - RuntimeEvent::Balances(crate::Event::Endowed { - account: account_id(1), - free_balance: 100 - }), - RuntimeEvent::Balances(crate::Event::Issued { amount: 100 }), - RuntimeEvent::Balances(crate::Event::BalanceSet { who: account_id(1), free: 100 }), - ] - ); - - let res = Balances::slash(&account_id(1), 1); - assert_eq!(res, (NegativeImbalance::new(1), 0)); - - assert_eq!( - events(), - [ - RuntimeEvent::System(system::Event::KilledAccount { account: account_id(1) }), - RuntimeEvent::Balances(crate::Event::DustLost { - account: account_id(1), - amount: 99 - }), - RuntimeEvent::Balances(crate::Event::Slashed { who: account_id(1), amount: 1 }), - RuntimeEvent::Balances(crate::Event::Rescinded { amount: 1 }), - ] - ); - }); -} - -#[test] -fn emit_events_with_no_existential_deposit_suicide() { - ExtBuilder::default().existential_deposit(1).build_and_execute_with(|| { - Balances::make_free_balance_be(&account_id(1), 100); - - assert_eq!( - events(), - [ - RuntimeEvent::Balances(crate::Event::BalanceSet { who: account_id(1), free: 100 }), - RuntimeEvent::System(system::Event::NewAccount { account: account_id(1) }), - RuntimeEvent::Balances(crate::Event::Endowed { - account: account_id(1), - free_balance: 100 - }), - RuntimeEvent::Balances(crate::Event::Issued { amount: 100 }), - ] - ); - - let res = Balances::slash(&account_id(1), 100); - assert_eq!(res, (NegativeImbalance::new(100), 0)); - - assert_eq!( - events(), - [ - RuntimeEvent::System(system::Event::KilledAccount { account: account_id(1) }), - RuntimeEvent::Balances(crate::Event::Slashed { who: account_id(1), amount: 100 }), - RuntimeEvent::Balances(crate::Event::Rescinded { amount: 100 }), - ] - ); - }); -} - -#[test] -fn slash_over_works() { - ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { - // SCENARIO: Over-slash will kill account, and report missing slash amount. - Balances::make_free_balance_be(&account_id(1), 1_000); - // Slashed full free_balance, and reports 300 not slashed - assert_eq!(Balances::slash(&account_id(1), 1_300), (NegativeImbalance::new(1000), 300)); - // Account is dead - assert!(!System::account_exists(&account_id(1))); - }); -} - -#[test] -fn slash_full_works() { - ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { - Balances::make_free_balance_be(&account_id(1), 1_000); - // Slashed completed in full - assert_eq!(Balances::slash(&account_id(1), 1_000), (NegativeImbalance::new(1000), 0)); - // Account is still alive - assert!(!System::account_exists(&account_id(1))); - System::assert_has_event(RuntimeEvent::Balances(crate::Event::Slashed { - who: account_id(1), - amount: 1000, - })); - }); -} - -#[test] -fn slash_partial_works() { - ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { - Balances::make_free_balance_be(&account_id(1), 1_000); - // Slashed completed in full - assert_eq!(Balances::slash(&account_id(1), 900), (NegativeImbalance::new(900), 0)); - // Account is still alive - assert!(System::account_exists(&account_id(1))); - System::assert_has_event(RuntimeEvent::Balances(crate::Event::Slashed { - who: account_id(1), - amount: 900, - })); - }); -} - -#[test] -fn slash_dusting_works() { - ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { - Balances::make_free_balance_be(&account_id(1), 1_000); - // Slashed completed in full - assert_eq!(Balances::slash(&account_id(1), 950), (NegativeImbalance::new(950), 0)); - assert!(!System::account_exists(&account_id(1))); - System::assert_has_event(RuntimeEvent::Balances(crate::Event::Slashed { - who: account_id(1), - amount: 950, - })); - }); -} - -#[test] -fn slash_does_not_take_from_reserve() { - ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { - Balances::make_free_balance_be(&account_id(1), 1_000); - assert_ok!(Balances::reserve(&account_id(1), 100)); - // Slashed completed in full - assert_eq!(Balances::slash(&account_id(1), 900), (NegativeImbalance::new(800), 100)); - assert_eq!(Balances::reserved_balance(account_id(1)), 100); - System::assert_has_event(RuntimeEvent::Balances(crate::Event::Slashed { - who: account_id(1), - amount: 800, - })); - }); -} - -#[test] -fn slash_consumed_slash_full_works() { - ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { - Balances::make_free_balance_be(&account_id(1), 1_000); - assert_ok!(System::inc_consumers(&account_id(1))); // <-- Reference counter added here is enough for all tests - // Slashed completed in full - assert_eq!(Balances::slash(&account_id(1), 900), (NegativeImbalance::new(900), 0)); - // Account is still alive - assert!(System::account_exists(&account_id(1))); - }); -} - -#[test] -fn slash_consumed_slash_over_works() { - ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { - Balances::make_free_balance_be(&account_id(1), 1_000); - assert_ok!(System::inc_consumers(&account_id(1))); // <-- Reference counter added here is enough for all tests - // Slashed completed in full - assert_eq!(Balances::slash(&account_id(1), 1_000), (NegativeImbalance::new(900), 100)); - // Account is still alive - assert!(System::account_exists(&account_id(1))); - }); -} - -#[test] -fn slash_consumed_slash_partial_works() { - ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { - Balances::make_free_balance_be(&account_id(1), 1_000); - assert_ok!(System::inc_consumers(&account_id(1))); // <-- Reference counter added here is enough for all tests - // Slashed completed in full - assert_eq!(Balances::slash(&account_id(1), 800), (NegativeImbalance::new(800), 0)); - // Account is still alive - assert!(System::account_exists(&account_id(1))); - }); -} - -#[test] -fn slash_on_non_existent_works() { - ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { - // Slash on non-existent account is okay. - assert_eq!(Balances::slash(&account_id(123), 1_300), (NegativeImbalance::new(0), 1300)); - }); -} - -#[test] -fn slash_reserved_slash_partial_works() { - ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { - Balances::make_free_balance_be(&account_id(1), 1_000); - assert_ok!(Balances::reserve(&account_id(1), 900)); - // Slashed completed in full - assert_eq!(Balances::slash_reserved(&account_id(1), 800), (NegativeImbalance::new(800), 0)); - assert_eq!(System::consumers(&account_id(1)), 1); - assert_eq!(Balances::reserved_balance(account_id(1)), 100); - assert_eq!(Balances::free_balance(account_id(1)), 100); - }); -} - -#[test] -fn slash_reserved_slash_everything_works() { - ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { - Balances::make_free_balance_be(&account_id(1), 1_000); - assert_ok!(Balances::reserve(&account_id(1), 900)); - assert_eq!(System::consumers(&account_id(1)), 1); - // Slashed completed in full - assert_eq!(Balances::slash_reserved(&account_id(1), 900), (NegativeImbalance::new(900), 0)); - assert_eq!(System::consumers(&account_id(1)), 0); - // Account is still alive - assert!(System::account_exists(&account_id(1))); - }); -} - -#[test] -fn slash_reserved_overslash_does_not_touch_free_balance() { - ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { - // SCENARIO: Over-slash doesn't touch free balance. - Balances::make_free_balance_be(&account_id(1), 1_000); - assert_ok!(Balances::reserve(&account_id(1), 800)); - // Slashed done - assert_eq!( - Balances::slash_reserved(&account_id(1), 900), - (NegativeImbalance::new(800), 100) - ); - assert_eq!(Balances::free_balance(account_id(1)), 200); - }); -} - -#[test] -fn slash_reserved_on_non_existent_works() { - ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { - // Slash on non-existent account is okay. - assert_eq!( - Balances::slash_reserved(&account_id(123), 1_300), - (NegativeImbalance::new(0), 1300) - ); - }); -} - -#[test] -fn operations_on_dead_account_should_not_change_state() { - // These functions all use `mutate_account` which may introduce a storage change when - // the account never existed to begin with, and shouldn't exist in the end. - ExtBuilder::default().existential_deposit(1).build_and_execute_with(|| { - assert!(!frame_system::Account::::contains_key(account_id(137))); - - // Unreserve - assert_storage_noop!(assert_eq!(Balances::unreserve(&account_id(137), 42), 42)); - // Reserve - assert_noop!( - Balances::reserve(&account_id(137), 42), - Error::::InsufficientBalance - ); - // Slash Reserve - assert_storage_noop!(assert_eq!(Balances::slash_reserved(&account_id(137), 42).1, 42)); - // Repatriate Reserve - assert_noop!( - Balances::repatriate_reserved(&account_id(137), &account_id(138), 42, Free), - Error::::DeadAccount - ); - // Slash - assert_storage_noop!(assert_eq!(Balances::slash(&account_id(137), 42).1, 42)); - }); -} - -#[test] -#[should_panic = "The existential deposit must be greater than zero!"] -fn zero_ed_is_prohibited() { - // These functions all use `mutate_account` which may introduce a storage change when - // the account never existed to begin with, and shouldn't exist in the end. - ExtBuilder::default().existential_deposit(0).build_and_execute_with(|| { - Balances::integrity_test(); - }); -} - -#[test] -fn named_reserve_should_work() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::deposit_creating(&account_id(1), 111); - - let id_1 = TestId::Foo; - let id_2 = TestId::Bar; - let id_3 = TestId::Baz; - - // reserve - - assert_noop!( - Balances::reserve_named(&id_1, &account_id(1), 112), - Error::::InsufficientBalance - ); - - assert_ok!(Balances::reserve_named(&id_1, &account_id(1), 12)); - - assert_eq!(Balances::reserved_balance(account_id(1)), 12); - assert_eq!(Balances::reserved_balance_named(&id_1, &account_id(1)), 12); - assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(1)), 0); - - assert_ok!(Balances::reserve_named(&id_1, &account_id(1), 2)); - - assert_eq!(Balances::reserved_balance(account_id(1)), 14); - assert_eq!(Balances::reserved_balance_named(&id_1, &account_id(1)), 14); - assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(1)), 0); - - assert_ok!(Balances::reserve_named(&id_2, &account_id(1), 23)); - - assert_eq!(Balances::reserved_balance(account_id(1)), 37); - assert_eq!(Balances::reserved_balance_named(&id_1, &account_id(1)), 14); - assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(1)), 23); - - assert_ok!(Balances::reserve(&account_id(1), 34)); - - assert_eq!(Balances::reserved_balance(account_id(1)), 71); - assert_eq!(Balances::reserved_balance_named(&id_1, &account_id(1)), 14); - assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(1)), 23); - - assert_eq!(Balances::total_balance(&account_id(1)), 111); - assert_eq!(Balances::free_balance(account_id(1)), 40); - - assert_noop!( - Balances::reserve_named(&id_3, &account_id(1), 2), - Error::::TooManyReserves - ); - - // unreserve - - assert_eq!(Balances::unreserve_named(&id_1, &account_id(1), 10), 0); - - assert_eq!(Balances::reserved_balance(account_id(1)), 61); - assert_eq!(Balances::reserved_balance_named(&id_1, &account_id(1)), 4); - assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(1)), 23); - - assert_eq!(Balances::unreserve_named(&id_1, &account_id(1), 5), 1); - - assert_eq!(Balances::reserved_balance(account_id(1)), 57); - assert_eq!(Balances::reserved_balance_named(&id_1, &account_id(1)), 0); - assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(1)), 23); - - assert_eq!(Balances::unreserve_named(&id_2, &account_id(1), 3), 0); - - assert_eq!(Balances::reserved_balance(account_id(1)), 54); - assert_eq!(Balances::reserved_balance_named(&id_1, &account_id(1)), 0); - assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(1)), 20); - - assert_eq!(Balances::total_balance(&account_id(1)), 111); - assert_eq!(Balances::free_balance(account_id(1)), 57); - - // slash_reserved_named - - assert_ok!(Balances::reserve_named(&id_1, &account_id(1), 10)); - - assert_eq!(Balances::slash_reserved_named(&id_1, &account_id(1), 25).1, 15); - - assert_eq!(Balances::reserved_balance(account_id(1)), 54); - assert_eq!(Balances::reserved_balance_named(&id_1, &account_id(1)), 0); - assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(1)), 20); - assert_eq!(Balances::total_balance(&account_id(1)), 101); - - assert_eq!(Balances::slash_reserved_named(&id_2, &account_id(1), 5).1, 0); - - assert_eq!(Balances::reserved_balance(account_id(1)), 49); - assert_eq!(Balances::reserved_balance_named(&id_1, &account_id(1)), 0); - assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(1)), 15); - assert_eq!(Balances::total_balance(&account_id(1)), 96); - - // repatriate_reserved_named - - let _ = Balances::deposit_creating(&account_id(2), 100); - - assert_eq!( - Balances::repatriate_reserved_named( - &id_2, - &account_id(1), - &account_id(2), - 10, - Reserved - ) - .unwrap(), - 0 - ); - - assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(1)), 5); - assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(2)), 10); - assert_eq!(Balances::reserved_balance(account_id(2)), 10); - - assert_eq!( - Balances::repatriate_reserved_named( - &id_2, - &account_id(2), - &account_id(1), - 11, - Reserved - ) - .unwrap(), - 1 - ); - - assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(1)), 15); - assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(2)), 0); - assert_eq!(Balances::reserved_balance(account_id(2)), 0); - - assert_eq!( - Balances::repatriate_reserved_named(&id_2, &account_id(1), &account_id(2), 10, Free) - .unwrap(), - 0 - ); - assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(1)), 5); - assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(2)), 0); - assert_eq!(Balances::free_balance(account_id(2)), 110); - - // repatriate_reserved_named to self - - assert_eq!( - Balances::repatriate_reserved_named( - &id_2, - &account_id(1), - &account_id(1), - 10, - Reserved - ) - .unwrap(), - 5 - ); - assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(1)), 5); - - assert_eq!(Balances::free_balance(account_id(1)), 47); - - assert_eq!( - Balances::repatriate_reserved_named(&id_2, &account_id(1), &account_id(1), 15, Free) - .unwrap(), - 10 - ); - assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(1)), 0); - - assert_eq!(Balances::free_balance(account_id(1)), 52); - }); -} - -#[test] -fn reserve_must_succeed_if_can_reserve_does() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::deposit_creating(&account_id(1), 1); - let _ = Balances::deposit_creating(&account_id(2), 2); - assert!( - Balances::can_reserve(&account_id(1), 1) == - Balances::reserve(&account_id(1), 1).is_ok() - ); - assert!( - Balances::can_reserve(&account_id(2), 1) == - Balances::reserve(&account_id(2), 1).is_ok() - ); - }); -} - -#[test] -fn reserved_named_to_yourself_should_work() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::deposit_creating(&account_id(1), 110); - - let id = TestId::Foo; - - assert_ok!(Balances::reserve_named(&id, &account_id(1), 50)); - assert_ok!( - Balances::repatriate_reserved_named(&id, &account_id(1), &account_id(1), 50, Free), - 0 - ); - assert_eq!(Balances::free_balance(account_id(1)), 110); - assert_eq!(Balances::reserved_balance_named(&id, &account_id(1)), 0); - - assert_ok!(Balances::reserve_named(&id, &account_id(1), 50)); - assert_ok!( - Balances::repatriate_reserved_named(&id, &account_id(1), &account_id(1), 60, Free), - 10 - ); - assert_eq!(Balances::free_balance(account_id(1)), 110); - assert_eq!(Balances::reserved_balance_named(&id, &account_id(1)), 0); - }); -} - -#[test] -fn ensure_reserved_named_should_work() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::deposit_creating(&account_id(1), 111); - - let id = TestId::Foo; - - assert_ok!(Balances::ensure_reserved_named(&id, &account_id(1), 15)); - assert_eq!(Balances::reserved_balance_named(&id, &account_id(1)), 15); - - assert_ok!(Balances::ensure_reserved_named(&id, &account_id(1), 10)); - assert_eq!(Balances::reserved_balance_named(&id, &account_id(1)), 10); - - assert_ok!(Balances::ensure_reserved_named(&id, &account_id(1), 20)); - assert_eq!(Balances::reserved_balance_named(&id, &account_id(1)), 20); - }); -} - -#[test] -fn unreserve_all_named_should_work() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::deposit_creating(&account_id(1), 111); - - let id = TestId::Foo; - - assert_ok!(Balances::reserve_named(&id, &account_id(1), 15)); - - assert_eq!(Balances::unreserve_all_named(&id, &account_id(1)), 15); - assert_eq!(Balances::reserved_balance_named(&id, &account_id(1)), 0); - assert_eq!(Balances::free_balance(account_id(1)), 111); - - assert_eq!(Balances::unreserve_all_named(&id, &account_id(1)), 0); - }); -} - -#[test] -fn slash_all_reserved_named_should_work() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::deposit_creating(&account_id(1), 111); - - let id = TestId::Foo; - - assert_ok!(Balances::reserve_named(&id, &account_id(1), 15)); - - assert_eq!(Balances::slash_all_reserved_named(&id, &account_id(1)).peek(), 15); - assert_eq!(Balances::reserved_balance_named(&id, &account_id(1)), 0); - assert_eq!(Balances::free_balance(account_id(1)), 96); - - assert_eq!(Balances::slash_all_reserved_named(&id, &account_id(1)).peek(), 0); - }); -} - -#[test] -fn repatriate_all_reserved_named_should_work() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::deposit_creating(&account_id(1), 111); - let _ = Balances::deposit_creating(&account_id(2), 10); - let _ = Balances::deposit_creating(&account_id(3), 10); - - let id = TestId::Foo; - - assert_ok!(Balances::reserve_named(&id, &account_id(1), 15)); - - assert_ok!(Balances::repatriate_all_reserved_named( - &id, - &account_id(1), - &account_id(2), - Reserved - )); - assert_eq!(Balances::reserved_balance_named(&id, &account_id(1)), 0); - assert_eq!(Balances::reserved_balance_named(&id, &account_id(2)), 15); - - assert_ok!(Balances::repatriate_all_reserved_named( - &id, - &account_id(2), - &account_id(3), - Free - )); - assert_eq!(Balances::reserved_balance_named(&id, &account_id(2)), 0); - assert_eq!(Balances::free_balance(account_id(3)), 25); - }); -} - -#[test] -fn freezing_and_locking_should_work() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - // Consumer is shared between freezing and locking. - assert_eq!(System::consumers(&account_id(1)), 0); - assert_ok!(>::set_freeze( - &TestId::Foo, - &account_id(1), - 4 - )); - assert_eq!(System::consumers(&account_id(1)), 1); - Balances::set_lock(ID_1, &account_id(1), 5, WithdrawReasons::all()); - assert_eq!(System::consumers(&account_id(1)), 1); - - // Frozen and locked balances update correctly. - assert_eq!(Balances::account(&account_id(1)).frozen, 5); - assert_ok!(>::set_freeze( - &TestId::Foo, - &account_id(1), - 6 - )); - assert_eq!(Balances::account(&account_id(1)).frozen, 6); - assert_ok!(>::set_freeze( - &TestId::Foo, - &account_id(1), - 4 - )); - assert_eq!(Balances::account(&account_id(1)).frozen, 5); - Balances::set_lock(ID_1, &account_id(1), 3, WithdrawReasons::all()); - assert_eq!(Balances::account(&account_id(1)).frozen, 4); - Balances::set_lock(ID_1, &account_id(1), 5, WithdrawReasons::all()); - assert_eq!(Balances::account(&account_id(1)).frozen, 5); - - // Locks update correctly. - Balances::remove_lock(ID_1, &account_id(1)); - assert_eq!(Balances::account(&account_id(1)).frozen, 4); - assert_ok!(>::thaw(&TestId::Foo, &account_id(1))); - assert_eq!(Balances::account(&account_id(1)).frozen, 0); - assert_eq!(System::consumers(&account_id(1)), 0); - }); -} - -#[test] -fn self_transfer_noop() { - ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { - assert_eq!(pallet_balances::TotalIssuance::::get(), 0); - let _ = Balances::deposit_creating(&account_id(1), 100); - - // The account is set up properly: - assert_eq!( - events(), - [ - Event::Deposit { who: account_id(1), amount: 100 }.into(), - SysEvent::NewAccount { account: account_id(1) }.into(), - Event::Endowed { account: account_id(1), free_balance: 100 }.into(), - Event::Issued { amount: 100 }.into(), - ] - ); - assert_eq!(Balances::free_balance(account_id(1)), 100); - assert_eq!(pallet_balances::TotalIssuance::::get(), 100); - - // Transfers to self are No-OPs: - let _g = StorageNoopGuard::new(); - for i in 0..200 { - let r = Balances::transfer_allow_death(Some(account_id(1)).into(), account_id(1), i); - - if i <= 100 { - assert_ok!(r); - } else { - assert!(r.is_err()); - } - - assert!(events().is_empty()); - assert_eq!( - Balances::free_balance(account_id(1)), - 100, - "Balance unchanged by self transfer" - ); - assert_eq!( - pallet_balances::TotalIssuance::::get(), - 100, - "TI unchanged by self transfers" - ); - } - }); -} diff --git a/pallets/balances/src/tests/dispatchable_tests.rs b/pallets/balances/src/tests/dispatchable_tests.rs deleted file mode 100644 index 94991c96..00000000 --- a/pallets/balances/src/tests/dispatchable_tests.rs +++ /dev/null @@ -1,410 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Tests regarding the functionality of the dispatchables/extrinsics. - -use super::*; -use crate::{ - AdjustmentDirection::{Decrease as Dec, Increase as Inc}, - Event, -}; -use frame_support::traits::{fungible::Unbalanced, tokens::Preservation::Expendable}; -use fungible::{hold::Mutate as HoldMutate, Inspect, Mutate}; - -/// Alice account ID for more readable tests. -fn alice() -> AccountId { - account_id(1) -} - -#[test] -fn default_indexing_on_new_accounts_should_not_work2() { - ExtBuilder::default() - .existential_deposit(10) - .monied(true) - .build_and_execute_with(|| { - // account 5 should not exist - // ext_deposit is 10, value is 9, not satisfies for ext_deposit - assert_noop!( - Balances::transfer_allow_death(Some(account_id(1)).into(), account_id(5), 9), - TokenError::BelowMinimum, - ); - assert_eq!(Balances::free_balance(account_id(1)), 100); - }); -} - -#[test] -fn dust_account_removal_should_work() { - ExtBuilder::default() - .existential_deposit(100) - .monied(true) - .build_and_execute_with(|| { - System::inc_account_nonce(account_id(2)); - assert_eq!(System::account_nonce(account_id(2)), 1); - assert_eq!(Balances::total_balance(&account_id(2)), 2000); - // index 1 (account 2) becomes zombie - assert_ok!(Balances::transfer_allow_death( - Some(account_id(2)).into(), - account_id(5), - 1901 - )); - assert_eq!(Balances::total_balance(&account_id(2)), 0); - assert_eq!(Balances::total_balance(&account_id(5)), 1901); - assert_eq!(System::account_nonce(account_id(2)), 0); - }); -} - -#[test] -fn balance_transfer_works() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::mint_into(&account_id(1), 111); - assert_ok!(Balances::transfer_allow_death(Some(account_id(1)).into(), account_id(2), 69)); - assert_eq!(Balances::total_balance(&account_id(1)), 42); - assert_eq!(Balances::total_balance(&account_id(2)), 69); - }); -} - -#[test] -fn force_transfer_works() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::mint_into(&account_id(1), 111); - assert_noop!( - Balances::force_transfer(Some(account_id(2)).into(), account_id(1), account_id(2), 69), - BadOrigin, - ); - assert_ok!(Balances::force_transfer( - RawOrigin::Root.into(), - account_id(1), - account_id(2), - 69 - )); - assert_eq!(Balances::total_balance(&account_id(1)), 42); - assert_eq!(Balances::total_balance(&account_id(2)), 69); - }); -} - -#[test] -fn balance_transfer_when_on_hold_should_not_work() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::mint_into(&account_id(1), 111); - assert_ok!(Balances::hold(&TestId::Foo, &account_id(1), 69)); - assert_noop!( - Balances::transfer_allow_death(Some(account_id(1)).into(), account_id(2), 69), - TokenError::FundsUnavailable, - ); - }); -} - -#[test] -fn transfer_keep_alive_works() { - ExtBuilder::default().existential_deposit(1).build_and_execute_with(|| { - let _ = Balances::mint_into(&account_id(1), 100); - assert_noop!( - Balances::transfer_keep_alive(Some(account_id(1)).into(), account_id(2), 100), - TokenError::NotExpendable - ); - assert_eq!(Balances::total_balance(&account_id(1)), 100); - assert_eq!(Balances::total_balance(&account_id(2)), 0); - }); -} - -#[test] -fn transfer_keep_alive_all_free_succeed() { - ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { - assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), account_id(1), 300)); - assert_ok!(Balances::hold(&TestId::Foo, &account_id(1), 100)); - assert_ok!(Balances::transfer_keep_alive(Some(account_id(1)).into(), account_id(2), 100)); - assert_eq!(Balances::total_balance(&account_id(1)), 200); - assert_eq!(Balances::total_balance(&account_id(2)), 100); - }); -} - -#[test] -fn transfer_all_works_1() { - ExtBuilder::default().existential_deposit(100).build().execute_with(|| { - // setup - assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), account_id(1), 200)); - assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), account_id(2), 0)); - // transfer all and allow death - assert_ok!(Balances::transfer_all(Some(account_id(1)).into(), account_id(2), false)); - assert_eq!(Balances::total_balance(&account_id(1)), 0); - assert_eq!(Balances::total_balance(&account_id(2)), 200); - }); -} - -#[test] -fn transfer_all_works_2() { - ExtBuilder::default().existential_deposit(100).build().execute_with(|| { - // setup - assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), account_id(1), 200)); - assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), account_id(2), 0)); - // transfer all and keep alive - assert_ok!(Balances::transfer_all(Some(account_id(1)).into(), account_id(2), true)); - assert_eq!(Balances::total_balance(&account_id(1)), 100); - assert_eq!(Balances::total_balance(&account_id(2)), 100); - }); -} - -#[test] -fn transfer_all_works_3() { - ExtBuilder::default().existential_deposit(100).build().execute_with(|| { - // setup - assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), account_id(1), 210)); - assert_ok!(Balances::hold(&TestId::Foo, &account_id(1), 10)); - assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), account_id(2), 0)); - // transfer all and allow death w/ reserved - assert_ok!(Balances::transfer_all(Some(account_id(1)).into(), account_id(2), false)); - assert_eq!(Balances::total_balance(&account_id(1)), 110); - assert_eq!(Balances::total_balance(&account_id(2)), 100); - }); -} - -#[test] -fn transfer_all_works_4() { - ExtBuilder::default().existential_deposit(100).build().execute_with(|| { - // setup - assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), account_id(1), 210)); - assert_ok!(Balances::hold(&TestId::Foo, &account_id(1), 10)); - assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), account_id(2), 0)); - // transfer all and keep alive w/ reserved - assert_ok!(Balances::transfer_all(Some(account_id(1)).into(), account_id(2), true)); - assert_eq!(Balances::total_balance(&account_id(1)), 110); - assert_eq!(Balances::total_balance(&account_id(2)), 100); - }); -} - -#[test] -fn set_balance_handles_killing_account() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::mint_into(&account_id(1), 111); - assert_ok!(frame_system::Pallet::::inc_consumers(&account_id(1))); - assert_noop!( - Balances::force_set_balance(RuntimeOrigin::root(), account_id(1), 0), - DispatchError::ConsumerRemaining, - ); - }); -} - -#[test] -fn set_balance_handles_total_issuance() { - ExtBuilder::default().build_and_execute_with(|| { - let old_total_issuance = pallet_balances::TotalIssuance::::get(); - assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), account_id(137), 69)); - assert_eq!(pallet_balances::TotalIssuance::::get(), old_total_issuance + 69); - assert_eq!(Balances::total_balance(&account_id(137)), 69); - assert_eq!(Balances::free_balance(account_id(137)), 69); - }); -} - -#[test] -fn upgrade_accounts_should_work() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - System::inc_providers(&account_id(7)); - assert_ok!(::AccountStore::try_mutate_exists( - &account_id(7), - |a| -> DispatchResult { - *a = Some(AccountData { - free: 5, - reserved: 5, - frozen: Zero::zero(), - flags: crate::types::ExtraFlags::old_logic(), - }); - Ok(()) - } - )); - assert!(!Balances::account(&account_id(7)).flags.is_new_logic()); - assert_eq!(System::providers(&account_id(7)), 1); - assert_eq!(System::consumers(&account_id(7)), 0); - assert_ok!(Balances::upgrade_accounts(Some(account_id(1)).into(), vec![account_id(7)])); - assert!(Balances::account(&account_id(7)).flags.is_new_logic()); - assert_eq!(System::providers(&account_id(7)), 1); - assert_eq!(System::consumers(&account_id(7)), 1); - - >::unreserve( - &account_id(7), - 5, - ); - assert_ok!(>::transfer( - &account_id(7), - &account_id(1), - 10, - Expendable - )); - assert_eq!(Balances::total_balance(&account_id(7)), 0); - assert_eq!(System::providers(&account_id(7)), 0); - assert_eq!(System::consumers(&account_id(7)), 0); - }); -} - -#[test] -#[docify::export] -fn force_adjust_total_issuance_example() { - ExtBuilder::default().build_and_execute_with(|| { - // First we set the TotalIssuance to 64 by giving Alice a balance of 64. - assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), alice(), 64)); - let old_ti = pallet_balances::TotalIssuance::::get(); - assert_eq!(old_ti, 64, "TI should be 64"); - - // Now test the increase: - assert_ok!(Balances::force_adjust_total_issuance(RawOrigin::Root.into(), Inc, 32)); - let new_ti = pallet_balances::TotalIssuance::::get(); - assert_eq!(old_ti + 32, new_ti, "Should increase by 32"); - - // If Alice tries to call it, it errors: - assert_noop!( - Balances::force_adjust_total_issuance(RawOrigin::Signed(alice()).into(), Inc, 69), - BadOrigin, - ); - }); -} - -#[test] -fn force_adjust_total_issuance_works() { - ExtBuilder::default().build_and_execute_with(|| { - assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), account_id(137), 64)); - let ti = pallet_balances::TotalIssuance::::get(); - - // Increase works: - assert_ok!(Balances::force_adjust_total_issuance(RawOrigin::Root.into(), Inc, 32)); - assert_eq!(pallet_balances::TotalIssuance::::get(), ti + 32); - System::assert_last_event(RuntimeEvent::Balances(Event::TotalIssuanceForced { - old: 64, - new: 96, - })); - - // Decrease works: - assert_ok!(Balances::force_adjust_total_issuance(RawOrigin::Root.into(), Dec, 64)); - assert_eq!(pallet_balances::TotalIssuance::::get(), ti - 32); - System::assert_last_event(RuntimeEvent::Balances(Event::TotalIssuanceForced { - old: 96, - new: 32, - })); - }); -} - -#[test] -fn force_adjust_total_issuance_saturates() { - ExtBuilder::default().build_and_execute_with(|| { - assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), account_id(137), 64)); - let ti = pallet_balances::TotalIssuance::::get(); - let max = ::Balance::max_value(); - assert_eq!(ti, 64); - - // Increment saturates: - assert_ok!(Balances::force_adjust_total_issuance(RawOrigin::Root.into(), Inc, max)); - assert_ok!(Balances::force_adjust_total_issuance(RawOrigin::Root.into(), Inc, 123)); - assert_eq!(pallet_balances::TotalIssuance::::get(), max); - - // Decrement saturates: - assert_ok!(Balances::force_adjust_total_issuance(RawOrigin::Root.into(), Dec, max)); - assert_ok!(Balances::force_adjust_total_issuance(RawOrigin::Root.into(), Dec, 123)); - assert_eq!(pallet_balances::TotalIssuance::::get(), 0); - }); -} - -#[test] -fn force_adjust_total_issuance_rejects_zero_delta() { - ExtBuilder::default().build_and_execute_with(|| { - assert_noop!( - Balances::force_adjust_total_issuance(RawOrigin::Root.into(), Inc, 0), - Error::::DeltaZero, - ); - assert_noop!( - Balances::force_adjust_total_issuance(RawOrigin::Root.into(), Dec, 0), - Error::::DeltaZero, - ); - }); -} - -#[test] -fn force_adjust_total_issuance_rejects_more_than_inactive() { - ExtBuilder::default().build_and_execute_with(|| { - assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), account_id(137), 64)); - Balances::deactivate(16u32.into()); - - assert_eq!(pallet_balances::TotalIssuance::::get(), 64); - assert_eq!(Balances::active_issuance(), 48); - - // Works with up to 48: - assert_ok!(Balances::force_adjust_total_issuance(RawOrigin::Root.into(), Dec, 40),); - assert_ok!(Balances::force_adjust_total_issuance(RawOrigin::Root.into(), Dec, 8),); - assert_eq!(pallet_balances::TotalIssuance::::get(), 16); - assert_eq!(Balances::active_issuance(), 0); - // Errors with more than 48: - assert_noop!( - Balances::force_adjust_total_issuance(RawOrigin::Root.into(), Dec, 1), - Error::::IssuanceDeactivated, - ); - // Increasing again increases the inactive issuance: - assert_ok!(Balances::force_adjust_total_issuance(RawOrigin::Root.into(), Inc, 10),); - assert_eq!(pallet_balances::TotalIssuance::::get(), 26); - assert_eq!(Balances::active_issuance(), 10); - }); -} - -#[test] -fn burn_works() { - ExtBuilder::default().build().execute_with(|| { - // Prepare account with initial balance - let (account, init_balance) = (account_id(1), 37); - assert_ok!(Balances::force_set_balance( - RuntimeOrigin::root(), - account.clone(), - init_balance - )); - let init_issuance = pallet_balances::TotalIssuance::::get(); - let (keep_alive, allow_death) = (true, false); - - // 1. Cannot burn more than what's available - assert_noop!( - Balances::burn(Some(account.clone()).into(), init_balance + 1, allow_death), - TokenError::FundsUnavailable, - ); - - // 2. Burn some funds, without reaping the account - let burn_amount_1 = 1; - assert_ok!(Balances::burn(Some(account.clone()).into(), burn_amount_1, allow_death)); - System::assert_last_event(RuntimeEvent::Balances(Event::Burned { - who: account.clone(), - amount: burn_amount_1, - })); - assert_eq!(pallet_balances::TotalIssuance::::get(), init_issuance - burn_amount_1); - assert_eq!(Balances::total_balance(&account), init_balance - burn_amount_1); - - // 3. Cannot burn funds below existential deposit if `keep_alive` is `true` - let burn_amount_2 = - init_balance - burn_amount_1 - ::ExistentialDeposit::get() + 1; - assert_noop!( - Balances::burn(Some(account.clone()).into(), init_balance + 1, keep_alive), - TokenError::FundsUnavailable, - ); - - // 4. Burn some more funds, this time reaping the account - assert_ok!(Balances::burn(Some(account.clone()).into(), burn_amount_2, allow_death)); - System::assert_last_event(RuntimeEvent::Balances(Event::Burned { - who: account.clone(), - amount: burn_amount_2, - })); - assert_eq!( - pallet_balances::TotalIssuance::::get(), - init_issuance - burn_amount_1 - burn_amount_2 - ); - assert!(Balances::total_balance(&account).is_zero()); - }); -} diff --git a/pallets/balances/src/tests/fungible_conformance_tests.rs b/pallets/balances/src/tests/fungible_conformance_tests.rs deleted file mode 100644 index 5c0c19a5..00000000 --- a/pallets/balances/src/tests/fungible_conformance_tests.rs +++ /dev/null @@ -1,141 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use super::*; -use frame_support::traits::fungible::{conformance_tests, Inspect, Mutate}; -use paste::paste; - -macro_rules! generate_tests { - // Handle a conformance test that requires special testing with and without a dust trap. - (dust_trap_variation, $base_path:path, $scope:expr, $trait:ident, $ext_deposit:expr, $($test_name:ident),*) => { - $( - paste! { - #[test] - fn [<$trait _ $scope _ $test_name _existential_deposit_ $ext_deposit _dust_trap_on >]() { - // Some random trap account. - let trap_account = ::AccountId::from(65174286u64); - let builder = ExtBuilder::default().existential_deposit($ext_deposit).dust_trap(trap_account); - builder.build_and_execute_with(|| { - Balances::set_balance(&trap_account, Balances::minimum_balance()); - $base_path::$scope::$trait::$test_name::< - Balances, - ::AccountId, - >(Some(trap_account)); - }); - } - - #[test] - fn [< $trait _ $scope _ $test_name _existential_deposit_ $ext_deposit _dust_trap_off >]() { - let builder = ExtBuilder::default().existential_deposit($ext_deposit); - builder.build_and_execute_with(|| { - $base_path::$scope::$trait::$test_name::< - Balances, - ::AccountId, - >(None); - }); - } - } - )* - }; - // Regular conformance test - ($base_path:path, $scope:expr, $trait:ident, $ext_deposit:expr, $($test_name:ident),*) => { - $( - paste! { - #[test] - fn [< $trait _ $scope _ $test_name _existential_deposit_ $ext_deposit>]() { - let builder = ExtBuilder::default().existential_deposit($ext_deposit); - builder.build_and_execute_with(|| { - $base_path::$scope::$trait::$test_name::< - Balances, - ::AccountId, - >(); - }); - } - } - )* - }; - ($base_path:path, $ext_deposit:expr) => { - // regular::mutate - generate_tests!( - dust_trap_variation, - $base_path, - regular, - mutate, - $ext_deposit, - transfer_expendable_dust - ); - generate_tests!( - $base_path, - regular, - mutate, - $ext_deposit, - mint_into_success, - mint_into_overflow, - mint_into_below_minimum, - burn_from_exact_success, - burn_from_best_effort_success, - burn_from_exact_insufficient_funds, - restore_success, - restore_overflow, - restore_below_minimum, - shelve_success, - shelve_insufficient_funds, - transfer_success, - transfer_expendable_all, - transfer_protect_preserve, - set_balance_mint_success, - set_balance_burn_success, - can_deposit_success, - can_deposit_below_minimum, - can_deposit_overflow, - can_withdraw_success, - can_withdraw_reduced_to_zero, - can_withdraw_balance_low, - reducible_balance_expendable, - reducible_balance_protect_preserve - ); - // regular::unbalanced - generate_tests!( - $base_path, - regular, - unbalanced, - $ext_deposit, - write_balance, - decrease_balance_expendable, - decrease_balance_preserve, - increase_balance, - set_total_issuance, - deactivate_and_reactivate - ); - // regular::balanced - generate_tests!( - $base_path, - regular, - balanced, - $ext_deposit, - issue_and_resolve_credit, - rescind_and_settle_debt, - deposit, - withdraw, - pair - ); - }; -} - -generate_tests!(conformance_tests, 1); -generate_tests!(conformance_tests, 5); -generate_tests!(conformance_tests, 1000); diff --git a/pallets/balances/src/tests/fungible_tests.rs b/pallets/balances/src/tests/fungible_tests.rs deleted file mode 100644 index 4840258e..00000000 --- a/pallets/balances/src/tests/fungible_tests.rs +++ /dev/null @@ -1,883 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Tests regarding the functionality of the `fungible` trait set implementations. - -use super::*; -use frame_support::traits::{ - tokens::{ - Fortitude::{Force, Polite}, - Precision::{BestEffort, Exact}, - Preservation::{Expendable, Preserve, Protect}, - Restriction::Free, - }, - Consideration, Footprint, LinearStoragePrice, MaybeConsideration, -}; -use fungible::{ - FreezeConsideration, HoldConsideration, Inspect, InspectFreeze, InspectHold, - LoneFreezeConsideration, LoneHoldConsideration, Mutate, MutateFreeze, MutateHold, Unbalanced, -}; -use sp_core::ConstU128; - -#[test] -fn inspect_trait_reducible_balance_basic_works() { - ExtBuilder::default().existential_deposit(10).build_and_execute_with(|| { - Balances::set_balance(&account_id(1), 100); - assert_eq!(Balances::reducible_balance(&account_id(1), Expendable, Polite), 100); - assert_eq!(Balances::reducible_balance(&account_id(1), Protect, Polite), 90); - assert_eq!(Balances::reducible_balance(&account_id(1), Preserve, Polite), 90); - assert_eq!(Balances::reducible_balance(&account_id(1), Expendable, Force), 100); - assert_eq!(Balances::reducible_balance(&account_id(1), Protect, Force), 90); - assert_eq!(Balances::reducible_balance(&account_id(1), Preserve, Force), 90); - }); -} - -#[test] -fn inspect_trait_reducible_balance_other_provide_works() { - ExtBuilder::default().existential_deposit(10).build_and_execute_with(|| { - Balances::set_balance(&account_id(1), 100); - System::inc_providers(&account_id(1)); - assert_eq!(Balances::reducible_balance(&account_id(1), Expendable, Polite), 100); - assert_eq!(Balances::reducible_balance(&account_id(1), Protect, Polite), 100); - assert_eq!(Balances::reducible_balance(&account_id(1), Preserve, Polite), 90); - assert_eq!(Balances::reducible_balance(&account_id(1), Expendable, Force), 100); - assert_eq!(Balances::reducible_balance(&account_id(1), Protect, Force), 100); - assert_eq!(Balances::reducible_balance(&account_id(1), Preserve, Force), 90); - }); -} - -#[test] -fn inspect_trait_reducible_balance_frozen_works() { - ExtBuilder::default().existential_deposit(10).build_and_execute_with(|| { - Balances::set_balance(&account_id(1), 100); - assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), 50)); - assert_eq!(Balances::reducible_balance(&account_id(1), Expendable, Polite), 50); - assert_eq!(Balances::reducible_balance(&account_id(1), Protect, Polite), 50); - assert_eq!(Balances::reducible_balance(&account_id(1), Preserve, Polite), 50); - assert_eq!(Balances::reducible_balance(&account_id(1), Expendable, Force), 90); - assert_eq!(Balances::reducible_balance(&account_id(1), Protect, Force), 90); - assert_eq!(Balances::reducible_balance(&account_id(1), Preserve, Force), 90); - }); -} - -#[test] -fn unbalanced_trait_set_balance_works() { - ExtBuilder::default().build_and_execute_with(|| { - assert_eq!(>::balance(&account_id(137)), 0); - assert_ok!(Balances::write_balance(&account_id(137), 100)); - assert_eq!(>::balance(&account_id(137)), 100); - - assert_ok!(>::hold(&TestId::Foo, &account_id(137), 60)); - assert_eq!(>::balance(&account_id(137)), 40); - assert_eq!( - >::total_balance_on_hold(&account_id(137)), - 60 - ); - assert_eq!( - >::balance_on_hold(&TestId::Foo, &account_id(137)), - 60 - ); - - assert_noop!( - Balances::write_balance(&account_id(137), 0), - Error::::InsufficientBalance - ); - - assert_ok!(Balances::write_balance(&account_id(137), 1)); - assert_eq!(>::balance(&account_id(137)), 1); - assert_eq!( - >::balance_on_hold(&TestId::Foo, &account_id(137)), - 60 - ); - - assert_ok!(>::release( - &TestId::Foo, - &account_id(137), - 60, - Exact - )); - assert_eq!( - >::balance_on_hold(&TestId::Foo, &account_id(137)), - 0 - ); - assert_eq!( - >::total_balance_on_hold(&account_id(137)), - 0 - ); - }); -} - -#[test] -fn unbalanced_trait_set_total_issuance_works() { - ExtBuilder::default().build_and_execute_with(|| { - assert_eq!(>::total_issuance(), 0); - Balances::set_total_issuance(100); - assert_eq!(>::total_issuance(), 100); - }); -} - -#[test] -fn unbalanced_trait_decrease_balance_simple_works() { - ExtBuilder::default().build_and_execute_with(|| { - // An Account that starts at 100 - assert_ok!(Balances::write_balance(&account_id(137), 100)); - assert_eq!(>::balance(&account_id(137)), 100); - // and reserves 50 - assert_ok!(>::hold(&TestId::Foo, &account_id(137), 50)); - assert_eq!(>::balance(&account_id(137)), 50); - // and is decreased by 20 - assert_ok!(Balances::decrease_balance(&account_id(137), 20, Exact, Expendable, Polite)); - assert_eq!(>::balance(&account_id(137)), 30); - }); -} - -#[test] -fn unbalanced_trait_decrease_balance_works_1() { - ExtBuilder::default().build_and_execute_with(|| { - assert_ok!(Balances::write_balance(&account_id(137), 100)); - assert_eq!(>::balance(&account_id(137)), 100); - - assert_noop!( - Balances::decrease_balance(&account_id(137), 101, Exact, Expendable, Polite), - TokenError::FundsUnavailable - ); - assert_eq!( - Balances::decrease_balance(&account_id(137), 100, Exact, Expendable, Polite), - Ok(100) - ); - assert_eq!(>::balance(&account_id(137)), 0); - }); -} - -#[test] -fn unbalanced_trait_decrease_balance_works_2() { - ExtBuilder::default().build_and_execute_with(|| { - // free: 40, reserved: 60 - assert_ok!(Balances::write_balance(&account_id(137), 100)); - assert_ok!(Balances::hold(&TestId::Foo, &account_id(137), 60)); - assert_eq!(>::balance(&account_id(137)), 40); - assert_eq!(Balances::total_balance_on_hold(&account_id(137)), 60); - assert_noop!( - Balances::decrease_balance(&account_id(137), 40, Exact, Expendable, Polite), - TokenError::FundsUnavailable - ); - assert_eq!( - Balances::decrease_balance(&account_id(137), 39, Exact, Expendable, Polite), - Ok(39) - ); - assert_eq!(>::balance(&account_id(137)), 1); - assert_eq!(Balances::total_balance_on_hold(&account_id(137)), 60); - }); -} - -#[test] -fn unbalanced_trait_decrease_balance_at_most_works_1() { - ExtBuilder::default().build_and_execute_with(|| { - assert_ok!(Balances::write_balance(&account_id(137), 100)); - assert_eq!(>::balance(&account_id(137)), 100); - - assert_eq!( - Balances::decrease_balance(&account_id(137), 101, BestEffort, Expendable, Polite), - Ok(100) - ); - assert_eq!(>::balance(&account_id(137)), 0); - }); -} - -#[test] -fn unbalanced_trait_decrease_balance_at_most_works_2() { - ExtBuilder::default().build_and_execute_with(|| { - assert_ok!(Balances::write_balance(&account_id(137), 99)); - assert_eq!( - Balances::decrease_balance(&account_id(137), 99, BestEffort, Expendable, Polite), - Ok(99) - ); - assert_eq!(>::balance(&account_id(137)), 0); - }); -} - -#[test] -fn unbalanced_trait_decrease_balance_at_most_works_3() { - ExtBuilder::default().build_and_execute_with(|| { - // free: 40, reserved: 60 - assert_ok!(Balances::write_balance(&account_id(137), 100)); - assert_ok!(Balances::hold(&TestId::Foo, &account_id(137), 60)); - assert_eq!(Balances::free_balance(account_id(137)), 40); - assert_eq!(Balances::total_balance_on_hold(&account_id(137)), 60); - assert_eq!( - Balances::decrease_balance(&account_id(137), 0, BestEffort, Expendable, Polite), - Ok(0) - ); - assert_eq!(Balances::free_balance(account_id(137)), 40); - assert_eq!(Balances::total_balance_on_hold(&account_id(137)), 60); - assert_eq!( - Balances::decrease_balance(&account_id(137), 10, BestEffort, Expendable, Polite), - Ok(10) - ); - assert_eq!(Balances::free_balance(account_id(137)), 30); - assert_eq!( - Balances::decrease_balance(&account_id(137), 200, BestEffort, Expendable, Polite), - Ok(29) - ); - assert_eq!(>::balance(&account_id(137)), 1); - assert_eq!(Balances::free_balance(account_id(137)), 1); - assert_eq!(Balances::total_balance_on_hold(&account_id(137)), 60); - }); -} - -#[test] -fn unbalanced_trait_increase_balance_works() { - ExtBuilder::default().build_and_execute_with(|| { - assert_noop!( - Balances::increase_balance(&account_id(137), 0, Exact), - TokenError::BelowMinimum - ); - assert_eq!(Balances::increase_balance(&account_id(137), 1, Exact), Ok(1)); - assert_noop!( - Balances::increase_balance(&account_id(137), Balance::MAX, Exact), - ArithmeticError::Overflow - ); - }); -} - -#[test] -fn unbalanced_trait_increase_balance_at_most_works() { - ExtBuilder::default().build_and_execute_with(|| { - assert_eq!(Balances::increase_balance(&account_id(137), 0, BestEffort), Ok(0)); - assert_eq!(Balances::increase_balance(&account_id(137), 1, BestEffort), Ok(1)); - assert_eq!( - Balances::increase_balance(&account_id(137), Balance::MAX, BestEffort), - Ok(Balance::MAX - 1) - ); - }); -} - -#[test] -fn freezing_and_holds_should_overlap() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), 10)); - assert_ok!(Balances::hold(&TestId::Foo, &account_id(1), 9)); - assert_eq!(Balances::account(&account_id(1)).free, 1); - assert_eq!(System::consumers(&account_id(1)), 1); - assert_eq!(Balances::account(&account_id(1)).free, 1); - assert_eq!(Balances::account(&account_id(1)).frozen, 10); - assert_eq!(Balances::account(&account_id(1)).reserved, 9); - assert_eq!(Balances::total_balance_on_hold(&account_id(1)), 9); - }); -} - -#[test] -fn frozen_hold_balance_cannot_be_moved_without_force() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), 10)); - assert_ok!(Balances::hold(&TestId::Foo, &account_id(1), 9)); - assert_eq!(Balances::reducible_total_balance_on_hold(&account_id(1), Force), 9); - assert_eq!(Balances::reducible_total_balance_on_hold(&account_id(1), Polite), 0); - let e = TokenError::Frozen; - assert_noop!( - Balances::transfer_on_hold( - &TestId::Foo, - &account_id(1), - &account_id(2), - 1, - Exact, - Free, - Polite - ), - e - ); - assert_ok!(Balances::transfer_on_hold( - &TestId::Foo, - &account_id(1), - &account_id(2), - 1, - Exact, - Free, - Force - )); - }); -} - -#[test] -fn frozen_hold_balance_best_effort_transfer_works() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), 5)); - assert_ok!(Balances::hold(&TestId::Foo, &account_id(1), 9)); - assert_eq!(Balances::reducible_total_balance_on_hold(&account_id(1), Force), 9); - assert_eq!(Balances::reducible_total_balance_on_hold(&account_id(1), Polite), 5); - assert_ok!(Balances::transfer_on_hold( - &TestId::Foo, - &account_id(1), - &account_id(2), - 10, - BestEffort, - Free, - Polite - )); - assert_eq!(Balances::total_balance(&account_id(1)), 5); - assert_eq!(Balances::total_balance(&account_id(2)), 25); - }); -} - -#[test] -fn partial_freezing_should_work() { - ExtBuilder::default() - .existential_deposit(10) - .monied(true) - .build_and_execute_with(|| { - assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), 5)); - assert_eq!(System::consumers(&account_id(1)), 1); - assert_ok!(>::transfer( - &account_id(1), - &account_id(2), - 5, - Expendable - )); - // After transferring 5, balance is 95. With 10 frozen, can transfer up to 85 more - // (95-10=85) - assert_ok!(>::transfer( - &account_id(1), - &account_id(2), - 85, - Expendable - )); - // Now balance is 10. Transferring 1 more would leave 9, which is < 10 frozen, so should - // fail - assert_noop!( - >::transfer( - &account_id(1), - &account_id(2), - 1, - Expendable - ), - TokenError::Frozen - ); - }); -} - -#[test] -fn thaw_should_work() { - ExtBuilder::default() - .existential_deposit(10) - .monied(true) - .build_and_execute_with(|| { - assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), Balance::MAX)); - assert_ok!(Balances::thaw(&TestId::Foo, &account_id(1))); - assert_eq!(System::consumers(&account_id(1)), 0); - assert_eq!(Balances::balance_frozen(&TestId::Foo, &account_id(1)), 0); - assert_eq!(Balances::account(&account_id(1)).frozen, 0); - assert_ok!(>::transfer( - &account_id(1), - &account_id(2), - 10, - Expendable - )); - }); -} - -#[test] -fn set_freeze_zero_should_work() { - ExtBuilder::default() - .existential_deposit(10) - .monied(true) - .build_and_execute_with(|| { - assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), Balance::MAX)); - assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), 0)); - assert_eq!(System::consumers(&account_id(1)), 0); - assert_eq!(Balances::balance_frozen(&TestId::Foo, &account_id(1)), 0); - assert_eq!(Balances::account(&account_id(1)).frozen, 0); - assert_ok!(>::transfer( - &account_id(1), - &account_id(2), - 10, - Expendable - )); - }); -} - -#[test] -fn set_freeze_should_work() { - ExtBuilder::default() - .existential_deposit(10) - .monied(true) - .build_and_execute_with(|| { - assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), Balance::MAX)); - assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), 5)); - assert_ok!(>::transfer( - &account_id(1), - &account_id(2), - 5, - Expendable - )); - // After transferring 5, balance is 95. With 10 frozen, can transfer up to 85 more - assert_ok!(>::transfer( - &account_id(1), - &account_id(2), - 85, - Expendable - )); - // Now balance is 10. Transferring 1 more would leave 9, which is < 10 frozen, so should - // fail - assert_noop!( - >::transfer( - &account_id(1), - &account_id(2), - 1, - Expendable - ), - TokenError::Frozen - ); - }); -} - -#[test] -fn extend_freeze_should_work() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), 5)); - assert_ok!(Balances::extend_freeze(&TestId::Foo, &account_id(1), 10)); - assert_eq!(Balances::account(&account_id(1)).frozen, 10); - assert_eq!(Balances::balance_frozen(&TestId::Foo, &account_id(1)), 10); - assert_noop!( - >::transfer( - &account_id(1), - &account_id(2), - 1, - Expendable - ), - TokenError::Frozen - ); - }); -} - -#[test] -fn debug_freeze_behavior() { - ExtBuilder::default() - .existential_deposit(10) - .monied(true) - .build_and_execute_with(|| { - println!("=== Debug freeze behavior ==="); - println!("Initial balance: {}", Balances::free_balance(account_id(1))); - - // Set a freeze - assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), 10)); - println!("After freeze(10):"); - println!(" Free balance: {}", Balances::free_balance(account_id(1))); - println!(" Frozen amount: {}", Balances::balance_frozen(&TestId::Foo, &account_id(1))); - println!(" Account frozen: {}", Balances::account(&account_id(1)).frozen); - - // Try transfers of different amounts - for amount in [1, 5, 10, 89, 90, 91] { - let balance_before = Balances::free_balance(account_id(1)); - let result = >::transfer( - &account_id(1), - &account_id(2), - amount, - Expendable, - ); - let balance_after = Balances::free_balance(account_id(1)); - - println!( - "Transfer {} units: {:?} (balance: {} -> {})", - amount, result, balance_before, balance_after - ); - - // Restore balance for next test - if result.is_ok() { - let _ = >::transfer( - &account_id(2), - &account_id(1), - amount, - Expendable, - ); - } - } - }); -} - -#[test] -fn double_freezing_should_work() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), 5)); - assert_ok!(Balances::set_freeze(&TestId::Bar, &account_id(1), 5)); - assert_eq!(System::consumers(&account_id(1)), 1); - assert_ok!(>::transfer( - &account_id(1), - &account_id(2), - 5, - Expendable - )); - assert_noop!( - >::transfer( - &account_id(1), - &account_id(2), - 1, - Expendable - ), - TokenError::Frozen - ); - }); -} - -#[test] -fn can_hold_entire_balance_when_second_provider() { - ExtBuilder::default() - .existential_deposit(1) - .monied(false) - .build_and_execute_with(|| { - >::set_balance(&account_id(1), 100); - assert_noop!( - Balances::hold(&TestId::Foo, &account_id(1), 100), - TokenError::FundsUnavailable - ); - System::inc_providers(&account_id(1)); - assert_eq!(System::providers(&account_id(1)), 2); - assert_ok!(Balances::hold(&TestId::Foo, &account_id(1), 100)); - assert_eq!(System::providers(&account_id(1)), 1); - assert_noop!(System::dec_providers(&account_id(1)), DispatchError::ConsumerRemaining); - }); -} - -#[test] -fn unholding_frees_hold_slot() { - ExtBuilder::default() - .existential_deposit(1) - .monied(false) - .build_and_execute_with(|| { - >::set_balance(&account_id(1), 100); - assert_ok!(Balances::hold(&TestId::Foo, &account_id(1), 10)); - assert_ok!(Balances::hold(&TestId::Bar, &account_id(1), 10)); - assert_ok!(Balances::release(&TestId::Foo, &account_id(1), 10, Exact)); - assert_ok!(Balances::hold(&TestId::Baz, &account_id(1), 10)); - }); -} - -#[test] -fn sufficients_work_properly_with_reference_counting() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - // Only run PoC when the system pallet is enabled, since the underlying bug is in the - // system pallet it won't work with BalancesAccountStore - if UseSystem::get() { - // Start with a balance of 100 - >::set_balance(&account_id(1), 100); - // Emulate a sufficient, in reality this could be reached by transferring a - // sufficient asset to the account - System::inc_sufficients(&account_id(1)); - // Spend the same balance multiple times - assert_ok!(>::transfer( - &account_id(1), - &account_id(137), - 100, - Expendable - )); - assert_eq!(Balances::free_balance(account_id(1)), 0); - assert_noop!( - >::transfer( - &account_id(1), - &account_id(137), - 100, - Expendable - ), - TokenError::FundsUnavailable - ); - } - }); -} - -#[test] -fn emit_events_with_changing_freezes() { - ExtBuilder::default().build_and_execute_with(|| { - let _ = Balances::set_balance(&account_id(1), 100); - System::reset_events(); - - // Freeze = [] --> [10] - assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), 10)); - assert_eq!( - events(), - [RuntimeEvent::Balances(crate::Event::Frozen { who: account_id(1), amount: 10 })] - ); - - // Freeze = [10] --> [15] - assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), 15)); - assert_eq!( - events(), - [RuntimeEvent::Balances(crate::Event::Frozen { who: account_id(1), amount: 5 })] - ); - - // Freeze = [15] --> [15, 20] - assert_ok!(Balances::set_freeze(&TestId::Bar, &account_id(1), 20)); - assert_eq!( - events(), - [RuntimeEvent::Balances(crate::Event::Frozen { who: account_id(1), amount: 5 })] - ); - - // Freeze = [15, 20] --> [17, 20] - assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), 17)); - for event in events() { - match event { - RuntimeEvent::Balances(crate::Event::Frozen { .. }) => { - assert!(false, "unexpected freeze event") - }, - RuntimeEvent::Balances(crate::Event::Thawed { .. }) => { - assert!(false, "unexpected thaw event") - }, - _ => continue, - } - } - - // Freeze = [17, 20] --> [17, 15] - assert_ok!(Balances::set_freeze(&TestId::Bar, &account_id(1), 15)); - assert_eq!( - events(), - [RuntimeEvent::Balances(crate::Event::Thawed { who: account_id(1), amount: 3 })] - ); - - // Freeze = [17, 15] --> [15] - assert_ok!(Balances::thaw(&TestId::Foo, &account_id(1))); - assert_eq!( - events(), - [RuntimeEvent::Balances(crate::Event::Thawed { who: account_id(1), amount: 2 })] - ); - - // Freeze = [15] --> [] - assert_ok!(Balances::thaw(&TestId::Bar, &account_id(1))); - assert_eq!( - events(), - [RuntimeEvent::Balances(crate::Event::Thawed { who: account_id(1), amount: 15 })] - ); - }); -} - -#[test] -fn withdraw_precision_exact_works() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), 10)); - assert_eq!(Balances::account(&account_id(1)).free, 10); - assert_eq!(Balances::account(&account_id(1)).frozen, 10); - - // `BestEffort` will not reduce anything - assert_ok!(>::withdraw( - &account_id(1), - 5, - BestEffort, - Preserve, - Polite - )); - - assert_eq!(Balances::account(&account_id(1)).free, 10); - assert_eq!(Balances::account(&account_id(1)).frozen, 10); - - assert_noop!( - >::withdraw( - &account_id(1), - 5, - Exact, - Preserve, - Polite - ), - TokenError::FundsUnavailable - ); - }); -} - -#[test] -fn freeze_consideration_works() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - type Consideration = FreezeConsideration< - AccountId, - Balances, - FooReason, - LinearStoragePrice, ConstU128<1>, Balance>, - Footprint, - >; - - let who = account_id(4); - // freeze amount taken somewhere outside of our (Consideration) scope. - let extend_freeze = 15; - - let ticket = Consideration::new(&who, Footprint::from_parts(0, 0)).unwrap(); - assert!(ticket.is_none()); - assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 0); - - let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap(); - assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 10); - - let ticket = ticket.update(&who, Footprint::from_parts(4, 1)).unwrap(); - assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 4); - - assert_ok!(Balances::increase_frozen(&TestId::Foo, &who, extend_freeze)); - assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 4 + extend_freeze); - - let ticket = ticket.update(&who, Footprint::from_parts(8, 1)).unwrap(); - assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 8 + extend_freeze); - - let ticket = ticket.update(&who, Footprint::from_parts(0, 0)).unwrap(); - assert!(ticket.is_none()); - assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), extend_freeze); - - let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap(); - assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 10 + extend_freeze); - - ticket.drop(&who).unwrap(); - assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), extend_freeze); - }); -} - -#[test] -fn hold_consideration_works() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - type Consideration = HoldConsideration< - AccountId, - Balances, - FooReason, - LinearStoragePrice, ConstU128<1>, Balance>, - Footprint, - >; - - let who = account_id(4); - // hold amount taken somewhere outside of our (Consideration) scope. - let extend_hold = 15; - - let ticket = Consideration::new(&who, Footprint::from_parts(0, 0)).unwrap(); - assert!(ticket.is_none()); - assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 0); - - let ticket = ticket.update(&who, Footprint::from_parts(10, 1)).unwrap(); - assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 10); - - let ticket = ticket.update(&who, Footprint::from_parts(4, 1)).unwrap(); - assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 4); - - assert_ok!(Balances::hold(&TestId::Foo, &who, extend_hold)); - assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 4 + extend_hold); - - let ticket = ticket.update(&who, Footprint::from_parts(8, 1)).unwrap(); - assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 8 + extend_hold); - - let ticket = ticket.update(&who, Footprint::from_parts(0, 0)).unwrap(); - assert!(ticket.is_none()); - assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), extend_hold); - - let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap(); - assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 10 + extend_hold); - - ticket.drop(&who).unwrap(); - assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), extend_hold); - }); -} - -#[test] -fn lone_freeze_consideration_works() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - type Consideration = LoneFreezeConsideration< - AccountId, - Balances, - FooReason, - LinearStoragePrice, ConstU128<1>, Balance>, - Footprint, - >; - - let who = account_id(4); - let zero_ticket = Consideration::new(&who, Footprint::from_parts(0, 0)).unwrap(); - assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 0); - - let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap(); - assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 10); - - assert_ok!(Balances::increase_frozen(&TestId::Foo, &who, 5)); - assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 15); - - let ticket = ticket.update(&who, Footprint::from_parts(4, 1)).unwrap(); - assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 4); - - assert_eq!(ticket.update(&who, Footprint::from_parts(0, 0)).unwrap(), zero_ticket); - assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 0); - - let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap(); - assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 10); - - ticket.drop(&who).unwrap(); - assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 0); - }); -} - -#[test] -fn lone_hold_consideration_works() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - type Consideration = LoneHoldConsideration< - AccountId, - Balances, - FooReason, - LinearStoragePrice, ConstU128<1>, Balance>, - Footprint, - >; - - let who = account_id(4); - let zero_ticket = Consideration::new(&who, Footprint::from_parts(0, 0)).unwrap(); - assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 0); - - let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap(); - assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 10); - - assert_ok!(Balances::hold(&TestId::Foo, &who, 5)); - assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 15); - - let ticket = ticket.update(&who, Footprint::from_parts(4, 1)).unwrap(); - assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 4); - - assert_eq!(ticket.update(&who, Footprint::from_parts(0, 0)).unwrap(), zero_ticket); - assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 0); - - let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap(); - assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 10); - - ticket.drop(&who).unwrap(); - assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 0); - }); -} diff --git a/pallets/balances/src/tests/general_tests.rs b/pallets/balances/src/tests/general_tests.rs deleted file mode 100644 index a56f9f98..00000000 --- a/pallets/balances/src/tests/general_tests.rs +++ /dev/null @@ -1,143 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#![cfg(test)] - -use crate::{ - system::AccountInfo, - tests::{account_id, ensure_ti_valid, Balances, ExtBuilder, System, Test, TestId, UseSystem}, - AccountData, ExtraFlags, TotalIssuance, -}; -use frame_support::{ - assert_noop, assert_ok, hypothetically, - traits::{ - fungible::{Mutate, MutateHold}, - tokens::Precision, - }, -}; -use sp_runtime::DispatchError; - -/// There are some accounts that have one consumer ref too few. These accounts are at risk of losing -/// their held (reserved) balance. They do not just lose it - it is also not accounted for in the -/// Total Issuance. Here we test the case that the account does not reap in such a case, but gets -/// one consumer ref for its reserved balance. -#[test] -fn regression_historic_acc_does_not_evaporate_reserve() { - ExtBuilder::default().build_and_execute_with(|| { - UseSystem::set(true); - let (alice, bob) = (account_id(0), account_id(1)); - // Alice is in a bad state with consumer == 0 && reserved > 0: - Balances::set_balance(&alice, 100); - TotalIssuance::::put(100); - ensure_ti_valid(); - - assert_ok!(Balances::hold(&TestId::Foo, &alice, 10)); - // This is the issue of the account: - System::dec_consumers(&alice); - - assert_eq!( - System::account(&alice), - AccountInfo { - data: AccountData { - free: 90, - reserved: 10, - frozen: 0, - flags: ExtraFlags(1u128 << 127), - }, - nonce: 0, - consumers: 0, // should be 1 on a good acc - providers: 1, - sufficients: 0, - } - ); - - ensure_ti_valid(); - - // Reaping the account is prevented by the new logic: - assert_noop!( - Balances::transfer_allow_death(Some(alice.clone()).into(), bob.clone(), 90), - DispatchError::ConsumerRemaining - ); - assert_noop!( - Balances::transfer_all(Some(alice.clone()).into(), bob.clone(), false), - DispatchError::ConsumerRemaining - ); - - // normal transfers still work: - hypothetically!({ - assert_ok!(Balances::transfer_keep_alive(Some(alice.clone()).into(), bob.clone(), 40)); - // Alice got back her consumer ref: - assert_eq!(System::consumers(&alice), 1); - ensure_ti_valid(); - }); - hypothetically!({ - assert_ok!(Balances::transfer_all(Some(alice.clone()).into(), bob.clone(), true)); - // Alice got back her consumer ref: - assert_eq!(System::consumers(&alice), 1); - ensure_ti_valid(); - }); - - // un-reserving all does not add a consumer ref: - hypothetically!({ - assert_ok!(Balances::release(&TestId::Foo, &alice, 10, Precision::Exact)); - assert_eq!(System::consumers(&alice), 0); - assert_ok!(Balances::transfer_keep_alive(Some(alice.clone()).into(), bob.clone(), 40)); - assert_eq!(System::consumers(&alice), 0); - ensure_ti_valid(); - }); - // un-reserving some does add a consumer ref: - hypothetically!({ - assert_ok!(Balances::release(&TestId::Foo, &alice, 5, Precision::Exact)); - assert_eq!(System::consumers(&alice), 1); - assert_ok!(Balances::transfer_keep_alive(Some(alice.clone()).into(), bob.clone(), 40)); - assert_eq!(System::consumers(&alice), 1); - ensure_ti_valid(); - }); - }); -} - -#[cfg(feature = "try-runtime")] -#[test] -fn try_state_works() { - use crate::{Config, Freezes, Holds}; - use frame_support::{ - storage, - traits::{Get, Hooks, VariantCount}, - }; - - ExtBuilder::default().build_and_execute_with(|| { - storage::unhashed::put( - &Holds::::hashed_key_for(account_id(1)), - &vec![0u8; ::RuntimeHoldReason::VARIANT_COUNT as usize + 1], - ); - - assert!(format!("{:?}", Balances::try_state(0).unwrap_err()) - .contains("Found `Hold` with too many elements")); - }); - - ExtBuilder::default().build_and_execute_with(|| { - let max_freezes: u32 = ::MaxFreezes::get(); - - storage::unhashed::put( - &Freezes::::hashed_key_for(account_id(1)), - &vec![0u8; max_freezes as usize + 1], - ); - - assert!(format!("{:?}", Balances::try_state(0).unwrap_err()) - .contains("Found `Freeze` with too many elements")); - }); -} diff --git a/pallets/balances/src/tests/mod.rs b/pallets/balances/src/tests/mod.rs deleted file mode 100644 index cc469066..00000000 --- a/pallets/balances/src/tests/mod.rs +++ /dev/null @@ -1,330 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Tests. - -#![cfg(test)] - -use crate::{self as pallet_balances, AccountData, Config, CreditOf, Error, Pallet, TotalIssuance}; -use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; -use frame_support::{ - assert_err, assert_noop, assert_ok, assert_storage_noop, derive_impl, - dispatch::{DispatchInfo, GetDispatchInfo}, - parameter_types, - traits::{ - fungible, ConstU32, ConstU8, Imbalance as ImbalanceT, OnUnbalanced, StorageMapShim, - StoredMap, VariantCount, VariantCountOf, WhitelistedStorageKeys, - }, - weights::{IdentityFee, Weight}, -}; -use frame_system::{self as system, RawOrigin}; -use pallet_transaction_payment::{ChargeTransactionPayment, FungibleAdapter, Multiplier}; -use scale_info::TypeInfo; -use sp_core::hexdisplay::HexDisplay; -use sp_runtime::{ - traits::{BadOrigin, Zero}, - ArithmeticError, BuildStorage, DispatchError, DispatchResult, FixedPointNumber, RuntimeDebug, - TokenError, -}; -use std::collections::BTreeSet; - -mod currency_tests; -mod dispatchable_tests; -// mod fungible_conformance_tests; // Commented out due to AccountId32 incompatibility -mod fungible_tests; -mod general_tests; -mod reentrancy_tests; -mod transfer_counter_tests; -type Block = frame_system::mocking::MockBlock; -type AccountId = sp_core::crypto::AccountId32; - -#[derive( - Encode, - Decode, - Copy, - Clone, - Eq, - PartialEq, - Ord, - PartialOrd, - MaxEncodedLen, - TypeInfo, - DecodeWithMemTracking, - RuntimeDebug, -)] -pub enum TestId { - Foo, - Bar, - Baz, -} - -impl VariantCount for TestId { - const VARIANT_COUNT: u32 = 3; -} - -frame_support::construct_runtime!( - pub enum Test { - System: frame_system, - Balances: pallet_balances, - TransactionPayment: pallet_transaction_payment, - } -); - -type Balance = u128; - -parameter_types! { - pub BlockWeights: frame_system::limits::BlockWeights = - frame_system::limits::BlockWeights::simple_max( - frame_support::weights::Weight::from_parts(1024, u64::MAX), - ); - pub static ExistentialDeposit: Balance = 1; -} - -#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] -impl frame_system::Config for Test { - type Block = Block; - type AccountId = AccountId; - type Lookup = sp_runtime::traits::IdentityLookup; - type AccountData = super::AccountData; -} - -#[derive_impl(pallet_transaction_payment::config_preludes::TestDefaultConfig)] -impl pallet_transaction_payment::Config for Test { - type OnChargeTransaction = FungibleAdapter, ()>; - type OperationalFeeMultiplier = ConstU8<5>; - type WeightToFee = IdentityFee; - type LengthToFee = IdentityFee; -} - -parameter_types! { - pub FooReason: TestId = TestId::Foo; -} - -#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] -impl Config for Test { - type Balance = Balance; - type DustRemoval = DustTrap; - type ExistentialDeposit = ExistentialDeposit; - type AccountStore = TestAccountStore; - type MaxReserves = ConstU32<2>; - type ReserveIdentifier = TestId; - type RuntimeHoldReason = TestId; - type RuntimeFreezeReason = TestId; - type FreezeIdentifier = TestId; - type MaxFreezes = VariantCountOf; -} - -#[derive(Clone)] -pub struct ExtBuilder { - existential_deposit: Balance, - monied: bool, - dust_trap: Option, -} -impl Default for ExtBuilder { - fn default() -> Self { - Self { existential_deposit: 1, monied: false, dust_trap: None } - } -} -impl ExtBuilder { - pub fn existential_deposit(mut self, existential_deposit: Balance) -> Self { - self.existential_deposit = existential_deposit; - self - } - pub fn monied(mut self, monied: bool) -> Self { - self.monied = monied; - if self.existential_deposit == 0 { - self.existential_deposit = 1; - } - self - } - pub fn dust_trap(mut self, account: u64) -> Self { - self.dust_trap = Some(account); - self - } - pub fn set_associated_consts(&self) { - DUST_TRAP_TARGET.with(|v| v.replace(self.dust_trap)); - EXISTENTIAL_DEPOSIT.with(|v| v.replace(self.existential_deposit)); - } - pub fn build(self) -> sp_io::TestExternalities { - self.set_associated_consts(); - let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); - pallet_balances::GenesisConfig:: { - balances: if self.monied { - vec![ - (account_id(1), 10 * self.existential_deposit), - (account_id(2), 20 * self.existential_deposit), - (account_id(3), 30 * self.existential_deposit), - (account_id(4), 40 * self.existential_deposit), - (account_id(12), 10 * self.existential_deposit), - ] - } else { - vec![] - }, - } - .assimilate_storage(&mut t) - .unwrap(); - - let mut ext = sp_io::TestExternalities::new(t); - ext.execute_with(|| System::set_block_number(1)); - ext - } - pub fn build_and_execute_with(self, f: impl Fn()) { - let other = self.clone(); - UseSystem::set(false); - other.build().execute_with(&f); - UseSystem::set(true); - self.build().execute_with(f); - } -} - -parameter_types! { - static DustTrapTarget: Option = None; -} - -pub struct DustTrap; - -impl OnUnbalanced> for DustTrap { - fn on_nonzero_unbalanced(amount: CreditOf) { - match DustTrapTarget::get() { - None => drop(amount), - Some(a) => { - let account = account_id(a as u8); - let result = >::resolve(&account, amount); - debug_assert!(result.is_ok()); - }, - } - } -} - -parameter_types! { - pub static UseSystem: bool = false; -} - -type BalancesAccountStore = - StorageMapShim, AccountId, super::AccountData>; -type SystemAccountStore = frame_system::Pallet; - -pub struct TestAccountStore; -impl StoredMap> for TestAccountStore { - fn get(k: &AccountId) -> super::AccountData { - if UseSystem::get() { - >::get(k) - } else { - >::get(k) - } - } - fn try_mutate_exists>( - k: &AccountId, - f: impl FnOnce(&mut Option>) -> Result, - ) -> Result { - if UseSystem::get() { - >::try_mutate_exists(k, f) - } else { - >::try_mutate_exists(k, f) - } - } - fn mutate( - k: &AccountId, - f: impl FnOnce(&mut super::AccountData) -> R, - ) -> Result { - if UseSystem::get() { - >::mutate(k, f) - } else { - >::mutate(k, f) - } - } - fn mutate_exists( - k: &AccountId, - f: impl FnOnce(&mut Option>) -> R, - ) -> Result { - if UseSystem::get() { - >::mutate_exists(k, f) - } else { - >::mutate_exists(k, f) - } - } - fn insert(k: &AccountId, t: super::AccountData) -> Result<(), DispatchError> { - if UseSystem::get() { - >::insert(k, t) - } else { - >::insert(k, t) - } - } - fn remove(k: &AccountId) -> Result<(), DispatchError> { - if UseSystem::get() { - >::remove(k) - } else { - >::remove(k) - } - } -} - -pub fn events() -> Vec { - let evt = System::events().into_iter().map(|evt| evt.event).collect::>(); - System::reset_events(); - evt -} - -/// create a transaction info struct from weight. Handy to avoid building the whole struct. -pub fn info_from_weight(w: Weight) -> DispatchInfo { - DispatchInfo { call_weight: w, ..Default::default() } -} - -/// Helper function to convert a u8 to an AccountId32 -pub fn account_id(id: u8) -> AccountId { - sp_core::crypto::AccountId32::from([id; 32]) -} - -/// Check that the total-issuance matches the sum of all accounts' total balances. -pub fn ensure_ti_valid() { - let mut sum = 0; - - for acc in frame_system::Account::::iter_keys() { - if UseSystem::get() { - let data = frame_system::Pallet::::account(acc); - sum += data.data.total(); - } else { - let data = crate::Account::::get(acc); - sum += data.total(); - } - } - - assert_eq!(TotalIssuance::::get(), sum, "Total Issuance wrong"); -} - -#[test] -fn weights_sane() { - let info = crate::Call::::transfer_allow_death { dest: account_id(10), value: 4 } - .get_dispatch_info(); - assert_eq!(<() as crate::WeightInfo>::transfer_allow_death(), info.call_weight); - - let info = - crate::Call::::force_unreserve { who: account_id(10), amount: 4 }.get_dispatch_info(); - assert_eq!(<() as crate::WeightInfo>::force_unreserve(), info.call_weight); -} - -#[test] -fn check_whitelist() { - let whitelist: BTreeSet = AllPalletsWithSystem::whitelisted_storage_keys() - .iter() - .map(|s| HexDisplay::from(&s.key).to_string()) - .collect(); - // Inactive Issuance - assert!(whitelist.contains("c2261276cc9d1f8598ea4b6a74b15c2f1ccde6872881f893a21de93dfe970cd5")); - // Total Issuance - assert!(whitelist.contains("c2261276cc9d1f8598ea4b6a74b15c2f57c875e4cff74148e4628f264b974c80")); -} diff --git a/pallets/balances/src/tests/reentrancy_tests.rs b/pallets/balances/src/tests/reentrancy_tests.rs deleted file mode 100644 index 767f3ddf..00000000 --- a/pallets/balances/src/tests/reentrancy_tests.rs +++ /dev/null @@ -1,212 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Tests regarding the reentrancy functionality. - -use super::*; -use frame_support::traits::tokens::{ - Fortitude::Force, - Precision::BestEffort, - Preservation::{Expendable, Protect}, -}; -use fungible::Balanced; - -#[test] -fn transfer_dust_removal_tst1_should_work() { - ExtBuilder::default() - .existential_deposit(100) - .dust_trap(1) - .build_and_execute_with(|| { - // Verification of reentrancy in dust removal - assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), account_id(1), 1000)); - assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), account_id(2), 500)); - - // In this transaction, account 2 free balance - // drops below existential balance - // and dust balance is removed from account 2 - assert_ok!(Balances::transfer_allow_death( - RawOrigin::Signed(account_id(2)).into(), - account_id(3), - 450 - )); - - // As expected dust balance is removed. - assert_eq!(Balances::free_balance(account_id(2)), 0); - - // As expected beneficiary account 3 - // received the transferred fund. - assert_eq!(Balances::free_balance(account_id(3)), 450); - - // Dust balance is deposited to account 1 - // during the process of dust removal. - assert_eq!(Balances::free_balance(account_id(1)), 1050); - - // Verify the events - assert_eq!(System::events().len(), 15); - - System::assert_has_event(RuntimeEvent::Balances(crate::Event::Transfer { - from: account_id(2), - to: account_id(3), - amount: 450, - })); - System::assert_has_event(RuntimeEvent::Balances(crate::Event::DustLost { - account: account_id(2), - amount: 50, - })); - System::assert_has_event(RuntimeEvent::Balances(crate::Event::Deposit { - who: account_id(1), - amount: 50, - })); - }); -} - -#[test] -fn transfer_dust_removal_tst2_should_work() { - ExtBuilder::default() - .existential_deposit(100) - .dust_trap(1) - .build_and_execute_with(|| { - // Verification of reentrancy in dust removal - assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), account_id(1), 1000)); - assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), account_id(2), 500)); - - // In this transaction, account 2 free balance - // drops below existential balance - // and dust balance is removed from account 2 - assert_ok!(Balances::transfer_allow_death( - RawOrigin::Signed(account_id(2)).into(), - account_id(1), - 450 - )); - - // As expected dust balance is removed. - assert_eq!(Balances::free_balance(account_id(2)), 0); - - // Dust balance is deposited to account 1 - assert_eq!(Balances::free_balance(account_id(1)), 1000 + 450 + 50); - // Verify the events - assert_eq!(System::events().len(), 13); - - System::assert_has_event(RuntimeEvent::Balances(crate::Event::Transfer { - from: account_id(2), - to: account_id(1), - amount: 450, - })); - System::assert_has_event(RuntimeEvent::Balances(crate::Event::DustLost { - account: account_id(2), - amount: 50, - })); - System::assert_has_event(RuntimeEvent::Balances(crate::Event::Deposit { - who: account_id(1), - amount: 50, - })); - }); -} - -#[test] -fn repatriating_reserved_balance_dust_removal_should_work() { - ExtBuilder::default() - .existential_deposit(100) - .dust_trap(1) - .build_and_execute_with(|| { - // Verification of reentrancy in dust removal - assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), account_id(1), 1000)); - assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), account_id(2), 500)); - - // Reserve a value on account 2, - // Such that free balance is lower than - // Existential deposit. - assert_ok!(Balances::transfer_allow_death( - RuntimeOrigin::signed(account_id(2)), - account_id(1), - 450 - )); - - // Since free balance of account 2 is lower than - // existential deposit, dust amount is - // removed from the account 2 - assert_eq!(Balances::reserved_balance(account_id(2)), 0); - assert_eq!(Balances::free_balance(account_id(2)), 0); - - // account 1 is credited with reserved amount - // together with dust balance during dust - // removal. - assert_eq!(Balances::reserved_balance(account_id(1)), 0); - assert_eq!(Balances::free_balance(account_id(1)), 1500); - - // Verify the events - assert_eq!(System::events().len(), 13); - - System::assert_has_event(RuntimeEvent::Balances(crate::Event::Transfer { - from: account_id(2), - to: account_id(1), - amount: 450, - })); - System::assert_has_event(RuntimeEvent::Balances(crate::Event::DustLost { - account: account_id(2), - amount: 50, - })); - System::assert_has_event(RuntimeEvent::Balances(crate::Event::Deposit { - who: account_id(1), - amount: 50, - })); - }); -} - -#[test] -fn emit_events_with_no_existential_deposit_suicide_with_dust() { - ExtBuilder::default().existential_deposit(2).build_and_execute_with(|| { - assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), account_id(1), 100)); - - assert_eq!( - events(), - [ - RuntimeEvent::System(system::Event::NewAccount { account: account_id(1) }), - RuntimeEvent::Balances(crate::Event::Endowed { - account: account_id(1), - free_balance: 100 - }), - RuntimeEvent::Balances(crate::Event::Issued { amount: 100 }), - RuntimeEvent::Balances(crate::Event::BalanceSet { who: account_id(1), free: 100 }), - ] - ); - - let res = Balances::withdraw(&account_id(1), 98, BestEffort, Protect, Force); - assert_eq!(res.unwrap().peek(), 98); - - // no events - assert_eq!( - events(), - [RuntimeEvent::Balances(crate::Event::Withdraw { who: account_id(1), amount: 98 })] - ); - - let res = Balances::withdraw(&account_id(1), 1, BestEffort, Expendable, Force); - assert_eq!(res.unwrap().peek(), 1); - - assert_eq!( - events(), - [ - RuntimeEvent::System(system::Event::KilledAccount { account: account_id(1) }), - RuntimeEvent::Balances(crate::Event::DustLost { - account: account_id(1), - amount: 1 - }), - RuntimeEvent::Balances(crate::Event::Withdraw { who: account_id(1), amount: 1 }), - ] - ); - }); -} diff --git a/pallets/balances/src/tests/transfer_counter_tests.rs b/pallets/balances/src/tests/transfer_counter_tests.rs deleted file mode 100644 index 92cb360e..00000000 --- a/pallets/balances/src/tests/transfer_counter_tests.rs +++ /dev/null @@ -1,336 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Tests for the global transfer counter functionality. - -use super::*; -use crate::{TransferCount, TransferProof}; -use sp_runtime::{ArithmeticError::Underflow, DispatchError::Arithmetic}; - -/// Alice account ID for more readable tests. -fn alice() -> AccountId { - account_id(1) -} -/// Bob account ID for more readable tests. -fn bob() -> AccountId { - account_id(2) -} -/// Charlie account ID for more readable tests. -fn charlie() -> AccountId { - account_id(3) -} - -#[test] -fn transfer_counter_starts_at_zero() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - // Transfer counter should start at 0 - assert_eq!(Balances::transfer_count(), 0); - }); -} - -#[test] -fn transfer_allow_death_increments_counter() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - // Initial counter should be 0 - assert_eq!(Balances::transfer_count(), 0); - - // Perform a transfer - assert_ok!(Balances::transfer_allow_death(Some(alice()).into(), bob(), 5)); - - // Counter should increment to 1 - assert_eq!(Balances::transfer_count(), 1); - - // Perform another transfer - assert_ok!(Balances::transfer_allow_death(Some(bob()).into(), charlie(), 3)); - - // Counter should increment to 2 - assert_eq!(Balances::transfer_count(), 2); - }); -} - -#[test] -fn transfer_keep_alive_increments_counter() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - // Initial counter should be 0 - assert_eq!(Balances::transfer_count(), 0); - - // Perform a transfer_keep_alive - assert_ok!(Balances::transfer_keep_alive(Some(alice()).into(), bob(), 5)); - - // Counter should increment to 1 - assert_eq!(Balances::transfer_count(), 1); - }); -} - -#[test] -fn force_transfer_increments_counter() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - // Initial counter should be 0 - assert_eq!(Balances::transfer_count(), 0); - - // Perform a force transfer - assert_ok!(Balances::force_transfer(RuntimeOrigin::root(), alice(), bob(), 5)); - - // Counter should increment to 1 - assert_eq!(Balances::transfer_count(), 1); - }); -} - -#[test] -fn transfer_all_increments_counter() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - // Initial counter should be 0 - assert_eq!(Balances::transfer_count(), 0); - - // Perform a transfer_all - assert_ok!(Balances::transfer_all(Some(alice()).into(), bob(), false)); - - // Counter should increment to 1 - assert_eq!(Balances::transfer_count(), 1); - }); -} - -#[test] -fn self_transfer_does_not_increment_counter() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - // Initial counter should be 0 - assert_eq!(Balances::transfer_count(), 0); - - // Self transfer should not increment counter - assert_ok!(Balances::transfer_allow_death(Some(alice()).into(), alice(), 5)); - - // Counter should remain 0 since it's a self-transfer - assert_eq!(Balances::transfer_count(), 0); - }); -} - -#[test] -fn transfer_proof_storage_is_created() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - // Perform a transfer - assert_ok!(Balances::transfer_allow_death(Some(alice()).into(), bob(), 5)); - - // Check that transfer proof was stored with correct key - let key = (0u64, alice(), bob(), 5); - assert!(TransferProof::::contains_key(&key)); - }); -} - -#[test] -fn multiple_transfers_create_sequential_proofs() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - // First transfer - assert_ok!(Balances::transfer_allow_death(Some(alice()).into(), bob(), 5)); - assert_eq!(Balances::transfer_count(), 1); - - // Check first proof exists - let key1 = (0u64, alice(), bob(), 5u128); - assert!(TransferProof::::contains_key(&key1)); - - // Second transfer - assert_ok!(Balances::transfer_allow_death(Some(bob()).into(), charlie(), 3)); - assert_eq!(Balances::transfer_count(), 2); - - // Check second proof exists - let key2 = (1u64, bob(), charlie(), 3u128); - assert!(TransferProof::::contains_key(&key2)); - - // Third transfer with different amount - assert_ok!(Balances::transfer_allow_death(Some(alice()).into(), charlie(), 1)); - assert_eq!(Balances::transfer_count(), 3); - - // Check third proof exists - let key3 = (2u64, alice(), charlie(), 1u128); - assert!(TransferProof::::contains_key(&key3)); - }); -} - -#[test] -fn failed_transfers_do_not_increment_counter() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - // Initial counter should be 0 - assert_eq!(Balances::transfer_count(), 0); - - // Attempt transfer with insufficient funds - assert_noop!( - Balances::transfer_allow_death(Some(alice()).into(), bob(), 1000), - Arithmetic(Underflow) - ); - - // Counter should remain 0 since transfer failed - assert_eq!(Balances::transfer_count(), 0); - }); -} - -#[test] -fn transfer_proof_storage_key_generation() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - let transfer_count = 5u64; - let from = alice(); - let to = bob(); - let amount = 100u128; - - // Generate storage key - let key = Balances::transfer_proof_storage_key( - transfer_count, - from.clone(), - to.clone(), - amount, - ); - - // Key should not be empty - assert!(!key.is_empty()); - - // The same parameters should generate the same key - let key2 = Balances::transfer_proof_storage_key( - transfer_count, - from.clone(), - to.clone(), - amount, - ); - assert_eq!(key, key2); - - // Different parameters should generate different keys - let key3 = Balances::transfer_proof_storage_key(transfer_count + 1, from, to, amount); - assert_ne!(key, key3); - }); -} - -#[test] -fn counter_saturates_at_max_value() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - // Set counter to near maximum value (u64::MAX - 1) - let near_max = u64::MAX - 1; - TransferCount::::put(near_max); - - assert_eq!(Balances::transfer_count(), near_max); - - // Perform a transfer - should increment to u64::MAX - assert_ok!(Balances::transfer_allow_death(Some(alice()).into(), bob(), 5)); - assert_eq!(Balances::transfer_count(), u64::MAX); - - // Perform another transfer - should saturate at u64::MAX - assert_ok!(Balances::transfer_allow_death(Some(alice()).into(), charlie(), 3)); - assert_eq!(Balances::transfer_count(), u64::MAX); - }); -} - -#[test] -fn transfer_counter_persists_across_blocks() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - // Perform transfer in block 1 - assert_ok!(Balances::transfer_allow_death(Some(alice()).into(), bob(), 5)); - assert_eq!(Balances::transfer_count(), 1); - - // Move to block 2 - System::set_block_number(2); - - // Counter should persist - assert_eq!(Balances::transfer_count(), 1); - - // Perform another transfer in block 2 - assert_ok!(Balances::transfer_allow_death(Some(bob()).into(), charlie(), 3)); - assert_eq!(Balances::transfer_count(), 2); - }); -} - -#[test] -fn zero_value_transfers_increment_counter() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - // Initial counter should be 0 - assert_eq!(Balances::transfer_count(), 0); - - // Perform a zero-value transfer - assert_ok!(Balances::transfer_allow_death(Some(alice()).into(), bob(), 0)); - - // Counter should increment even for zero-value transfers - assert_eq!(Balances::transfer_count(), 1); - - // Transfer proof should be created - let key = (0u64, alice(), bob(), 0u128); - assert!(TransferProof::::contains_key(&key)); - }); -} - -#[test] -fn different_transfer_types_all_increment_counter() { - ExtBuilder::default() - .existential_deposit(1) - .monied(true) - .build_and_execute_with(|| { - // Initial counter should be 0 - assert_eq!(Balances::transfer_count(), 0); - - // transfer_allow_death - assert_ok!(Balances::transfer_allow_death(Some(alice()).into(), bob(), 1)); - assert_eq!(Balances::transfer_count(), 1); - - // transfer_keep_alive - assert_ok!(Balances::transfer_keep_alive(Some(alice()).into(), charlie(), 1)); - assert_eq!(Balances::transfer_count(), 2); - - // force_transfer - assert_ok!(Balances::force_transfer(RuntimeOrigin::root(), bob(), charlie(), 1)); - assert_eq!(Balances::transfer_count(), 3); - - // transfer_all (transfer remaining balance) - let remaining = Balances::free_balance(alice()); - if remaining > 1 { - assert_ok!(Balances::transfer_all(Some(alice()).into(), bob(), false)); - assert_eq!(Balances::transfer_count(), 4); - } - }); -} diff --git a/pallets/balances/src/types.rs b/pallets/balances/src/types.rs deleted file mode 100644 index a7ccfd3d..00000000 --- a/pallets/balances/src/types.rs +++ /dev/null @@ -1,164 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Types used in the pallet. - -use crate::{Config, CreditOf, Event, Pallet}; -use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; -use core::ops::BitOr; -use frame_support::traits::{Imbalance, LockIdentifier, OnUnbalanced, WithdrawReasons}; -use scale_info::TypeInfo; -use sp_runtime::{RuntimeDebug, Saturating}; - -/// Simplified reasons for withdrawing balance. -#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] -pub enum Reasons { - /// Paying system transaction fees. - Fee = 0, - /// Any reason other than paying system transaction fees. - Misc = 1, - /// Any reason at all. - All = 2, -} - -impl From for Reasons { - fn from(r: WithdrawReasons) -> Reasons { - if r == WithdrawReasons::TRANSACTION_PAYMENT { - Reasons::Fee - } else if r.contains(WithdrawReasons::TRANSACTION_PAYMENT) { - Reasons::All - } else { - Reasons::Misc - } - } -} - -impl BitOr for Reasons { - type Output = Reasons; - fn bitor(self, other: Reasons) -> Reasons { - if self == other { - return self; - } - Reasons::All - } -} - -/// A single lock on a balance. There can be many of these on an account and they "overlap", so the -/// same balance is frozen by multiple locks. -#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] -pub struct BalanceLock { - /// An identifier for this lock. Only one lock may be in existence for each identifier. - pub id: LockIdentifier, - /// The amount which the free balance may not drop below when this lock is in effect. - pub amount: Balance, - /// If true, then the lock remains in effect even for payment of transaction fees. - pub reasons: Reasons, -} - -/// Store named reserved balance. -#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] -pub struct ReserveData { - /// The identifier for the named reserve. - pub id: ReserveIdentifier, - /// The amount of the named reserve. - pub amount: Balance, -} - -/// All balance information for an account. -#[derive(Encode, Decode, Clone, PartialEq, Eq, Default, RuntimeDebug, MaxEncodedLen, TypeInfo)] -pub struct AccountData { - /// Non-reserved part of the balance which the account holder may be able to control. - /// - /// This is the only balance that matters in terms of most operations on tokens. - pub free: Balance, - /// Balance which is has active holds on it and may not be used at all. - /// - /// This is the sum of all individual holds together with any sums still under the (deprecated) - /// reserves API. - pub reserved: Balance, - /// The amount that `free + reserved` may not drop below when reducing the balance, except for - /// actions where the account owner cannot reasonably benefit from the balance reduction, such - /// as slashing. - pub frozen: Balance, - /// Extra information about this account. The MSB is a flag indicating whether the new ref- - /// counting logic is in place for this account. - pub flags: ExtraFlags, -} - -const IS_NEW_LOGIC: u128 = 0x80000000_00000000_00000000_00000000u128; - -#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] -pub struct ExtraFlags(pub(crate) u128); -impl Default for ExtraFlags { - fn default() -> Self { - Self(IS_NEW_LOGIC) - } -} -impl ExtraFlags { - pub fn old_logic() -> Self { - Self(0) - } - pub fn set_new_logic(&mut self) { - self.0 |= IS_NEW_LOGIC - } - pub fn is_new_logic(&self) -> bool { - (self.0 & IS_NEW_LOGIC) == IS_NEW_LOGIC - } -} - -impl AccountData { - pub fn usable(&self) -> Balance { - self.free.saturating_sub(self.frozen) - } - - /// The total balance in this account including any that is reserved and ignoring any frozen. - pub fn total(&self) -> Balance { - self.free.saturating_add(self.reserved) - } -} - -pub struct DustCleaner, I: 'static = ()>( - pub(crate) Option<(T::AccountId, CreditOf)>, -); - -impl, I: 'static> Drop for DustCleaner { - fn drop(&mut self) { - if let Some((who, dust)) = self.0.take() { - Pallet::::deposit_event(Event::DustLost { account: who, amount: dust.peek() }); - T::DustRemoval::on_unbalanced(dust); - } - } -} - -/// Whether something should be interpreted as an increase or a decrease. -#[derive( - Encode, - Decode, - Clone, - PartialEq, - Eq, - RuntimeDebug, - MaxEncodedLen, - TypeInfo, - DecodeWithMemTracking, -)] -pub enum AdjustmentDirection { - /// Increase the amount. - Increase, - /// Decrease the amount. - Decrease, -} diff --git a/pallets/balances/src/weights.rs b/pallets/balances/src/weights.rs deleted file mode 100644 index 0c7a1354..00000000 --- a/pallets/balances/src/weights.rs +++ /dev/null @@ -1,300 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Autogenerated weights for `pallet_balances` -//! -//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 -//! DATE: 2024-11-08, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` -//! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `runner-wiukf8gn-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` -//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` - -// Executed Command: -// ./target/production/substrate-node -// benchmark -// pallet -// --chain=dev -// --steps=50 -// --repeat=20 -// --pallet=pallet_balances -// --no-storage-info -// --no-median-slopes -// --no-min-squares -// --extrinsic=* -// --wasm-execution=compiled -// --heap-pages=4096 -// --output=./substrate/frame/balances/src/weights.rs -// --header=./substrate/HEADER-APACHE2 -// --template=./substrate/.maintain/frame-weight-template.hbs - -#![cfg_attr(rustfmt, rustfmt_skip)] -#![allow(unused_parens)] -#![allow(unused_imports)] -#![allow(missing_docs)] - -use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; -use core::marker::PhantomData; - -/// Weight functions needed for `pallet_balances`. -pub trait WeightInfo { - fn transfer_allow_death() -> Weight; - fn transfer_keep_alive() -> Weight; - fn force_set_balance_creating() -> Weight; - fn force_set_balance_killing() -> Weight; - fn force_transfer() -> Weight; - fn transfer_all() -> Weight; - fn force_unreserve() -> Weight; - fn upgrade_accounts(u: u32, ) -> Weight; - fn force_adjust_total_issuance() -> Weight; - fn burn_allow_death() -> Weight; - fn burn_keep_alive() -> Weight; -} - -/// Weights for `pallet_balances` using the Substrate node and recommended hardware. -pub struct SubstrateWeight(PhantomData); -impl WeightInfo for SubstrateWeight { - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn transfer_allow_death() -> Weight { - // Proof Size summary in bytes: - // Measured: `52` - // Estimated: `3593` - // Minimum execution time: 50_023_000 picoseconds. - Weight::from_parts(51_105_000, 3593) - .saturating_add(T::DbWeight::get().reads(1_u64)) - .saturating_add(T::DbWeight::get().writes(1_u64)) - } - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn transfer_keep_alive() -> Weight { - // Proof Size summary in bytes: - // Measured: `52` - // Estimated: `3593` - // Minimum execution time: 39_923_000 picoseconds. - Weight::from_parts(40_655_000, 3593) - .saturating_add(T::DbWeight::get().reads(1_u64)) - .saturating_add(T::DbWeight::get().writes(1_u64)) - } - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn force_set_balance_creating() -> Weight { - // Proof Size summary in bytes: - // Measured: `174` - // Estimated: `3593` - // Minimum execution time: 15_062_000 picoseconds. - Weight::from_parts(15_772_000, 3593) - .saturating_add(T::DbWeight::get().reads(1_u64)) - .saturating_add(T::DbWeight::get().writes(1_u64)) - } - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn force_set_balance_killing() -> Weight { - // Proof Size summary in bytes: - // Measured: `174` - // Estimated: `3593` - // Minimum execution time: 21_797_000 picoseconds. - Weight::from_parts(22_287_000, 3593) - .saturating_add(T::DbWeight::get().reads(1_u64)) - .saturating_add(T::DbWeight::get().writes(1_u64)) - } - /// Storage: `System::Account` (r:2 w:2) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn force_transfer() -> Weight { - // Proof Size summary in bytes: - // Measured: `155` - // Estimated: `6196` - // Minimum execution time: 51_425_000 picoseconds. - Weight::from_parts(52_600_000, 6196) - .saturating_add(T::DbWeight::get().reads(2_u64)) - .saturating_add(T::DbWeight::get().writes(2_u64)) - } - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn transfer_all() -> Weight { - // Proof Size summary in bytes: - // Measured: `52` - // Estimated: `3593` - // Minimum execution time: 49_399_000 picoseconds. - Weight::from_parts(51_205_000, 3593) - .saturating_add(T::DbWeight::get().reads(1_u64)) - .saturating_add(T::DbWeight::get().writes(1_u64)) - } - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn force_unreserve() -> Weight { - // Proof Size summary in bytes: - // Measured: `174` - // Estimated: `3593` - // Minimum execution time: 18_119_000 picoseconds. - Weight::from_parts(18_749_000, 3593) - .saturating_add(T::DbWeight::get().reads(1_u64)) - .saturating_add(T::DbWeight::get().writes(1_u64)) - } - /// Storage: `System::Account` (r:999 w:999) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// The range of component `u` is `[1, 1000]`. - fn upgrade_accounts(u: u32, ) -> Weight { - // Proof Size summary in bytes: - // Measured: `0 + u * (135 ±0)` - // Estimated: `990 + u * (2603 ±0)` - // Minimum execution time: 16_783_000 picoseconds. - Weight::from_parts(17_076_000, 990) - // Standard Error: 15_126 - .saturating_add(Weight::from_parts(14_834_157, 0).saturating_mul(u.into())) - .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(u.into()))) - .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(u.into()))) - .saturating_add(Weight::from_parts(0, 2603).saturating_mul(u.into())) - } - fn force_adjust_total_issuance() -> Weight { - // Proof Size summary in bytes: - // Measured: `0` - // Estimated: `0` - // Minimum execution time: 6_048_000 picoseconds. - Weight::from_parts(6_346_000, 0) - } - fn burn_allow_death() -> Weight { - // Proof Size summary in bytes: - // Measured: `0` - // Estimated: `0` - // Minimum execution time: 30_215_000 picoseconds. - Weight::from_parts(30_848_000, 0) - } - fn burn_keep_alive() -> Weight { - // Proof Size summary in bytes: - // Measured: `0` - // Estimated: `0` - // Minimum execution time: 20_813_000 picoseconds. - Weight::from_parts(21_553_000, 0) - } -} - -// For backwards compatibility and tests. -impl WeightInfo for () { - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn transfer_allow_death() -> Weight { - // Proof Size summary in bytes: - // Measured: `52` - // Estimated: `3593` - // Minimum execution time: 50_023_000 picoseconds. - Weight::from_parts(51_105_000, 3593) - .saturating_add(RocksDbWeight::get().reads(1_u64)) - .saturating_add(RocksDbWeight::get().writes(1_u64)) - } - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn transfer_keep_alive() -> Weight { - // Proof Size summary in bytes: - // Measured: `52` - // Estimated: `3593` - // Minimum execution time: 39_923_000 picoseconds. - Weight::from_parts(40_655_000, 3593) - .saturating_add(RocksDbWeight::get().reads(1_u64)) - .saturating_add(RocksDbWeight::get().writes(1_u64)) - } - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn force_set_balance_creating() -> Weight { - // Proof Size summary in bytes: - // Measured: `174` - // Estimated: `3593` - // Minimum execution time: 15_062_000 picoseconds. - Weight::from_parts(15_772_000, 3593) - .saturating_add(RocksDbWeight::get().reads(1_u64)) - .saturating_add(RocksDbWeight::get().writes(1_u64)) - } - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn force_set_balance_killing() -> Weight { - // Proof Size summary in bytes: - // Measured: `174` - // Estimated: `3593` - // Minimum execution time: 21_797_000 picoseconds. - Weight::from_parts(22_287_000, 3593) - .saturating_add(RocksDbWeight::get().reads(1_u64)) - .saturating_add(RocksDbWeight::get().writes(1_u64)) - } - /// Storage: `System::Account` (r:2 w:2) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn force_transfer() -> Weight { - // Proof Size summary in bytes: - // Measured: `155` - // Estimated: `6196` - // Minimum execution time: 51_425_000 picoseconds. - Weight::from_parts(52_600_000, 6196) - .saturating_add(RocksDbWeight::get().reads(2_u64)) - .saturating_add(RocksDbWeight::get().writes(2_u64)) - } - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn transfer_all() -> Weight { - // Proof Size summary in bytes: - // Measured: `52` - // Estimated: `3593` - // Minimum execution time: 49_399_000 picoseconds. - Weight::from_parts(51_205_000, 3593) - .saturating_add(RocksDbWeight::get().reads(1_u64)) - .saturating_add(RocksDbWeight::get().writes(1_u64)) - } - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn force_unreserve() -> Weight { - // Proof Size summary in bytes: - // Measured: `174` - // Estimated: `3593` - // Minimum execution time: 18_119_000 picoseconds. - Weight::from_parts(18_749_000, 3593) - .saturating_add(RocksDbWeight::get().reads(1_u64)) - .saturating_add(RocksDbWeight::get().writes(1_u64)) - } - /// Storage: `System::Account` (r:999 w:999) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// The range of component `u` is `[1, 1000]`. - fn upgrade_accounts(u: u32, ) -> Weight { - // Proof Size summary in bytes: - // Measured: `0 + u * (135 ±0)` - // Estimated: `990 + u * (2603 ±0)` - // Minimum execution time: 16_783_000 picoseconds. - Weight::from_parts(17_076_000, 990) - // Standard Error: 15_126 - .saturating_add(Weight::from_parts(14_834_157, 0).saturating_mul(u.into())) - .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(u.into()))) - .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(u.into()))) - .saturating_add(Weight::from_parts(0, 2603).saturating_mul(u.into())) - } - fn force_adjust_total_issuance() -> Weight { - // Proof Size summary in bytes: - // Measured: `0` - // Estimated: `0` - // Minimum execution time: 6_048_000 picoseconds. - Weight::from_parts(6_346_000, 0) - } - fn burn_allow_death() -> Weight { - // Proof Size summary in bytes: - // Measured: `0` - // Estimated: `0` - // Minimum execution time: 30_215_000 picoseconds. - Weight::from_parts(30_848_000, 0) - } - fn burn_keep_alive() -> Weight { - // Proof Size summary in bytes: - // Measured: `0` - // Estimated: `0` - // Minimum execution time: 20_813_000 picoseconds. - Weight::from_parts(21_553_000, 0) - } -} diff --git a/pallets/merkle-airdrop/src/mock.rs b/pallets/merkle-airdrop/src/mock.rs index 0a5c865c..a2486392 100644 --- a/pallets/merkle-airdrop/src/mock.rs +++ b/pallets/merkle-airdrop/src/mock.rs @@ -80,6 +80,7 @@ impl pallet_balances::Config for Test { type MaxFreezes = (); type RuntimeFreezeReason = (); type DoneSlashHandler = (); + type RuntimeEvent = RuntimeEvent; } parameter_types! { @@ -121,6 +122,7 @@ pub fn new_test_ext() -> sp_io::TestExternalities { let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); pallet_balances::GenesisConfig:: { balances: vec![(1, 10_000_000), (MerkleAirdrop::account_id(), 1)], + dev_accounts: None, } .assimilate_storage(&mut t) .unwrap(); diff --git a/pallets/mining-rewards/src/lib.rs b/pallets/mining-rewards/src/lib.rs index cd2a8a32..e135ff79 100644 --- a/pallets/mining-rewards/src/lib.rs +++ b/pallets/mining-rewards/src/lib.rs @@ -26,7 +26,7 @@ pub mod pallet { }, }; use frame_system::pallet_prelude::*; - use qp_wormhole::TransferProofs; + use qp_wormhole::TransferProofRecorder; use sp_consensus_pow::POW_ENGINE_ID; use sp_runtime::{ generic::DigestItem, @@ -48,9 +48,18 @@ pub mod pallet { /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; - /// Currency type that also stores zk proofs - type Currency: Mutate - + qp_wormhole::TransferProofs, Self::AccountId>; + /// Currency type for minting rewards + type Currency: Mutate; + + /// Asset ID type for the proof recorder + type AssetId: Default; + + /// Proof recorder for storing wormhole transfer proofs + type ProofRecorder: qp_wormhole::TransferProofRecorder< + Self::AccountId, + Self::AssetId, + BalanceOf, + >; /// The base block reward given to miners #[pallet::constant] @@ -165,7 +174,12 @@ pub mod pallet { Some(miner) => { let _ = T::Currency::mint_into(&miner, reward).defensive(); - T::Currency::store_transfer_proof(&mint_account, &miner, reward); + let _ = T::ProofRecorder::record_transfer_proof( + None, + mint_account.clone(), + miner.clone(), + reward, + ); Self::deposit_event(Event::MinerRewarded { miner: miner.clone(), reward }); @@ -180,7 +194,12 @@ pub mod pallet { let treasury = T::TreasuryPalletId::get().into_account_truncating(); let _ = T::Currency::mint_into(&treasury, reward).defensive(); - T::Currency::store_transfer_proof(&mint_account, &treasury, reward); + let _ = T::ProofRecorder::record_transfer_proof( + None, + mint_account.clone(), + treasury.clone(), + reward, + ); Self::deposit_event(Event::TreasuryRewarded { reward }); diff --git a/pallets/mining-rewards/src/mock.rs b/pallets/mining-rewards/src/mock.rs index e69e6012..1b036245 100644 --- a/pallets/mining-rewards/src/mock.rs +++ b/pallets/mining-rewards/src/mock.rs @@ -67,6 +67,7 @@ impl frame_system::Config for Test { } impl pallet_balances::Config for Test { + type RuntimeEvent = RuntimeEvent; type RuntimeHoldReason = (); type RuntimeFreezeReason = (); type WeightInfo = (); @@ -87,8 +88,27 @@ parameter_types! { pub const MintingAccount: sp_core::crypto::AccountId32 = sp_core::crypto::AccountId32::new([99u8; 32]); } +// Mock proof recorder that does nothing +pub struct MockProofRecorder; +impl qp_wormhole::TransferProofRecorder + for MockProofRecorder +{ + type Error = (); + + fn record_transfer_proof( + _asset_id: Option, + _from: sp_core::crypto::AccountId32, + _to: sp_core::crypto::AccountId32, + _amount: u128, + ) -> Result<(), Self::Error> { + Ok(()) + } +} + impl pallet_mining_rewards::Config for Test { type Currency = Balances; + type AssetId = u32; + type ProofRecorder = MockProofRecorder; type WeightInfo = (); type MinerBlockReward = BlockReward; type TreasuryBlockReward = TreasuryBlockReward; @@ -115,6 +135,7 @@ pub fn new_test_ext() -> sp_io::TestExternalities { pallet_balances::GenesisConfig:: { balances: vec![(miner(), ExistentialDeposit::get()), (miner2(), ExistentialDeposit::get())], + dev_accounts: None, } .assimilate_storage(&mut t) .unwrap(); diff --git a/pallets/reversible-transfers/src/tests/mock.rs b/pallets/reversible-transfers/src/tests/mock.rs index 39aa6052..3c078314 100644 --- a/pallets/reversible-transfers/src/tests/mock.rs +++ b/pallets/reversible-transfers/src/tests/mock.rs @@ -349,6 +349,7 @@ pub fn new_test_ext() -> sp_io::TestExternalities { // Treasury account for fee collection tests (must meet existential deposit) (account_id(99), 1), ], + dev_accounts: None, } .assimilate_storage(&mut t) .unwrap(); diff --git a/pallets/wormhole/Cargo.toml b/pallets/wormhole/Cargo.toml index 78e624e7..a1a1d064 100644 --- a/pallets/wormhole/Cargo.toml +++ b/pallets/wormhole/Cargo.toml @@ -27,10 +27,12 @@ scale-info = { workspace = true, default-features = false, features = [ ] } sp-core.workspace = true sp-io.workspace = true +sp-metadata-ir.workspace = true sp-runtime.workspace = true [dev-dependencies] hex = { workspace = true, features = ["alloc"] } +pallet-assets = { workspace = true, features = ["std"] } qp-dilithium-crypto = { workspace = true, features = ["std"] } qp-plonky2 = { workspace = true, default-features = false } qp-poseidon-core.workspace = true diff --git a/pallets/wormhole/common.bin b/pallets/wormhole/common.bin index 9232df9c3bdbf92f77f73c7a28db999bdfafe660..5fbea0f3cae044a80d0ad7def777f01eaf656179 100644 GIT binary patch delta 14 Vcmey!_mOXdC^MtjW-;by762x)1PK5D delta 14 Vcmey!_mOXdC^MtzW-;by762x!1PA~C diff --git a/pallets/wormhole/src/lib.rs b/pallets/wormhole/src/lib.rs index 86d8281a..ec635407 100644 --- a/pallets/wormhole/src/lib.rs +++ b/pallets/wormhole/src/lib.rs @@ -2,8 +2,13 @@ extern crate alloc; +use core::marker::PhantomData; + +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::StorageHasher; use lazy_static::lazy_static; pub use pallet::*; +use qp_poseidon::PoseidonHasher as PoseidonCore; use qp_wormhole_verifier::WormholeVerifier; #[cfg(test)] @@ -11,6 +16,7 @@ mod mock; #[cfg(test)] mod tests; pub mod weights; +use sp_metadata_ir::StorageHasherIR; pub use weights::*; #[cfg(feature = "runtime-benchmarks")] @@ -29,43 +35,86 @@ pub fn get_wormhole_verifier() -> Result<&'static WormholeVerifier, &'static str WORMHOLE_VERIFIER.as_ref().ok_or("Wormhole verifier not available") } +pub struct PoseidonStorageHasher(PhantomData); + +impl StorageHasher + for PoseidonStorageHasher +{ + // We are lying here, but maybe it's ok because it's just metadata + const METADATA: StorageHasherIR = StorageHasherIR::Identity; + type Output = [u8; 32]; + + fn hash(x: &[u8]) -> Self::Output { + PoseidonCore::hash_storage::(x) + } + + fn max_len() -> usize { + 32 + } +} + #[frame_support::pallet] pub mod pallet { - use crate::WeightInfo; + use crate::{PoseidonStorageHasher, WeightInfo}; use alloc::vec::Vec; use codec::Decode; use frame_support::{ pallet_prelude::*, traits::{ fungible::{Mutate, Unbalanced}, + fungibles::{self, Inspect as FungiblesInspect, Mutate as FungiblesMutate}, + tokens::Preservation, Currency, ExistenceRequirement, WithdrawReasons, }, weights::WeightToFee, }; use frame_system::pallet_prelude::*; - use qp_wormhole::TransferProofs; use qp_wormhole_circuit::inputs::PublicCircuitInputs; use qp_wormhole_verifier::ProofWithPublicInputs; use qp_zk_circuits_common::circuit::{C, D, F}; use sp_runtime::{ - traits::{Saturating, Zero}, + traits::{Saturating, StaticLookup, Zero}, Perbill, }; pub type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; + pub type AssetIdOf = <::Assets as fungibles::Inspect< + ::AccountId, + >>::AssetId; + pub type AssetBalanceOf = <::Assets as fungibles::Inspect< + ::AccountId, + >>::Balance; + pub type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; #[pallet::pallet] pub struct Pallet(_); #[pallet::config] - pub trait Config: frame_system::Config { - /// Currency type used for minting tokens and handling wormhole transfers + pub trait Config: frame_system::Config + where + AssetIdOf: Default + From + Clone, + BalanceOf: Default, + AssetBalanceOf: Into> + From>, + { + /// Currency type used for native token transfers and minting type Currency: Mutate> - + TransferProofs, Self::AccountId> + Unbalanced + Currency; + /// Assets type used for managing fungible assets + type Assets: fungibles::Inspect + + fungibles::Mutate + + fungibles::Create; + + /// Transfer count type used in storage + type TransferCount: Parameter + + MaxEncodedLen + + Default + + Saturating + + Copy + + sp_runtime::traits::One; + /// Account ID used as the "from" account when creating transfer proofs for minted tokens #[pallet::constant] type MintingAccount: Get; @@ -81,10 +130,41 @@ pub mod pallet { pub(super) type UsedNullifiers = StorageMap<_, Blake2_128Concat, [u8; 32], bool, ValueQuery>; + /// Transfer proofs for wormhole transfers (both native and assets) + #[pallet::storage] + #[pallet::getter(fn transfer_proof)] + pub type TransferProof = StorageMap< + _, + PoseidonStorageHasher, + (AssetIdOf, T::TransferCount, T::AccountId, T::AccountId, BalanceOf), /* (asset_id, tx_count, from, to, amount) */ + (), + OptionQuery, + >; + + /// Transfer count for all wormhole transfers + #[pallet::storage] + #[pallet::getter(fn transfer_count)] + pub type TransferCount = StorageValue<_, T::TransferCount, ValueQuery>; + #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { - ProofVerified { exit_amount: BalanceOf }, + ProofVerified { + exit_amount: BalanceOf, + }, + NativeTransferred { + from: T::AccountId, + to: T::AccountId, + amount: BalanceOf, + transfer_count: T::TransferCount, + }, + AssetTransferred { + asset_id: AssetIdOf, + from: T::AccountId, + to: T::AccountId, + amount: AssetBalanceOf, + transfer_count: T::TransferCount, + }, } #[pallet::error] @@ -99,6 +179,8 @@ pub mod pallet { StorageRootMismatch, BlockNotFound, InvalidBlockNumber, + AssetNotFound, + SelfTransfer, } #[pallet::call] @@ -167,6 +249,10 @@ pub mod pallet { let exit_account = T::AccountId::decode(&mut &exit_account_bytes[..]) .map_err(|_| Error::::InvalidPublicInputs)?; + // Extract asset_id from public inputs + let asset_id_u32 = public_inputs.asset_id; + let asset_id: AssetIdOf = asset_id_u32.into(); + // Calculate fees first let weight = ::WeightInfo::verify_wormhole_proof(); let weight_fee = T::WeightToFee::weight_to_fee(&weight); @@ -174,34 +260,154 @@ pub mod pallet { let volume_fee = volume_fee_perbill * exit_balance; let total_fee = weight_fee.saturating_add(volume_fee); - // Mint tokens to the exit account - // This does not affect total issuance and does not create an imbalance - >::increase_balance( - &exit_account, - exit_balance, - frame_support::traits::tokens::Precision::Exact, - )?; + // Handle native (asset_id = 0) or asset transfers + if asset_id == AssetIdOf::::default() { + // Native token transfer + // Mint tokens to the exit account + // This does not affect total issuance and does not create an imbalance + >::increase_balance( + &exit_account, + exit_balance, + frame_support::traits::tokens::Precision::Exact, + )?; - // Withdraw fee from exit account if fees are non-zero - // This creates a negative imbalance that will be handled by the transaction payment - // pallet - if !total_fee.is_zero() { - let _fee_imbalance = T::Currency::withdraw( + // Withdraw fee from exit account if fees are non-zero + // This creates a negative imbalance that will be handled by the transaction payment + // pallet + if !total_fee.is_zero() { + let _fee_imbalance = T::Currency::withdraw( + &exit_account, + total_fee, + WithdrawReasons::TRANSACTION_PAYMENT, + ExistenceRequirement::KeepAlive, + )?; + } + } else { + // Asset transfer + let asset_balance: AssetBalanceOf = exit_balance.into(); + >::mint_into( + asset_id.clone(), &exit_account, - total_fee, - WithdrawReasons::TRANSACTION_PAYMENT, - ExistenceRequirement::KeepAlive, + asset_balance, )?; + + // For assets, we still need to charge fees in native currency + // The exit account must have enough native balance to pay fees + if !total_fee.is_zero() { + let _fee_imbalance = T::Currency::withdraw( + &exit_account, + total_fee, + WithdrawReasons::TRANSACTION_PAYMENT, + ExistenceRequirement::AllowDeath, + )?; + } } // Create a transfer proof for the minted tokens let mint_account = T::MintingAccount::get(); - T::Currency::store_transfer_proof(&mint_account, &exit_account, exit_balance); + Self::record_transfer(asset_id, mint_account, exit_account, exit_balance)?; // Emit event Self::deposit_event(Event::ProofVerified { exit_amount: exit_balance }); Ok(()) } + + /// Transfer native tokens and store proof for wormhole + #[pallet::call_index(1)] + #[pallet::weight(T::DbWeight::get().reads_writes(1, 2))] + pub fn transfer_native( + origin: OriginFor, + dest: AccountIdLookupOf, + #[pallet::compact] amount: BalanceOf, + ) -> DispatchResult { + let source = ensure_signed(origin)?; + let dest = T::Lookup::lookup(dest)?; + + // Prevent self-transfers + ensure!(source != dest, Error::::SelfTransfer); + + // Perform the transfer + >::transfer(&source, &dest, amount, Preservation::Expendable)?; + + // Store proof with asset_id = Default (0 for native) + Self::record_transfer(AssetIdOf::::default(), source, dest, amount)?; + + Ok(()) + } + + /// Transfer asset tokens and store proof for wormhole + #[pallet::call_index(2)] + #[pallet::weight(T::DbWeight::get().reads_writes(2, 2))] + pub fn transfer_asset( + origin: OriginFor, + asset_id: AssetIdOf, + dest: AccountIdLookupOf, + #[pallet::compact] amount: AssetBalanceOf, + ) -> DispatchResult { + let source = ensure_signed(origin)?; + let dest = T::Lookup::lookup(dest)?; + + // Prevent self-transfers + ensure!(source != dest, Error::::SelfTransfer); + + // Check if asset exists + ensure!( + >::asset_exists(asset_id.clone()), + Error::::AssetNotFound + ); + + // Perform the transfer + >::transfer( + asset_id.clone(), + &source, + &dest, + amount, + Preservation::Expendable, + )?; + + // Store proof + Self::record_transfer(asset_id, source, dest, amount.into())?; + + Ok(()) + } + } + + // Helper functions for recording transfer proofs + impl Pallet { + /// Record a transfer proof + /// This should be called by transaction extensions or other runtime components + pub fn record_transfer( + asset_id: AssetIdOf, + from: T::AccountId, + to: T::AccountId, + amount: BalanceOf, + ) -> DispatchResult { + let current_count = TransferCount::::get(); + TransferProof::::insert( + (asset_id, current_count, from.clone(), to.clone(), amount), + (), + ); + TransferCount::::put(current_count.saturating_add(T::TransferCount::one())); + + Ok(()) + } + } + + // Implement the TransferProofRecorder trait for other pallets to use + impl qp_wormhole::TransferProofRecorder, BalanceOf> + for Pallet + { + type Error = DispatchError; + + fn record_transfer_proof( + asset_id: Option>, + from: T::AccountId, + to: T::AccountId, + amount: BalanceOf, + ) -> Result<(), Self::Error> { + let asset_id_value = asset_id.unwrap_or_default(); + Self::record_transfer(asset_id_value, from, to, amount) + } } } diff --git a/pallets/wormhole/src/mock.rs b/pallets/wormhole/src/mock.rs index c902099c..7f0a4024 100644 --- a/pallets/wormhole/src/mock.rs +++ b/pallets/wormhole/src/mock.rs @@ -1,19 +1,19 @@ use crate as pallet_wormhole; use frame_support::{ construct_runtime, parameter_types, - traits::{ConstU32, Everything}, + traits::{ConstU128, ConstU32, Everything}, weights::IdentityFee, }; use frame_system::mocking::MockUncheckedExtrinsic; use qp_poseidon::PoseidonHasher; use sp_core::H256; use sp_runtime::{traits::IdentityLookup, BuildStorage}; -// --- MOCK RUNTIME --- construct_runtime!( pub enum Test { System: frame_system, Balances: pallet_balances, + Assets: pallet_assets, Wormhole: pallet_wormhole, } ); @@ -32,8 +32,6 @@ pub fn account_id(id: u64) -> AccountId { AccountId::new(bytes) } -// --- FRAME SYSTEM --- - parameter_types! { pub const BlockHashCount: u64 = 250; } @@ -71,8 +69,6 @@ impl frame_system::Config for Test { type PostTransactions = (); } -// --- PALLET BALANCES --- - parameter_types! { pub const ExistentialDeposit: Balance = 1; } @@ -91,9 +87,31 @@ impl pallet_balances::Config for Test { type MaxReserves = (); type MaxFreezes = (); type DoneSlashHandler = (); + type RuntimeEvent = RuntimeEvent; } -// --- PALLET WORMHOLE --- +impl pallet_assets::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type AssetId = u32; + type AssetIdParameter = u32; + type Currency = Balances; + type CreateOrigin = + frame_support::traits::AsEnsureOriginWithArg>; + type ForceOrigin = frame_system::EnsureRoot; + type AssetDeposit = ConstU128<1>; + type AssetAccountDeposit = ConstU128<1>; + type MetadataDepositBase = ConstU128<1>; + type MetadataDepositPerByte = ConstU128<1>; + type ApprovalDeposit = ConstU128<1>; + type StringLimit = ConstU32<50>; + type Freezer = (); + type Extra = (); + type WeightInfo = (); + type RemoveItemsLimit = ConstU32<1000>; + type CallbackHandle = (); + type Holder = (); +} parameter_types! { pub const MintingAccount: AccountId = AccountId::new([ @@ -106,19 +124,13 @@ impl pallet_wormhole::Config for Test { type WeightInfo = crate::weights::SubstrateWeight; type WeightToFee = IdentityFee; type Currency = Balances; + type Assets = Assets; + type TransferCount = u64; type MintingAccount = MintingAccount; } // Helper function to build a genesis configuration pub fn new_test_ext() -> sp_state_machine::TestExternalities { - let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); - - let endowment = 1e18 as Balance; - pallet_balances::GenesisConfig:: { - balances: vec![(account_id(1), endowment), (account_id(2), endowment)], - } - .assimilate_storage(&mut t) - .unwrap(); - + let t = frame_system::GenesisConfig::::default().build_storage().unwrap(); t.into() } diff --git a/pallets/wormhole/src/tests.rs b/pallets/wormhole/src/tests.rs index ae2a7de1..74054d08 100644 --- a/pallets/wormhole/src/tests.rs +++ b/pallets/wormhole/src/tests.rs @@ -21,7 +21,6 @@ mod wormhole_tests { }; use sp_runtime::{traits::Header, DigestItem}; - // Helper function to generate proof and inputs for fn generate_proof(inputs: CircuitInputs) -> ProofWithPublicInputs { let config = CircuitConfig::standard_recursion_config(); let prover = WormholeProver::new(config); @@ -32,7 +31,6 @@ mod wormhole_tests { #[test] fn test_wormhole_transfer_proof_generation() { - // Setup accounts let alice = account_id(1); let secret: BytesDigest = [1u8; 32].try_into().expect("valid secret"); let unspendable_account = @@ -49,12 +47,10 @@ mod wormhole_tests { let mut ext = new_test_ext(); - // Execute the transfer and get the header let (storage_key, state_root, leaf_hash, event_transfer_count, header) = ext.execute_with(|| { System::set_block_number(1); - // Add dummy digest items to match expected format let pre_runtime_data = vec![ 233, 182, 183, 107, 158, 1, 115, 19, 219, 126, 253, 86, 30, 208, 176, 70, 21, 45, 180, 229, 9, 62, 91, 4, 6, 53, 245, 52, 48, 38, 123, 225, @@ -68,18 +64,18 @@ mod wormhole_tests { System::deposit_log(DigestItem::PreRuntime(*b"pow_", pre_runtime_data)); System::deposit_log(DigestItem::Seal(*b"pow_", seal_data)); - assert_ok!(Balances::mint_into(&unspendable_account_id, funding_amount)); - assert_ok!(Balances::transfer_keep_alive( + assert_ok!(Balances::mint_into(&alice, funding_amount)); + assert_ok!(Wormhole::transfer_native( frame_system::RawOrigin::Signed(alice.clone()).into(), unspendable_account_id.clone(), funding_amount, )); - let transfer_count = pallet_balances::TransferCount::::get(); - let event_transfer_count = transfer_count - 1; + let event_transfer_count = 0u64; let leaf_hash = PoseidonHasher::hash_storage::( &( + 0u32, event_transfer_count, alice.clone(), unspendable_account_id.clone(), @@ -88,7 +84,8 @@ mod wormhole_tests { .encode(), ); - let proof_address = pallet_balances::TransferProof::::hashed_key_for(&( + let proof_address = crate::pallet::TransferProof::::hashed_key_for(&( + 0u32, event_transfer_count, alice.clone(), unspendable_account_id.clone(), @@ -103,36 +100,31 @@ mod wormhole_tests { (storage_key, state_root, leaf_hash, event_transfer_count, header) }); - // Generate a storage proof for the specific storage key use sp_state_machine::prove_read; let proof = prove_read(ext.as_backend(), &[&storage_key]) .expect("failed to generate storage proof"); let proof_nodes_vec: Vec> = proof.iter_nodes().map(|n| n.to_vec()).collect(); - // Prepare the storage proof for the circuit let processed_storage_proof = prepare_proof_for_circuit(proof_nodes_vec, hex::encode(&state_root), leaf_hash) .expect("failed to prepare proof for circuit"); - // Build the header components let parent_hash = *header.parent_hash(); let extrinsics_root = *header.extrinsics_root(); let digest = header.digest().encode(); let digest_array: [u8; 110] = digest.try_into().expect("digest should be 110 bytes"); let block_number: u32 = (*header.number()).try_into().expect("block number fits in u32"); - // Compute block hash let block_hash = header.hash(); - // Assemble circuit inputs let circuit_inputs = CircuitInputs { private: PrivateCircuitInputs { secret, + storage_proof: processed_storage_proof, transfer_count: event_transfer_count, funding_account: BytesDigest::try_from(alice.as_ref() as &[u8]) .expect("account is 32 bytes"), - storage_proof: processed_storage_proof, unspendable_account: Digest::from(unspendable_account).into(), state_root: BytesDigest::try_from(state_root.as_ref()) .expect("state root is 32 bytes"), @@ -141,6 +133,7 @@ mod wormhole_tests { digest: digest_array, }, public: PublicCircuitInputs { + asset_id: 0u32, funding_amount, nullifier: Nullifier::from_preimage(secret, event_transfer_count).hash.into(), exit_account: BytesDigest::try_from(exit_account_id.as_ref() as &[u8]) @@ -153,33 +146,25 @@ mod wormhole_tests { }, }; - // Generate the ZK proof let proof = generate_proof(circuit_inputs); - // Verify the proof can be parsed let public_inputs = PublicCircuitInputs::try_from(&proof).expect("failed to parse public inputs"); - // Verify that the public inputs match what we expect assert_eq!(public_inputs.funding_amount, funding_amount); assert_eq!( public_inputs.exit_account, BytesDigest::try_from(exit_account_id.as_ref() as &[u8]).unwrap() ); - // Verify the proof using the verifier let verifier = get_wormhole_verifier().expect("verifier should be available"); verifier.verify(proof.clone()).expect("proof should verify"); - // Serialize the proof to bytes for extrinsic testing let proof_bytes = proof.to_bytes(); - // Now test the extrinsic in a new environment new_test_ext().execute_with(|| { - // Set up the blockchain state to have block 1 System::set_block_number(1); - // Add the same digest items let pre_runtime_data = vec![ 233, 182, 183, 107, 158, 1, 115, 19, 219, 126, 253, 86, 30, 208, 176, 70, 21, 45, 180, 229, 9, 62, 91, 4, 6, 53, 245, 52, 48, 38, 123, 225, @@ -193,36 +178,28 @@ mod wormhole_tests { System::deposit_log(DigestItem::PreRuntime(*b"pow_", pre_runtime_data)); System::deposit_log(DigestItem::Seal(*b"pow_", seal_data)); - // Execute the same transfer to recreate the exact state - assert_ok!(Balances::mint_into(&unspendable_account_id, funding_amount)); - assert_ok!(Balances::transfer_keep_alive( + assert_ok!(Balances::mint_into(&alice, funding_amount)); + assert_ok!(Wormhole::transfer_native( frame_system::RawOrigin::Signed(alice.clone()).into(), unspendable_account_id.clone(), funding_amount, )); - // Finalize the block to get the same header and store the block hash let block_1_header = System::finalize(); - // Initialize block 2 to store block 1's hash System::reset_events(); System::initialize(&2, &block_1_header.hash(), block_1_header.digest()); - // Check exit account balance before verification let balance_before = Balances::balance(&exit_account_id); assert_eq!(balance_before, 0); - // Call the verify_wormhole_proof extrinsic assert_ok!(Wormhole::verify_wormhole_proof( frame_system::RawOrigin::None.into(), proof_bytes.clone() )); - // Check that the exit account received the funds (minus fees) let balance_after = Balances::balance(&exit_account_id); - // The balance should be funding_amount minus fees - // Weight fee + 0.1% volume fee assert!(balance_after > 0, "Exit account should have received funds"); assert!( balance_after < funding_amount, @@ -230,33 +207,160 @@ mod wormhole_tests { ); }); - // Test that proof fails when state doesn't match new_test_ext().execute_with(|| { - // Set up block 1 but DON'T recreate the exact same state System::set_block_number(1); - // Add different digest items with same 110-byte format but different content - let pre_runtime_data = vec![1u8; 32]; // Different data - let seal_data = vec![2u8; 64]; // Different data + let pre_runtime_data = vec![1u8; 32]; + let seal_data = vec![2u8; 64]; System::deposit_log(DigestItem::PreRuntime(*b"pow_", pre_runtime_data)); System::deposit_log(DigestItem::Seal(*b"pow_", seal_data)); - // Finalize block 1 with different state let different_header = System::finalize(); - // Initialize block 2 System::reset_events(); System::initialize(&2, &different_header.hash(), different_header.digest()); - // Try to use the proof with the original header (which has different block hash) let result = Wormhole::verify_wormhole_proof( frame_system::RawOrigin::None.into(), proof_bytes.clone(), ); - // This should fail because the block hash in the proof doesn't match assert!(result.is_err(), "Proof verification should fail with mismatched state"); }); } + + #[test] + fn transfer_native_works() { + new_test_ext().execute_with(|| { + let alice = account_id(1); + let bob = account_id(2); + let amount = 1000u128; + + assert_ok!(Balances::mint_into(&alice, amount * 2)); + + let count_before = Wormhole::transfer_count(); + assert_ok!(Wormhole::transfer_native( + frame_system::RawOrigin::Signed(alice.clone()).into(), + bob.clone(), + amount, + )); + + assert_eq!(Balances::balance(&alice), amount); + assert_eq!(Balances::balance(&bob), amount); + assert_eq!(Wormhole::transfer_count(), count_before + 1); + assert!(Wormhole::transfer_proof((0u32, count_before, alice, bob, amount)).is_some()); + }); + } + + #[test] + fn transfer_native_fails_on_self_transfer() { + new_test_ext().execute_with(|| { + let alice = account_id(1); + let amount = 1000u128; + + assert_ok!(Balances::mint_into(&alice, amount)); + + let result = Wormhole::transfer_native( + frame_system::RawOrigin::Signed(alice.clone()).into(), + alice.clone(), + amount, + ); + + assert!(result.is_err()); + }); + } + + #[test] + fn transfer_asset_works() { + new_test_ext().execute_with(|| { + let alice = account_id(1); + let bob = account_id(2); + let asset_id = 1u32; + let amount = 1000u128; + + assert_ok!(Balances::mint_into(&alice, 1000)); + assert_ok!(Balances::mint_into(&bob, 1000)); + + assert_ok!(Assets::create( + frame_system::RawOrigin::Signed(alice.clone()).into(), + asset_id.into(), + alice.clone(), + 1, + )); + assert_ok!(Assets::mint( + frame_system::RawOrigin::Signed(alice.clone()).into(), + asset_id.into(), + alice.clone(), + amount * 2, + )); + + let count_before = Wormhole::transfer_count(); + assert_ok!(Wormhole::transfer_asset( + frame_system::RawOrigin::Signed(alice.clone()).into(), + asset_id, + bob.clone(), + amount, + )); + + assert_eq!(Assets::balance(asset_id, &alice), amount); + assert_eq!(Assets::balance(asset_id, &bob), amount); + assert_eq!(Wormhole::transfer_count(), count_before + 1); + assert!( + Wormhole::transfer_proof((asset_id, count_before, alice, bob, amount)).is_some() + ); + }); + } + + #[test] + fn transfer_asset_fails_on_nonexistent_asset() { + new_test_ext().execute_with(|| { + let alice = account_id(1); + let bob = account_id(2); + let asset_id = 999u32; + let amount = 1000u128; + + let result = Wormhole::transfer_asset( + frame_system::RawOrigin::Signed(alice.clone()).into(), + asset_id, + bob.clone(), + amount, + ); + + assert!(result.is_err()); + }); + } + + #[test] + fn transfer_asset_fails_on_self_transfer() { + new_test_ext().execute_with(|| { + let alice = account_id(1); + let asset_id = 1u32; + let amount = 1000u128; + + assert_ok!(Balances::mint_into(&alice, 1000)); + + assert_ok!(Assets::create( + frame_system::RawOrigin::Signed(alice.clone()).into(), + asset_id.into(), + alice.clone(), + 1, + )); + assert_ok!(Assets::mint( + frame_system::RawOrigin::Signed(alice.clone()).into(), + asset_id.into(), + alice.clone(), + amount, + )); + + let result = Wormhole::transfer_asset( + frame_system::RawOrigin::Signed(alice.clone()).into(), + asset_id, + alice.clone(), + amount, + ); + + assert!(result.is_err()); + }); + } } diff --git a/pallets/wormhole/verifier.bin b/pallets/wormhole/verifier.bin index 121eb2b02d5badfed2fbdcc2c29e9d7a7ffec909..229b59d97d5e8e140b16a1bc97e9549f0e4bc250 100644 GIT binary patch literal 552 zcmV+@0@wWn000000001mTjTr3B2agUHfFrUGtEY?c|aBKvf6rpL30{-^KRXXhj{y2 zz8rIqg$hj))SW(&TT7nI=r=es;;pp^jB9#{Wjfz5a$)`k+E?!pk^>&gHRX$Ingu`y8n-c zJ4+g-gc+nLhyn3)HX@t(d~8{_e(RT+vai%?7x2){Ce*}7_VylH$PGOLr)tkX#p})s zr7qItfqi#*J=?eEk2o=|CDSY_n{R`|O1U8T??>5(E688U=+Uj_pQxX(RL~e&kD1Pi z_i`~Yi$@m0KytTd7b+bf7E}nZ_#UPwlzOM)`cxy{9CBMxTGcWQxCcW?M<{4g+(VC~ z4v$C+4f{RIosj!{M*%kuv>w@IFe22t-faOfu;e#vsJH)K`f9dloD`s(J){@l1q|hK qi>>->!lCIbJkXVcY)vNi24;Io?*Yn7P+jd*v`UX^%^YUdrQkHvuml_sF!}-T;4qK!GOD-UtYyb zZfVj=Uoi;ph0JLtv6@F>U9H=K?8W=9Um3#E&28RBK*imrCh|tLm``n>T0iw%B&g~U zLHCzUTEj;0(O=t!{eN>!%#+cA6Jn(-a7yeFbw!h6R3ADGoPx(lp`OJ9E9cB0yYUCvl07q z2E9l&(l;2u;gF8HIhzbxcX&Omio1+M9&4fZM%uPN(d_}gXv3BeaZh4RY>l>s?G9bk z&ekwT5?b=MPr;MuP0MkQ%(e&ddMhGQVUJ5L>W-xNqo|R;gFc|_#5f`v2^nITGT}?g z)0Gs3-u}JJUGbhZm!M0U++70}*uA7{Dveh_&fT$~77p?a*$|iW9q^4&?iN?J93InB z_r-UE{N^JNuC9aIJk+lv!L++*S;2N+gcngm8J6`v+%Xqm<$jET;|f!$tR5)JxbPf~ qKk { + /// Error type for proof recording failures + type Error; -/// Trait for managing wormhole transfer proofs. -pub trait TransferProofs { - /// Get transfer proof, if any - fn transfer_proof_exists( - count: TxCount, - from: &AccountId, - to: &AccountId, - value: Balance, - ) -> bool; - - /// Get transfer proof key - fn transfer_proof_key( - count: TxCount, + /// Record a transfer proof for native or asset tokens + /// - `None` for native tokens (asset_id = 0) + /// - `Some(asset_id)` for specific assets + fn record_transfer_proof( + asset_id: Option, from: AccountId, to: AccountId, - value: Balance, - ) -> Vec; - - /// Store transfer proofs for a given wormhole transfer. - fn store_transfer_proof(from: &AccountId, to: &AccountId, value: Balance); + amount: Balance, + ) -> Result<(), Self::Error>; } diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index d236ac50..9bb0e64b 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -53,6 +53,10 @@ qp-dilithium-crypto.workspace = true qp-header = { workspace = true, features = ["serde"] } qp-poseidon = { workspace = true, features = ["serde"] } qp-scheduler.workspace = true +qp-wormhole.workspace = true +qp-wormhole-circuit = { workspace = true, default-features = false } +qp-wormhole-verifier = { workspace = true, default-features = false } +qp-zk-circuits-common = { workspace = true, default-features = false } scale-info = { features = ["derive", "serde"], workspace = true } serde_json = { workspace = true, default-features = false, features = [ "alloc", diff --git a/runtime/src/configs/mod.rs b/runtime/src/configs/mod.rs index 693319cf..c50898d8 100644 --- a/runtime/src/configs/mod.rs +++ b/runtime/src/configs/mod.rs @@ -63,10 +63,10 @@ use sp_version::RuntimeVersion; // Local module imports use super::{ - AccountId, Balance, Balances, Block, BlockNumber, Hash, Nonce, OriginCaller, PalletInfo, - Preimage, Referenda, Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, - RuntimeHoldReason, RuntimeOrigin, RuntimeTask, Scheduler, System, Timestamp, Vesting, DAYS, - EXISTENTIAL_DEPOSIT, MICRO_UNIT, TARGET_BLOCK_TIME_MS, UNIT, VERSION, + AccountId, Assets, Balance, Balances, Block, BlockNumber, Hash, Nonce, OriginCaller, + PalletInfo, Preimage, Referenda, Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, + RuntimeHoldReason, RuntimeOrigin, RuntimeTask, Scheduler, System, Timestamp, Vesting, Wormhole, + DAYS, EXISTENTIAL_DEPOSIT, MICRO_UNIT, TARGET_BLOCK_TIME_MS, UNIT, VERSION, }; use sp_core::U512; @@ -130,6 +130,8 @@ parameter_types! { impl pallet_mining_rewards::Config for Runtime { type Currency = Balances; + type AssetId = AssetId; + type ProofRecorder = Wormhole; type WeightInfo = pallet_mining_rewards::weights::SubstrateWeight; type MinerBlockReward = ConstU128<{ 10 * UNIT }>; // 10 tokens type TreasuryBlockReward = ConstU128<0>; // 0 tokens @@ -181,6 +183,7 @@ parameter_types! { } impl pallet_balances::Config for Runtime { + type RuntimeEvent = RuntimeEvent; type RuntimeHoldReason = RuntimeHoldReason; type RuntimeFreezeReason = RuntimeFreezeReason; type WeightInfo = pallet_balances::weights::SubstrateWeight; @@ -630,5 +633,7 @@ impl pallet_wormhole::Config for Runtime { type MintingAccount = WormholeMintingAccount; type WeightInfo = (); type Currency = Balances; + type Assets = Assets; + type TransferCount = u64; type WeightToFee = IdentityFee; } diff --git a/runtime/src/genesis_config_presets.rs b/runtime/src/genesis_config_presets.rs index 57eb5182..c86a093c 100644 --- a/runtime/src/genesis_config_presets.rs +++ b/runtime/src/genesis_config_presets.rs @@ -19,14 +19,15 @@ #![allow(clippy::expect_used)] use crate::{ - configs::TreasuryPalletId, AccountId, BalancesConfig, RuntimeGenesisConfig, SudoConfig, UNIT, + configs::TreasuryPalletId, AccountId, AssetsConfig, BalancesConfig, RuntimeGenesisConfig, + SudoConfig, EXISTENTIAL_DEPOSIT, UNIT, }; use alloc::{vec, vec::Vec}; use qp_dilithium_crypto::pair::{crystal_alice, crystal_charlie, dilithium_bob}; use serde_json::Value; use sp_core::crypto::Ss58Codec; use sp_genesis_builder::{self, PresetId}; -use sp_runtime::traits::{AccountIdConversion, IdentifyAccount}; +use sp_runtime::traits::{AccountIdConversion, IdentifyAccount, Zero}; /// Identifier for the heisenberg runtime preset. pub const HEISENBERG_RUNTIME_PRESET: &str = "heisenberg"; @@ -62,8 +63,16 @@ fn genesis_template(endowed_accounts: Vec, root: AccountId) -> Value balances.push((treasury_account, ONE_BILLION * UNIT)); let config = RuntimeGenesisConfig { - balances: BalancesConfig { balances }, + balances: BalancesConfig { balances, dev_accounts: None }, sudo: SudoConfig { key: Some(root.clone()) }, + assets: AssetsConfig { + // We need to initialize and reserve the first asset id for the native token transfers + // with wormhole. + assets: vec![(Zero::zero(), root.clone(), false, EXISTENTIAL_DEPOSIT)], /* (asset_id, + * owner, is_sufficient, + * min_balance) */ + ..Default::default() + }, ..Default::default() }; diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index d0835494..876fb79c 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -159,6 +159,7 @@ pub type TxExtension = ( pallet_transaction_payment::ChargeTransactionPayment, frame_metadata_hash_extension::CheckMetadataHash, transaction_extensions::ReversibleTransactionExtension, + transaction_extensions::WormholeProofRecorderExtension, ); /// Unchecked extrinsic type as expected by this runtime. diff --git a/runtime/src/transaction_extensions.rs b/runtime/src/transaction_extensions.rs index 5afed1e6..6216b96d 100644 --- a/runtime/src/transaction_extensions.rs +++ b/runtime/src/transaction_extensions.rs @@ -2,12 +2,17 @@ use crate::*; use codec::{Decode, DecodeWithMemTracking, Encode}; use core::marker::PhantomData; -use frame_support::pallet_prelude::{InvalidTransaction, ValidTransaction}; - +use frame_support::pallet_prelude::{ + InvalidTransaction, TransactionValidityError, ValidTransaction, +}; use frame_system::ensure_signed; +use qp_wormhole::TransferProofRecorder; use scale_info::TypeInfo; use sp_core::Get; -use sp_runtime::{traits::TransactionExtension, Weight}; +use sp_runtime::{ + traits::{DispatchInfoOf, PostDispatchInfoOf, StaticLookup, TransactionExtension}, + DispatchResult, Weight, +}; /// Transaction extension for reversible accounts /// @@ -45,7 +50,7 @@ impl _call: &RuntimeCall, _info: &sp_runtime::traits::DispatchInfoOf, _len: usize, - ) -> Result { + ) -> Result { Ok(()) } @@ -59,11 +64,8 @@ impl _inherited_implication: &impl sp_runtime::traits::Implication, _source: frame_support::pallet_prelude::TransactionSource, ) -> sp_runtime::traits::ValidateResult { - let who = ensure_signed(origin.clone()).map_err(|_| { - frame_support::pallet_prelude::TransactionValidityError::Invalid( - InvalidTransaction::BadSigner, - ) - })?; + let who = ensure_signed(origin.clone()) + .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::BadSigner))?; if ReversibleTransfers::is_high_security(&who).is_some() { // High-security accounts can only call schedule_transfer and cancel @@ -80,9 +82,7 @@ impl return Ok((ValidTransaction::default(), (), origin)); }, _ => { - return Err(frame_support::pallet_prelude::TransactionValidityError::Invalid( - InvalidTransaction::Custom(1), - )); + return Err(TransactionValidityError::Invalid(InvalidTransaction::Custom(1))); }, } } @@ -91,6 +91,165 @@ impl } } +/// Details of a transfer to be recorded +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TransferDetails { + from: AccountId, + to: AccountId, + amount: Balance, + asset_id: AssetId, +} + +/// Transaction extension that records transfer proofs in the wormhole pallet +/// +/// This extension: +/// - Extracts transfer details from balance/asset transfer calls +/// - Records proofs in wormhole storage after successful execution +/// - Increments transfer count +/// - Emits events +/// - Fails the transaction if proof recording fails +#[derive(Encode, Decode, Clone, Eq, PartialEq, Default, TypeInfo, Debug, DecodeWithMemTracking)] +#[scale_info(skip_type_params(T))] +pub struct WormholeProofRecorderExtension(PhantomData); + +impl WormholeProofRecorderExtension { + /// Creates new extension + pub fn new() -> Self { + Self(PhantomData) + } + + /// Helper to convert lookup errors to transaction validity errors + fn lookup(address: &Address) -> Result { + ::Lookup::lookup(address.clone()) + .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::BadSigner)) + } + + /// Extract transfer details from a runtime call + fn extract_transfer_details( + origin: &RuntimeOrigin, + call: &RuntimeCall, + ) -> Result, TransactionValidityError> { + // Only process signed transactions + let who = match ensure_signed(origin.clone()) { + Ok(signer) => signer, + Err(_) => return Ok(None), + }; + + let details = match call { + // Native balance transfers + RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive { dest, value }) => { + let to = Self::lookup(dest)?; + Some(TransferDetails { from: who, to, amount: *value, asset_id: 0 }) + }, + RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death { dest, value }) => { + let to = Self::lookup(dest)?; + Some(TransferDetails { from: who, to, amount: *value, asset_id: 0 }) + }, + RuntimeCall::Balances(pallet_balances::Call::transfer_all { .. }) => None, + + // Asset transfers + RuntimeCall::Assets(pallet_assets::Call::transfer { id, target, amount }) => { + let to = Self::lookup(target)?; + Some(TransferDetails { asset_id: id.0, from: who, to, amount: *amount }) + }, + RuntimeCall::Assets(pallet_assets::Call::transfer_keep_alive { + id, + target, + amount, + }) => { + let to = Self::lookup(target)?; + Some(TransferDetails { asset_id: id.0, from: who, to, amount: *amount }) + }, + + _ => None, + }; + + Ok(details) + } + + /// Record the transfer proof using the TransferProofRecorder trait + fn record_proof(details: TransferDetails) -> Result<(), TransactionValidityError> { + let asset_id = if details.asset_id == 0 { None } else { Some(details.asset_id) }; + + >::record_transfer_proof( + asset_id, + details.from, + details.to, + details.amount, + ) + .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Custom(100))) + } +} + +impl TransactionExtension + for WormholeProofRecorderExtension +{ + type Pre = Option; + type Val = (); + type Implicit = (); + + const IDENTIFIER: &'static str = "WormholeProofRecorderExtension"; + + fn weight(&self, call: &RuntimeCall) -> Weight { + // Account for proof recording in post_dispatch + match call { + RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive { .. }) | + RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death { .. }) | + RuntimeCall::Assets(pallet_assets::Call::transfer { .. }) | + RuntimeCall::Assets(pallet_assets::Call::transfer_keep_alive { .. }) => { + // 2 writes: TransferProof insert + TransferCount update + // 1 read: TransferCount get + T::DbWeight::get().reads_writes(1, 2) + }, + _ => Weight::zero(), + } + } + + fn prepare( + self, + _val: Self::Val, + origin: &sp_runtime::traits::DispatchOriginOf, + call: &RuntimeCall, + _info: &sp_runtime::traits::DispatchInfoOf, + _len: usize, + ) -> Result { + // Extract transfer details to pass to post_dispatch + Self::extract_transfer_details(origin, call) + } + + fn validate( + &self, + _origin: sp_runtime::traits::DispatchOriginOf, + _call: &RuntimeCall, + _info: &DispatchInfoOf, + _len: usize, + _self_implicit: Self::Implicit, + _inherited_implication: &impl sp_runtime::traits::Implication, + _source: frame_support::pallet_prelude::TransactionSource, + ) -> sp_runtime::traits::ValidateResult { + // No validation needed - just return Ok + Ok((ValidTransaction::default(), (), _origin)) + } + + fn post_dispatch( + pre: Self::Pre, + _info: &DispatchInfoOf, + post_info: &mut PostDispatchInfoOf, + _len: usize, + _result: &DispatchResult, + ) -> Result<(), TransactionValidityError> { + // Only record proof if the transaction succeeded (no error in post_info) + if post_info.actual_weight.is_some() || _result.is_ok() { + if let Some(details) = pre { + // Record the proof - if this fails, fail the whole transaction + Self::record_proof(details)?; + } + } + + Ok(()) + } +} + #[cfg(test)] mod tests { use super::*; @@ -117,6 +276,7 @@ mod tests { (bob(), EXISTENTIAL_DEPOSIT * 2), (charlie(), EXISTENTIAL_DEPOSIT * 100), ], + dev_accounts: None, } .assimilate_storage(&mut t) .unwrap(); @@ -326,8 +486,73 @@ mod tests { RuntimeCall::ReversibleTransfers(pallet_reversible_transfers::Call::cancel { tx_id: sp_core::H256::default(), }); - // High-security accounts can call cancel assert_ok!(check_call(call)); }); } + + #[test] + fn wormhole_proof_recorder_native_transfer() { + new_test_ext().execute_with(|| { + let alice_origin = RuntimeOrigin::signed(alice()); + let call = RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive { + dest: MultiAddress::Id(bob()), + value: 100 * UNIT, + }); + + let details = WormholeProofRecorderExtension::::extract_transfer_details( + &alice_origin, + &call, + ) + .unwrap(); + + assert!(details.is_some()); + let details = details.unwrap(); + assert_eq!(details.from, alice()); + assert_eq!(details.to, bob()); + assert_eq!(details.amount, 100 * UNIT); + assert_eq!(details.asset_id, 0); + }); + } + + #[test] + fn wormhole_proof_recorder_asset_transfer() { + new_test_ext().execute_with(|| { + let alice_origin = RuntimeOrigin::signed(alice()); + let asset_id = 42u32; + let call = RuntimeCall::Assets(pallet_assets::Call::transfer { + id: codec::Compact(asset_id), + target: MultiAddress::Id(bob()), + amount: 500, + }); + + let details = WormholeProofRecorderExtension::::extract_transfer_details( + &alice_origin, + &call, + ) + .unwrap(); + + assert!(details.is_some()); + let details = details.unwrap(); + assert_eq!(details.from, alice()); + assert_eq!(details.to, bob()); + assert_eq!(details.amount, 500); + assert_eq!(details.asset_id, asset_id); + }); + } + + #[test] + fn wormhole_proof_recorder_ignores_non_transfer() { + new_test_ext().execute_with(|| { + let alice_origin = RuntimeOrigin::signed(alice()); + let call = RuntimeCall::System(frame_system::Call::remark { remark: vec![1, 2, 3] }); + + let details = WormholeProofRecorderExtension::::extract_transfer_details( + &alice_origin, + &call, + ) + .unwrap(); + + assert!(details.is_none()); + }); + } } diff --git a/runtime/tests/governance/treasury.rs b/runtime/tests/governance/treasury.rs index 3b4fe933..3218957a 100644 --- a/runtime/tests/governance/treasury.rs +++ b/runtime/tests/governance/treasury.rs @@ -88,9 +88,12 @@ mod tests { pub fn build(self) -> sp_io::TestExternalities { let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); - pallet_balances::GenesisConfig:: { balances: self.balances } - .assimilate_storage(&mut t) - .unwrap(); + pallet_balances::GenesisConfig:: { + balances: self.balances, + dev_accounts: None, + } + .assimilate_storage(&mut t) + .unwrap(); // Pallet Treasury genesis (optional, as we fund it manually) // If your pallet_treasury::GenesisConfig needs setup, do it here. From 365eb946862d6340c6d8e0d0f012dc3c0e1920eb Mon Sep 17 00:00:00 2001 From: Dastan <88332432+dastansam@users.noreply.github.com> Date: Wed, 14 Jan 2026 22:53:51 +0600 Subject: [PATCH 05/27] Use `ToFelts` trait in the wormhole pallet (#347) * Apply ToFelts changes to wormhole * Fix checks * Passing tests * Revert unit test line * Rename explicit AccountId --- Cargo.lock | 8 +-- Cargo.toml | 4 +- pallets/wormhole/src/lib.rs | 128 +++++++++++++++++++++++----------- pallets/wormhole/src/mock.rs | 1 + pallets/wormhole/src/tests.rs | 2 +- runtime/src/configs/mod.rs | 3 +- 6 files changed, 98 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9bfabe60..35ec3be9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8984,9 +8984,9 @@ dependencies = [ [[package]] name = "qp-poseidon" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5029d1c8223c0312a0247ebc745c5b09622dcbebe104f0fdb9de358b87ef032a" +checksum = "fef3bb816fc51b4ffc4524fa03376037d2531b17e2349ef6489a417cef029b3a" dependencies = [ "log", "p3-field", @@ -9016,9 +9016,9 @@ dependencies = [ [[package]] name = "qp-poseidon-core" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71dd1bf5d2997abf70247fcd23c8f04d7093b1faf33b775a42fb00c07e0a0e05" +checksum = "e33b3fb9032ac9b197265da8fc8bdedd21ba76592ceefbf4a591286d47baec2d" dependencies = [ "p3-field", "p3-goldilocks", diff --git a/Cargo.toml b/Cargo.toml index 4878ed36..b954d7e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -149,8 +149,8 @@ sp-consensus-qpow = { path = "./primitives/consensus/qpow", default-features = f # Quantus network dependencies qp-plonky2 = { version = "1.1.3", default-features = false } -qp-poseidon = { version = "1.0.5", default-features = false } -qp-poseidon-core = { version = "1.0.5", default-features = false, features = ["p2", "p3"] } +qp-poseidon = { version = "1.0.6", default-features = false } +qp-poseidon-core = { version = "1.0.6", default-features = false, features = ["p2", "p3"] } qp-rusty-crystals-dilithium = { version = "2.0.0", default-features = false } qp-rusty-crystals-hdwallet = { version = "1.0.0" } qp-wormhole-circuit = { version = "0.1.7", default-features = false } diff --git a/pallets/wormhole/src/lib.rs b/pallets/wormhole/src/lib.rs index ec635407..2abd1cb2 100644 --- a/pallets/wormhole/src/lib.rs +++ b/pallets/wormhole/src/lib.rs @@ -4,11 +4,11 @@ extern crate alloc; use core::marker::PhantomData; -use codec::{Decode, Encode, MaxEncodedLen}; +use codec::{Decode, MaxEncodedLen}; use frame_support::StorageHasher; use lazy_static::lazy_static; pub use pallet::*; -use qp_poseidon::PoseidonHasher as PoseidonCore; +pub use qp_poseidon::{PoseidonHasher as PoseidonCore, ToFelts}; use qp_wormhole_verifier::WormholeVerifier; #[cfg(test)] @@ -35,17 +35,16 @@ pub fn get_wormhole_verifier() -> Result<&'static WormholeVerifier, &'static str WORMHOLE_VERIFIER.as_ref().ok_or("Wormhole verifier not available") } -pub struct PoseidonStorageHasher(PhantomData); +// We use a generic struct so we can pass the specific Key type to the hasher +pub struct PoseidonStorageHasher(PhantomData); -impl StorageHasher - for PoseidonStorageHasher -{ +impl StorageHasher for PoseidonStorageHasher { // We are lying here, but maybe it's ok because it's just metadata const METADATA: StorageHasherIR = StorageHasherIR::Identity; type Output = [u8; 32]; fn hash(x: &[u8]) -> Self::Output { - PoseidonCore::hash_storage::(x) + PoseidonCore::hash_storage::(x) } fn max_len() -> usize { @@ -55,10 +54,11 @@ impl StorageHasher #[frame_support::pallet] pub mod pallet { - use crate::{PoseidonStorageHasher, WeightInfo}; + use crate::{PoseidonStorageHasher, ToFelts, WeightInfo}; use alloc::vec::Vec; use codec::Decode; use frame_support::{ + dispatch::DispatchResult, pallet_prelude::*, traits::{ fungible::{Mutate, Unbalanced}, @@ -73,7 +73,7 @@ pub mod pallet { use qp_wormhole_verifier::ProofWithPublicInputs; use qp_zk_circuits_common::circuit::{C, D, F}; use sp_runtime::{ - traits::{Saturating, StaticLookup, Zero}, + traits::{MaybeDisplay, Saturating, StaticLookup, Zero}, Perbill, }; @@ -87,25 +87,33 @@ pub mod pallet { >>::Balance; pub type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; + pub type TransferProofKey = ( + AssetIdOf, + ::TransferCount, + ::WormholeAccountId, + ::WormholeAccountId, + BalanceOf, + ); + #[pallet::pallet] pub struct Pallet(_); #[pallet::config] pub trait Config: frame_system::Config where - AssetIdOf: Default + From + Clone, - BalanceOf: Default, + AssetIdOf: Default + From + Clone + ToFelts, + BalanceOf: Default + ToFelts, AssetBalanceOf: Into> + From>, { /// Currency type used for native token transfers and minting - type Currency: Mutate> - + Unbalanced - + Currency; + type Currency: Mutate<::AccountId, Balance = BalanceOf> + + Unbalanced<::AccountId> + + Currency<::AccountId>; /// Assets type used for managing fungible assets - type Assets: fungibles::Inspect - + fungibles::Mutate - + fungibles::Create; + type Assets: fungibles::Inspect<::AccountId> + + fungibles::Mutate<::AccountId> + + fungibles::Create<::AccountId>; /// Transfer count type used in storage type TransferCount: Parameter @@ -113,16 +121,29 @@ pub mod pallet { + Default + Saturating + Copy - + sp_runtime::traits::One; + + sp_runtime::traits::One + + ToFelts; /// Account ID used as the "from" account when creating transfer proofs for minted tokens #[pallet::constant] - type MintingAccount: Get; + type MintingAccount: Get<::AccountId>; /// Weight information for pallet operations. type WeightInfo: WeightInfo; type WeightToFee: WeightToFee>; + + /// Override system AccountId to make it felts encodable + type WormholeAccountId: Parameter + + Member + + MaybeSerializeDeserialize + + core::fmt::Debug + + MaybeDisplay + + Ord + + MaxEncodedLen + + ToFelts + + Into<::AccountId> + + From<::AccountId>; } #[pallet::storage] @@ -135,8 +156,8 @@ pub mod pallet { #[pallet::getter(fn transfer_proof)] pub type TransferProof = StorageMap< _, - PoseidonStorageHasher, - (AssetIdOf, T::TransferCount, T::AccountId, T::AccountId, BalanceOf), /* (asset_id, tx_count, from, to, amount) */ + PoseidonStorageHasher>, + TransferProofKey, (), OptionQuery, >; @@ -149,22 +170,22 @@ pub mod pallet { #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { - ProofVerified { - exit_amount: BalanceOf, - }, NativeTransferred { - from: T::AccountId, - to: T::AccountId, + from: ::AccountId, + to: ::AccountId, amount: BalanceOf, transfer_count: T::TransferCount, }, AssetTransferred { asset_id: AssetIdOf, - from: T::AccountId, - to: T::AccountId, + from: ::AccountId, + to: ::AccountId, amount: AssetBalanceOf, transfer_count: T::TransferCount, }, + ProofVerified { + exit_amount: BalanceOf, + }, } #[pallet::error] @@ -246,8 +267,9 @@ pub mod pallet { // Decode exit account from public inputs let exit_account_bytes = *public_inputs.exit_account; - let exit_account = T::AccountId::decode(&mut &exit_account_bytes[..]) - .map_err(|_| Error::::InvalidPublicInputs)?; + let exit_account = + ::AccountId::decode(&mut &exit_account_bytes[..]) + .map_err(|_| Error::::InvalidPublicInputs)?; // Extract asset_id from public inputs let asset_id_u32 = public_inputs.asset_id; @@ -305,7 +327,12 @@ pub mod pallet { // Create a transfer proof for the minted tokens let mint_account = T::MintingAccount::get(); - Self::record_transfer(asset_id, mint_account, exit_account, exit_balance)?; + Self::record_transfer( + asset_id, + mint_account.into(), + exit_account.into(), + exit_balance, + )?; // Emit event Self::deposit_event(Event::ProofVerified { exit_amount: exit_balance }); @@ -331,7 +358,7 @@ pub mod pallet { >::transfer(&source, &dest, amount, Preservation::Expendable)?; // Store proof with asset_id = Default (0 for native) - Self::record_transfer(AssetIdOf::::default(), source, dest, amount)?; + Self::record_transfer(AssetIdOf::::default(), source.into(), dest.into(), amount)?; Ok(()) } @@ -367,7 +394,7 @@ pub mod pallet { )?; // Store proof - Self::record_transfer(asset_id, source, dest, amount.into())?; + Self::record_transfer(asset_id, source.into(), dest.into(), amount.into())?; Ok(()) } @@ -379,31 +406,52 @@ pub mod pallet { /// This should be called by transaction extensions or other runtime components pub fn record_transfer( asset_id: AssetIdOf, - from: T::AccountId, - to: T::AccountId, + from: ::WormholeAccountId, + to: ::WormholeAccountId, amount: BalanceOf, ) -> DispatchResult { let current_count = TransferCount::::get(); TransferProof::::insert( - (asset_id, current_count, from.clone(), to.clone(), amount), + (asset_id.clone(), current_count, from.clone(), to.clone(), amount), (), ); TransferCount::::put(current_count.saturating_add(T::TransferCount::one())); + if asset_id == AssetIdOf::::default() { + Self::deposit_event(Event::::NativeTransferred { + from: from.into(), + to: to.into(), + amount, + transfer_count: current_count, + }); + } else { + Self::deposit_event(Event::::AssetTransferred { + from: from.into(), + to: to.into(), + asset_id, + amount: amount.into(), + transfer_count: current_count, + }); + } + Ok(()) } } // Implement the TransferProofRecorder trait for other pallets to use - impl qp_wormhole::TransferProofRecorder, BalanceOf> - for Pallet + impl + qp_wormhole::TransferProofRecorder< + ::WormholeAccountId, + AssetIdOf, + BalanceOf, + > for Pallet { type Error = DispatchError; fn record_transfer_proof( asset_id: Option>, - from: T::AccountId, - to: T::AccountId, + from: ::WormholeAccountId, + to: ::WormholeAccountId, amount: BalanceOf, ) -> Result<(), Self::Error> { let asset_id_value = asset_id.unwrap_or_default(); diff --git a/pallets/wormhole/src/mock.rs b/pallets/wormhole/src/mock.rs index 7f0a4024..8527f4ff 100644 --- a/pallets/wormhole/src/mock.rs +++ b/pallets/wormhole/src/mock.rs @@ -127,6 +127,7 @@ impl pallet_wormhole::Config for Test { type Assets = Assets; type TransferCount = u64; type MintingAccount = MintingAccount; + type WormholeAccountId = AccountId; } // Helper function to build a genesis configuration diff --git a/pallets/wormhole/src/tests.rs b/pallets/wormhole/src/tests.rs index 74054d08..2a706be8 100644 --- a/pallets/wormhole/src/tests.rs +++ b/pallets/wormhole/src/tests.rs @@ -73,7 +73,7 @@ mod wormhole_tests { let event_transfer_count = 0u64; - let leaf_hash = PoseidonHasher::hash_storage::( + let leaf_hash = PoseidonHasher::hash_storage::>( &( 0u32, event_transfer_count, diff --git a/runtime/src/configs/mod.rs b/runtime/src/configs/mod.rs index c50898d8..5b2e7ec8 100644 --- a/runtime/src/configs/mod.rs +++ b/runtime/src/configs/mod.rs @@ -57,7 +57,7 @@ use qp_poseidon::PoseidonHasher; use qp_scheduler::BlockNumberOrTimestamp; use sp_runtime::{ traits::{AccountIdConversion, ConvertInto, One}, - FixedU128, Perbill, Permill, + AccountId32, FixedU128, Perbill, Permill, }; use sp_version::RuntimeVersion; @@ -635,5 +635,6 @@ impl pallet_wormhole::Config for Runtime { type Currency = Balances; type Assets = Assets; type TransferCount = u64; + type WormholeAccountId = AccountId32; type WeightToFee = IdentityFee; } From 1de498791eba47548c8ac8c2750b98d788ca2a1d Mon Sep 17 00:00:00 2001 From: Dastan <88332432+dastansam@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:39:48 +0600 Subject: [PATCH 06/27] Add ValidateUnsigned impl to wormhole (#353) --- pallets/wormhole/src/lib.rs | 44 +++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/pallets/wormhole/src/lib.rs b/pallets/wormhole/src/lib.rs index 2abd1cb2..ff0bf0e7 100644 --- a/pallets/wormhole/src/lib.rs +++ b/pallets/wormhole/src/lib.rs @@ -74,6 +74,10 @@ pub mod pallet { use qp_zk_circuits_common::circuit::{C, D, F}; use sp_runtime::{ traits::{MaybeDisplay, Saturating, StaticLookup, Zero}, + transaction_validity::{ + InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity, + ValidTransaction, + }, Perbill, }; @@ -400,6 +404,46 @@ pub mod pallet { } } + #[pallet::validate_unsigned] + impl ValidateUnsigned for Pallet + where + AssetIdOf: Default + From + Clone + ToFelts, + BalanceOf: Default + ToFelts, + AssetBalanceOf: Into> + From>, + { + type Call = Call; + + fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity { + match call { + Call::verify_wormhole_proof { proof_bytes } => { + // Basic validation: check proof bytes are not empty + if proof_bytes.is_empty() { + return InvalidTransaction::Custom(1).into(); + } + ValidTransaction::with_tag_prefix("WormholeVerify") + .and_provides(sp_io::hashing::blake2_256(proof_bytes)) + .priority(TransactionPriority::MAX) + .longevity(5) + .propagate(true) + .build() + }, + Call::verify_aggregated_proof { proof_bytes } => { + // Basic validation: check proof bytes are not empty + if proof_bytes.is_empty() { + return InvalidTransaction::Custom(2).into(); + } + ValidTransaction::with_tag_prefix("WormholeAggregatedVerify") + .and_provides(sp_io::hashing::blake2_256(proof_bytes)) + .priority(TransactionPriority::MAX) + .longevity(5) + .propagate(true) + .build() + }, + _ => InvalidTransaction::Call.into(), + } + } + } + // Helper functions for recording transfer proofs impl Pallet { /// Record a transfer proof From 426041e4ca310d9d67490a37dd79a529133035a1 Mon Sep 17 00:00:00 2001 From: Dastan <88332432+dastansam@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:45:19 +0600 Subject: [PATCH 07/27] feat: aggregated proof verification in wormhole (#351) * Aggregated proofs verification wormhole * clippy --- pallets/wormhole/aggregated_common.bin | Bin 0 -> 1905 bytes pallets/wormhole/aggregated_verifier.bin | Bin 0 -> 552 bytes pallets/wormhole/src/lib.rs | 141 ++++++++++++++++++++++- pallets/wormhole/src/weights.rs | 22 ++++ 4 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 pallets/wormhole/aggregated_common.bin create mode 100644 pallets/wormhole/aggregated_verifier.bin diff --git a/pallets/wormhole/aggregated_common.bin b/pallets/wormhole/aggregated_common.bin new file mode 100644 index 0000000000000000000000000000000000000000..5fbea0f3cae044a80d0ad7def777f01eaf656179 GIT binary patch literal 1905 zcmbuAe^Aq99LG1Iw8@Wwj9>9%L*`-Ix`7H!2wHG}8jBm^DY46--C-GNa)JXnY%V5f zK^hDhNP%#+t{)2{y#zMc?34sn3ezwm2&21U3R1Sbc-HgoxcaN>x%=Mh{eGVBeLv6V zyXX5}CF8G=Nx6*j#eet})H4}wRJl`zP_h@v#NMmYw#idBboGm5Y2+v zEr`M(x&j}j&jXiJ58|bPkI;3HMLoEZdeDP<(35)5i+a$TdJr$nht31oFNpnt*bj*M zAnJjr1NzcJpda<1KlNZB^y?Y_;AFiS*>yg(I5PYoeP5?e?5%t&?Z{_^o#u)i*{{RBUbeGSe$X?{>wn zW9YcSk-GN7oNb2t4U-xC0a3WY-4Lv)SDdm>=x2HhuZzc?yU-if;x;WRQ#90ej7ip% ziEd7fjL+Qao!@@$Qqd-TVY)K5tF^g*Tbr+fH^IEyxoOL+F{6D|rs`CMbHtMuSCHXT z9(dsNM~A$ll=(U0D9(sBeEnol=*GwGPbahpxj(X%X5+EY#N<7@L?^O_zC6@ZlANbJKe4?D2bu3p#Tb)A{=Tf=3)0X=_oC$5CVu=0QYN@=LQDu60{m*|4{M2Xu zomF^KysK{Q#0z=CtmfT{ZVk^-5moP&lszbYW?_p-cCA=$PSZ2{;wlZDPu|wsr%u2B zt9r!LVfpsr(b22U@$t$B167d$DtCRUaQ6y}`bL_VbdiD_VQA@0DZ$@kxsp2EIe&h--eSVG{HAsb6YbD|kMG ztiZTdkesd+7$$Op)FB$VLNv0D=%x9B?T~qLkwJ)TBN}-_G%}88z5a$)`k+E?!pk^>&gHRX$Ingu`y8n-c zJ4+g-gc+nLhyn3)HX@t(d~8{_e(RT+vai%?7x2){Ce*}7_VylH$PGOLr)tkX#p})s zr7qItfqi#*J=?eEk2o=|CDSY_n{R`|O1U8T??>5(E688U=+Uj_pQxX(RL~e&kD1Pi z_i`~Yi$@m0KytTd7b+bf7E}nZ_#UPwlzOM)`cxy{9CBMxTGcWQxCcW?M<{4g+(VC~ z4v$C+4f{RIosj!{M*%kuv>w@IFe22t-faOfu;e#vsJH)K`f9dloD`s(J){@l1q|hK qi>>->!lCIbJkX = { + let verifier_bytes = include_bytes!("../aggregated_verifier.bin"); + let common_bytes = include_bytes!("../aggregated_common.bin"); + WormholeVerifier::new_from_bytes(verifier_bytes, common_bytes).ok() + }; } // Add a safe getter function @@ -35,6 +40,11 @@ pub fn get_wormhole_verifier() -> Result<&'static WormholeVerifier, &'static str WORMHOLE_VERIFIER.as_ref().ok_or("Wormhole verifier not available") } +// Getter for aggregated verifier +pub fn get_aggregated_verifier() -> Result<&'static WormholeVerifier, &'static str> { + AGGREGATED_VERIFIER.as_ref().ok_or("Aggregated verifier not available") +} + // We use a generic struct so we can pass the specific Key type to the hasher pub struct PoseidonStorageHasher(PhantomData); @@ -69,7 +79,7 @@ pub mod pallet { weights::WeightToFee, }; use frame_system::pallet_prelude::*; - use qp_wormhole_circuit::inputs::PublicCircuitInputs; + use qp_wormhole_circuit::inputs::{AggregatedPublicCircuitInputs, PublicCircuitInputs}; use qp_wormhole_verifier::ProofWithPublicInputs; use qp_zk_circuits_common::circuit::{C, D, F}; use sp_runtime::{ @@ -206,6 +216,10 @@ pub mod pallet { InvalidBlockNumber, AssetNotFound, SelfTransfer, + AggregatedVerifierNotAvailable, + AggregatedProofDeserializationFailed, + AggregatedVerificationFailed, + InvalidAggregatedPublicInputs, } #[pallet::call] @@ -237,8 +251,7 @@ pub mod pallet { ); // Extract the block number from public inputs - let block_number = BlockNumberFor::::try_from(public_inputs.block_number) - .map_err(|_| Error::::InvalidPublicInputs)?; + let block_number = BlockNumberFor::::from(public_inputs.block_number); // Get the block hash for the specified block number let block_hash = frame_system::Pallet::::block_hash(block_number); @@ -402,6 +415,128 @@ pub mod pallet { Ok(()) } + + /// Verify an aggregated wormhole proof and process all transfers in the batch + #[pallet::call_index(3)] + #[pallet::weight(::WeightInfo::verify_aggregated_proof())] + pub fn verify_aggregated_proof( + origin: OriginFor, + proof_bytes: Vec, + ) -> DispatchResult { + ensure_none(origin)?; + + let verifier = crate::get_aggregated_verifier() + .map_err(|_| Error::::AggregatedVerifierNotAvailable)?; + + let proof = ProofWithPublicInputs::::from_bytes( + proof_bytes, + &verifier.circuit_data.common, + ) + .map_err(|_| Error::::AggregatedProofDeserializationFailed)?; + + // Parse aggregated public inputs + let aggregated_inputs = + AggregatedPublicCircuitInputs::try_from_slice(&proof.public_inputs) + .map_err(|_| Error::::InvalidAggregatedPublicInputs)?; + + // Verify all nullifiers haven't been used + for nullifier in &aggregated_inputs.nullifiers { + let nullifier_bytes: [u8; 32] = (*nullifier) + .as_ref() + .try_into() + .map_err(|_| Error::::InvalidAggregatedPublicInputs)?; + ensure!( + !UsedNullifiers::::contains_key(nullifier_bytes), + Error::::NullifierAlreadyUsed + ); + } + + // Note: We don't verify individual block hashes on-chain because: + // 1. Aggregated proofs span multiple blocks + // 2. The ZK circuit cryptographically guarantees block chain connectivity through + // parent_hash linkage verification + // 3. Old block hashes may not be available on-chain (BlockHashCount limit) + // The proof verification itself is sufficient to guarantee validity. + + // Verify the aggregated proof + verifier + .verify(proof.clone()) + .map_err(|_| Error::::AggregatedVerificationFailed)?; + + // Mark all nullifiers as used + for nullifier in &aggregated_inputs.nullifiers { + let nullifier_bytes: [u8; 32] = (*nullifier) + .as_ref() + .try_into() + .map_err(|_| Error::::InvalidAggregatedPublicInputs)?; + UsedNullifiers::::insert(nullifier_bytes, true); + } + + // For now, aggregated proofs only support native token (asset_id = 0) + // TODO: Add asset_id support when the circuit is updated + let asset_id = AssetIdOf::::default(); + + // Get the minting account for recording transfer proofs + let mint_account = T::MintingAccount::get(); + + // Process each exit account + for account_data in &aggregated_inputs.account_data { + // Convert funding amount to Balance type + let exit_balance: BalanceOf = account_data + .summed_funding_amount + .try_into() + .map_err(|_| Error::::InvalidAggregatedPublicInputs)?; + + // Decode exit account from public inputs + let exit_account_bytes: [u8; 32] = (*account_data.exit_account) + .as_ref() + .try_into() + .map_err(|_| Error::::InvalidAggregatedPublicInputs)?; + let exit_account = + ::AccountId::decode(&mut &exit_account_bytes[..]) + .map_err(|_| Error::::InvalidAggregatedPublicInputs)?; + + // Calculate fees + let weight = ::WeightInfo::verify_wormhole_proof(); + let weight_fee = T::WeightToFee::weight_to_fee(&weight); + let volume_fee_perbill = Perbill::from_rational(1u32, 1000u32); + let volume_fee = volume_fee_perbill * exit_balance; + let total_fee = weight_fee.saturating_add(volume_fee); + + // Native token transfer - mint tokens to the exit account + // Note: Aggregated proofs currently only support native tokens (asset_id = 0) + // TODO: Add asset support when the circuit includes asset_id in + // AggregatedPublicCircuitInputs + >::increase_balance( + &exit_account, + exit_balance, + frame_support::traits::tokens::Precision::Exact, + )?; + + // Withdraw fee from exit account if fees are non-zero + if !total_fee.is_zero() { + let _fee_imbalance = T::Currency::withdraw( + &exit_account, + total_fee, + WithdrawReasons::TRANSACTION_PAYMENT, + ExistenceRequirement::KeepAlive, + )?; + } + + // Record transfer proof for the minted tokens + Self::record_transfer( + asset_id.clone(), + mint_account.clone().into(), + exit_account.into(), + exit_balance, + )?; + + // Emit event for each exit account + Self::deposit_event(Event::ProofVerified { exit_amount: exit_balance }); + } + + Ok(()) + } } #[pallet::validate_unsigned] diff --git a/pallets/wormhole/src/weights.rs b/pallets/wormhole/src/weights.rs index 8d06df5b..7cc49ac4 100644 --- a/pallets/wormhole/src/weights.rs +++ b/pallets/wormhole/src/weights.rs @@ -53,6 +53,7 @@ use core::marker::PhantomData; /// Weight functions needed for `pallet_wormhole`. pub trait WeightInfo { fn verify_wormhole_proof() -> Weight; + fn verify_aggregated_proof() -> Weight; } /// Weights for `pallet_wormhole` using the Substrate node and recommended hardware. @@ -77,6 +78,18 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } + + /// Weight for verifying an aggregated proof. + /// This is a placeholder - proper benchmarking should be done. + /// Aggregated proofs verify multiple transfers at once, so the weight + /// should be higher than single proof verification. + fn verify_aggregated_proof() -> Weight { + // Placeholder: use 8x the single proof weight as a conservative estimate + // for the default tree configuration (branching_factor=2, depth=3 = 8 proofs) + Weight::from_parts(115_744_000_000, 28744) + .saturating_add(T::DbWeight::get().reads(32_u64)) + .saturating_add(T::DbWeight::get().writes(32_u64)) + } } // For backwards compatibility and tests. @@ -100,4 +113,13 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } + + /// Weight for verifying an aggregated proof. + /// This is a placeholder - proper benchmarking should be done. + fn verify_aggregated_proof() -> Weight { + // Placeholder: use 8x the single proof weight as a conservative estimate + Weight::from_parts(115_744_000_000, 28744) + .saturating_add(RocksDbWeight::get().reads(32_u64)) + .saturating_add(RocksDbWeight::get().writes(32_u64)) + } } From ee4616a8528eff341cebe6fff533d3e92d78e5e2 Mon Sep 17 00:00:00 2001 From: illuzen Date: Mon, 19 Jan 2026 15:22:20 +0800 Subject: [PATCH 08/27] check block hash in agg proof --- pallets/wormhole/src/lib.rs | 46 ++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/pallets/wormhole/src/lib.rs b/pallets/wormhole/src/lib.rs index 6289d310..88b65a3b 100644 --- a/pallets/wormhole/src/lib.rs +++ b/pallets/wormhole/src/lib.rs @@ -434,12 +434,17 @@ pub mod pallet { ) .map_err(|_| Error::::AggregatedProofDeserializationFailed)?; + // Verify the aggregated proof + verifier + .verify(proof.clone()) + .map_err(|_| Error::::AggregatedVerificationFailed)?; + // Parse aggregated public inputs let aggregated_inputs = AggregatedPublicCircuitInputs::try_from_slice(&proof.public_inputs) .map_err(|_| Error::::InvalidAggregatedPublicInputs)?; - // Verify all nullifiers haven't been used + // Verify all nullifiers haven't been used and then mark them as used for nullifier in &aggregated_inputs.nullifiers { let nullifier_bytes: [u8; 32] = (*nullifier) .as_ref() @@ -449,28 +454,31 @@ pub mod pallet { !UsedNullifiers::::contains_key(nullifier_bytes), Error::::NullifierAlreadyUsed ); + UsedNullifiers::::insert(nullifier_bytes, true); } - // Note: We don't verify individual block hashes on-chain because: - // 1. Aggregated proofs span multiple blocks - // 2. The ZK circuit cryptographically guarantees block chain connectivity through - // parent_hash linkage verification - // 3. Old block hashes may not be available on-chain (BlockHashCount limit) - // The proof verification itself is sufficient to guarantee validity. + // Convert block number from u32 to BlockNumberFor + let block_number = BlockNumberFor::::from(aggregated_inputs.block_data.block_number); - // Verify the aggregated proof - verifier - .verify(proof.clone()) - .map_err(|_| Error::::AggregatedVerificationFailed)?; + // Check if block number is not in the future + let current_block = frame_system::Pallet::::block_number(); + // TODO: is this check necessary? + ensure!(block_number <= current_block, Error::::InvalidBlockNumber); - // Mark all nullifiers as used - for nullifier in &aggregated_inputs.nullifiers { - let nullifier_bytes: [u8; 32] = (*nullifier) - .as_ref() - .try_into() - .map_err(|_| Error::::InvalidAggregatedPublicInputs)?; - UsedNullifiers::::insert(nullifier_bytes, true); - } + // Get the block hash for the specified block number + let block_hash = frame_system::Pallet::::block_hash(block_number); + + // Validate that the block exists by checking if it's not the default hash + // The default hash (all zeros) indicates the block doesn't exist + // TODO: is this check necessary? + let default_hash = T::Hash::default(); + ensure!(block_hash != default_hash, Error::::BlockNotFound); + + // Ensure that the block hash from storage matches the one in public inputs + ensure!( + block_hash.as_ref() == aggregated_inputs.block_data.block_hash.as_ref(), + Error::::InvalidPublicInputs + ); // For now, aggregated proofs only support native token (asset_id = 0) // TODO: Add asset_id support when the circuit is updated From 44e406bdb57e3286adbbc19b58edf2032b1c526e Mon Sep 17 00:00:00 2001 From: Ethan Cemer Date: Mon, 19 Jan 2026 01:24:24 -0600 Subject: [PATCH 09/27] feat: quantized funding amounts (#354) * feat/quantized_wormhole_funding_amount * *fix formatting * *rollback zk enabled circuit artfiact builds at runtime. * fmt --------- Co-authored-by: illuzen --- Cargo.lock | 51 +++++++++++++++++++--------------- Cargo.toml | 23 +++++++-------- node/build.rs | 6 ++-- pallets/wormhole/Cargo.toml | 11 ++++++-- pallets/wormhole/common.bin | Bin 1905 -> 1905 bytes pallets/wormhole/src/lib.rs | 7 ++++- pallets/wormhole/src/tests.rs | 13 +++++++-- pallets/wormhole/verifier.bin | Bin 552 -> 552 bytes runtime/Cargo.toml | 8 ++++-- 9 files changed, 72 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 35ec3be9..f8d3202c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7234,7 +7234,7 @@ dependencies = [ "pallet-timestamp", "parity-scale-codec", "primitive-types 0.13.1", - "qp-poseidon-core", + "qp-poseidon-core 1.0.6 (git+https://github.com/Quantus-Network/qp-poseidon?branch=feat%2Ffunding_amount_quantization)", "qpow-math", "scale-info", "sp-arithmetic", @@ -7617,7 +7617,7 @@ dependencies = [ "qp-header", "qp-plonky2", "qp-poseidon", - "qp-poseidon-core", + "qp-poseidon-core 1.0.6 (git+https://github.com/Quantus-Network/qp-poseidon?branch=feat%2Ffunding_amount_quantization)", "qp-wormhole", "qp-wormhole-circuit", "qp-wormhole-circuit-builder", @@ -8842,7 +8842,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ "heck 0.5.0", - "itertools 0.14.0", + "itertools 0.10.5", "log", "multimap", "once_cell", @@ -8875,7 +8875,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools 0.10.5", "proc-macro2", "quote", "syn 2.0.110", @@ -8927,7 +8927,7 @@ dependencies = [ "p3-goldilocks", "parity-scale-codec", "qp-poseidon", - "qp-poseidon-core", + "qp-poseidon-core 1.0.6 (git+https://github.com/Quantus-Network/qp-poseidon?branch=feat%2Ffunding_amount_quantization)", "scale-info", "serde", "serde_json", @@ -8985,14 +8985,13 @@ dependencies = [ [[package]] name = "qp-poseidon" version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef3bb816fc51b4ffc4524fa03376037d2531b17e2349ef6489a417cef029b3a" +source = "git+https://github.com/Quantus-Network/qp-poseidon?branch=feat%2Ffunding_amount_quantization#acd1eec5bf54abc1d07c7046c90a4bae339eeb84" dependencies = [ "log", "p3-field", "p3-goldilocks", "parity-scale-codec", - "qp-poseidon-core", + "qp-poseidon-core 1.0.6 (git+https://github.com/Quantus-Network/qp-poseidon?branch=feat%2Ffunding_amount_quantization)", "scale-info", "serde", "sp-core", @@ -9029,6 +9028,20 @@ dependencies = [ "rand_chacha 0.9.0", ] +[[package]] +name = "qp-poseidon-core" +version = "1.0.6" +source = "git+https://github.com/Quantus-Network/qp-poseidon?branch=feat%2Ffunding_amount_quantization#acd1eec5bf54abc1d07c7046c90a4bae339eeb84" +dependencies = [ + "p3-field", + "p3-goldilocks", + "p3-poseidon2", + "p3-symmetric", + "qp-plonky2", + "qp-poseidon-constants", + "rand_chacha 0.9.0", +] + [[package]] name = "qp-rusty-crystals-dilithium" version = "2.0.0" @@ -9051,7 +9064,7 @@ dependencies = [ "hex", "hex-literal", "nam-tiny-hderive", - "qp-poseidon-core", + "qp-poseidon-core 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "qp-rusty-crystals-dilithium", "rand_chacha 0.9.0", "rand_core 0.9.3", @@ -9077,21 +9090,18 @@ version = "0.1.0" [[package]] name = "qp-wormhole-circuit" version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcbccee20e314a1c52f36d8e78b1fa8205da46050c18da60d4964f41875bd4f6" +source = "git+https://github.com/Quantus-Network/qp-zk-circuits?branch=feat%2Fu32_funding_amount#507d2dcefd5fd7d87d42a57b49b8bd7e40200a0e" dependencies = [ "anyhow", "hex", "qp-plonky2", - "qp-poseidon-core", "qp-zk-circuits-common", ] [[package]] name = "qp-wormhole-circuit-builder" version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aec473c32d69e2e0941d192ab8cf8712761ee2e84d3829fc211087f53be15bf3" +source = "git+https://github.com/Quantus-Network/qp-zk-circuits?branch=feat%2Fu32_funding_amount#507d2dcefd5fd7d87d42a57b49b8bd7e40200a0e" dependencies = [ "anyhow", "qp-plonky2", @@ -9102,8 +9112,7 @@ dependencies = [ [[package]] name = "qp-wormhole-prover" version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fd377a2fa936e6edb069ffecbf41afe10803b342e5cda8ff3aab199f77fde5d" +source = "git+https://github.com/Quantus-Network/qp-zk-circuits?branch=feat%2Fu32_funding_amount#507d2dcefd5fd7d87d42a57b49b8bd7e40200a0e" dependencies = [ "anyhow", "qp-plonky2", @@ -9114,8 +9123,7 @@ dependencies = [ [[package]] name = "qp-wormhole-verifier" version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08e114bc7ad7a7589c502960abc570685b726993ccce05443577a9ddd8dc4ee1" +source = "git+https://github.com/Quantus-Network/qp-zk-circuits?branch=feat%2Fu32_funding_amount#507d2dcefd5fd7d87d42a57b49b8bd7e40200a0e" dependencies = [ "anyhow", "qp-plonky2", @@ -9126,13 +9134,12 @@ dependencies = [ [[package]] name = "qp-zk-circuits-common" version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445cc21c39959d1b553c4d8ea94d058ceab84cd70e5d47ec82b11535494cec46" +source = "git+https://github.com/Quantus-Network/qp-zk-circuits?branch=feat%2Fu32_funding_amount#507d2dcefd5fd7d87d42a57b49b8bd7e40200a0e" dependencies = [ "anyhow", "hex", "qp-plonky2", - "qp-poseidon-core", + "qp-poseidon-core 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "serde", ] @@ -9145,7 +9152,7 @@ dependencies = [ "num-bigint", "num-traits", "primitive-types 0.13.1", - "qp-poseidon-core", + "qp-poseidon-core 1.0.6 (git+https://github.com/Quantus-Network/qp-poseidon?branch=feat%2Ffunding_amount_quantization)", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b954d7e3..0b5a330b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -149,21 +149,18 @@ sp-consensus-qpow = { path = "./primitives/consensus/qpow", default-features = f # Quantus network dependencies qp-plonky2 = { version = "1.1.3", default-features = false } -qp-poseidon = { version = "1.0.6", default-features = false } -qp-poseidon-core = { version = "1.0.6", default-features = false, features = ["p2", "p3"] } +qp-poseidon = { git = "https://github.com/Quantus-Network/qp-poseidon", branch = "feat/funding_amount_quantization", default-features = false } +qp-poseidon-core = { git = "https://github.com/Quantus-Network/qp-poseidon", package = "qp-poseidon-core", branch = "feat/funding_amount_quantization", default-features = false, features = [ + "p2", + "p3", +] } qp-rusty-crystals-dilithium = { version = "2.0.0", default-features = false } qp-rusty-crystals-hdwallet = { version = "1.0.0" } -qp-wormhole-circuit = { version = "0.1.7", default-features = false } -qp-wormhole-circuit-builder = { version = "0.1.7", default-features = false } -qp-wormhole-prover = { version = "0.1.7", default-features = false, features = [ - "no_random", -] } -qp-wormhole-verifier = { version = "0.1.7", default-features = false, features = [ - "no_random", -] } -qp-zk-circuits-common = { version = "0.1.7", default-features = false, features = [ - "no_random", -] } +qp-wormhole-circuit = { git = "https://github.com/Quantus-Network/qp-zk-circuits", package = "qp-wormhole-circuit", branch = "feat/u32_funding_amount", default-features = false } +qp-wormhole-circuit-builder = { git = "https://github.com/Quantus-Network/qp-zk-circuits", package = "qp-wormhole-circuit-builder", branch = "feat/u32_funding_amount", default-features = false } +qp-wormhole-prover = { git = "https://github.com/Quantus-Network/qp-zk-circuits", package = "qp-wormhole-prover", branch = "feat/u32_funding_amount", default-features = false } +qp-wormhole-verifier = { git = "https://github.com/Quantus-Network/qp-zk-circuits", package = "qp-wormhole-verifier", branch = "feat/u32_funding_amount", default-features = false } +qp-zk-circuits-common = { git = "https://github.com/Quantus-Network/qp-zk-circuits", package = "qp-zk-circuits-common", branch = "feat/u32_funding_amount", default-features = false } # polkadot-sdk dependencies frame-benchmarking = { version = "41.0.0", default-features = false } diff --git a/node/build.rs b/node/build.rs index 6b47474f..31f70376 100644 --- a/node/build.rs +++ b/node/build.rs @@ -33,9 +33,9 @@ fn generate_circuit_binaries() { // Call the circuit-builder to generate binaries directly in the pallet directory // We don't need the prover binary for the chain, only verifier and common - // TODO: uncomment this once `no_random` issue is fixed in zk-circuits - // qp_wormhole_circuit_builder::generate_circuit_binaries("../pallets/wormhole", false) - // .expect("Failed to generate circuit binaries"); + + qp_wormhole_circuit_builder::generate_circuit_binaries("../pallets/wormhole", false) + .expect("Failed to generate circuit binaries"); println!("cargo:trace=✅ Circuit binaries generated successfully"); } diff --git a/pallets/wormhole/Cargo.toml b/pallets/wormhole/Cargo.toml index a1a1d064..2c2da565 100644 --- a/pallets/wormhole/Cargo.toml +++ b/pallets/wormhole/Cargo.toml @@ -20,8 +20,12 @@ qp-header = { workspace = true, features = ["serde"] } qp-poseidon.workspace = true qp-wormhole.workspace = true qp-wormhole-circuit = { workspace = true, default-features = false } -qp-wormhole-verifier = { workspace = true, default-features = false } -qp-zk-circuits-common = { workspace = true, default-features = false } +qp-wormhole-verifier = { workspace = true, default-features = false, features = [ + "no_random", +] } +qp-zk-circuits-common = { workspace = true, default-features = false, features = [ + "no_random", +] } scale-info = { workspace = true, default-features = false, features = [ "derive", ] } @@ -39,7 +43,8 @@ qp-poseidon-core.workspace = true qp-wormhole-circuit.workspace = true qp-wormhole-circuit-builder = { workspace = true, default-features = false } qp-wormhole-prover.workspace = true -qp-zk-circuits-common.workspace = true +qp-wormhole-verifier = { workspace = true, default-features = false } +qp-zk-circuits-common = { workspace = true, default-features = false } sp-state-machine = { path = "../../primitives/state-machine" } sp-trie.workspace = true diff --git a/pallets/wormhole/common.bin b/pallets/wormhole/common.bin index 5fbea0f3cae044a80d0ad7def777f01eaf656179..7890c51d4946a4235c5af2aa7372969afe09f679 100644 GIT binary patch delta 34 qcmey!_mOXcAtU3&c-x8ju1t)o6A$W4yuiKjtp*dL@MbaQXchq1-wRCu delta 32 ocmey!_mOXc;lwz*iTSQf45||k>P)=Az45IE6QkH>G3IC%0MaH4O#lD@ diff --git a/pallets/wormhole/src/lib.rs b/pallets/wormhole/src/lib.rs index 88b65a3b..260d732c 100644 --- a/pallets/wormhole/src/lib.rs +++ b/pallets/wormhole/src/lib.rs @@ -228,6 +228,10 @@ pub mod pallet { #[pallet::weight(::WeightInfo::verify_wormhole_proof())] pub fn verify_wormhole_proof(origin: OriginFor, proof_bytes: Vec) -> DispatchResult { ensure_none(origin)?; + // Note: The funding_amount in public inputs is expected to be quantized (i.e., scaled + // down from 12 to 2 decimals points of precision) so we need to scale it back up + // here to get the actual amount the chain expects with 12 decimal places of precision. + const SCALE_DOWN_FACTOR: u128 = 10_000_000_000; // 10^10; let verifier = crate::get_wormhole_verifier().map_err(|_| Error::::VerifierNotAvailable)?; @@ -276,7 +280,8 @@ pub mod pallet { // Mark nullifier as used UsedNullifiers::::insert(nullifier_bytes, true); - let exit_balance_u128 = public_inputs.funding_amount; + let exit_balance_u128 = + (public_inputs.funding_amount as u128).saturating_mul(SCALE_DOWN_FACTOR); // Convert to Balance type let exit_balance: BalanceOf = diff --git a/pallets/wormhole/src/tests.rs b/pallets/wormhole/src/tests.rs index 2a706be8..3454e681 100644 --- a/pallets/wormhole/src/tests.rs +++ b/pallets/wormhole/src/tests.rs @@ -20,16 +20,19 @@ mod wormhole_tests { utils::{digest_felts_to_bytes, BytesDigest, Digest}, }; use sp_runtime::{traits::Header, DigestItem}; + const SCALE_DOWN_FACTOR: u128 = 10_000_000_000; // 10^10; fn generate_proof(inputs: CircuitInputs) -> ProofWithPublicInputs { - let config = CircuitConfig::standard_recursion_config(); + let config = CircuitConfig::standard_recursion_zk_config(); let prover = WormholeProver::new(config); let prover_next = prover.commit(&inputs).expect("proof failed"); let proof = prover_next.prove().expect("valid proof"); proof } + // Ignoring for now, will fix once the no_random feature issue is resolved for test dependencies #[test] + #[ignore] fn test_wormhole_transfer_proof_generation() { let alice = account_id(1); let secret: BytesDigest = [1u8; 32].try_into().expect("valid secret"); @@ -118,6 +121,10 @@ mod wormhole_tests { let block_hash = header.hash(); + let funding_amount_quantized: u32 = (funding_amount / SCALE_DOWN_FACTOR as u128) + .try_into() + .expect("funding amount fits in u32 after scaling down"); + let circuit_inputs = CircuitInputs { private: PrivateCircuitInputs { secret, @@ -134,7 +141,7 @@ mod wormhole_tests { }, public: PublicCircuitInputs { asset_id: 0u32, - funding_amount, + funding_amount: funding_amount_quantized, nullifier: Nullifier::from_preimage(secret, event_transfer_count).hash.into(), exit_account: BytesDigest::try_from(exit_account_id.as_ref() as &[u8]) .expect("account is 32 bytes"), @@ -151,7 +158,7 @@ mod wormhole_tests { let public_inputs = PublicCircuitInputs::try_from(&proof).expect("failed to parse public inputs"); - assert_eq!(public_inputs.funding_amount, funding_amount); + assert_eq!(public_inputs.funding_amount, funding_amount_quantized); assert_eq!( public_inputs.exit_account, BytesDigest::try_from(exit_account_id.as_ref() as &[u8]).unwrap() diff --git a/pallets/wormhole/verifier.bin b/pallets/wormhole/verifier.bin index 229b59d97d5e8e140b16a1bc97e9549f0e4bc250..53dffca148097185ec20ee30223c2fa2466a7051 100644 GIT binary patch literal 552 zcmV+@0@wWn000000001+bd1qbfvYTdvH&*2(43A?Jm`=gbY4f60*zOeTN83#IsYoc z^z>v8o#$n^vlrngH;dc8ceMCZerJZ0H2$ZxN)$1SjFd zsyBCIT+tFckUbXSN02e4a7*mTNKm(Kc%J($nDO?JCNJ^8JuAzq4&kYeHpNKK7fPD! zw&iA-2!0y*jV;c+3fBAF#9k{V)btkzzaj)>U{`sJ(S#?d%|_KzeW2m|cRe}x($ArB z86~y1M!R+E)#A5 literal 552 zcmV+@0@wWn000000001mTjTr3B2agUHfFrUGtEY?c|aBKvf6rpL30{-^KRXXhj{y2 zz8rIqg$hj))SW(&TT7nI=r=es;;pp^jB9#{Wjfz5a$)`k+E?!pk^>&gHRX$Ingu`y8n-c zJ4+g-gc+nLhyn3)HX@t(d~8{_e(RT+vai%?7x2){Ce*}7_VylH$PGOLr)tkX#p})s zr7qItfqi#*J=?eEk2o=|CDSY_n{R`|O1U8T??>5(E688U=+Uj_pQxX(RL~e&kD1Pi z_i`~Yi$@m0KytTd7b+bf7E}nZ_#UPwlzOM)`cxy{9CBMxTGcWQxCcW?M<{4g+(VC~ z4v$C+4f{RIosj!{M*%kuv>w@IFe22t-faOfu;e#vsJH)K`f9dloD`s(J){@l1q|hK qi>>->!lCIbJkX Date: Mon, 19 Jan 2026 15:31:44 +0800 Subject: [PATCH 10/27] Enforce miner wormhole address (#344) * feat: qp-header for Planck release (#338) * no circuit padding hasher for block header * *use custom hasher for header that encodes the pre-image in a felt aligned manner. * *bespoke header hasher * *patch bug with hash header fall back * *replace custom poseidon header hasher on generic header with a fork of header that has a custom hasher that overrides default on the header trait. * *rmv commented out impl of prior hash method * Update primitives/header/src/lib.rs Co-authored-by: Dastan <88332432+dastansam@users.noreply.github.com> * fixed tests * Use inherent struct method * Update Cargo.toml --------- Co-authored-by: Ethan Co-authored-by: illuzen Co-authored-by: Dastan <88332432+dastansam@users.noreply.github.com> * Exponentially decaying token rewards (#340) * exponentially decaying token rewards * script to simulate emissions * clean up constants and switch python script to rust test * log if we hit max supply somehow * convert rewards_address to rewards_preimage to enforce wormhole address usage * better documentation * change arg name * Exponentially decaying token rewards (#340) * exponentially decaying token rewards * script to simulate emissions * clean up constants and switch python script to rust test * log if we hit max supply somehow * convert rewards_address to rewards_preimage to enforce wormhole address usage * better documentation * change arg name * address style comments --------- Co-authored-by: Cezary Olborski Co-authored-by: Ethan Co-authored-by: Dastan <88332432+dastansam@users.noreply.github.com> --- Cargo.lock | 2 + MINING.md | 124 ++++++-- README.md | 50 +--- client/consensus/qpow/src/lib.rs | 12 +- client/consensus/qpow/src/worker.rs | 6 +- node/Cargo.toml | 1 + node/src/cli.rs | 6 +- node/src/command.rs | 13 +- node/src/service.rs | 11 +- pallets/mining-rewards/Cargo.toml | 3 + pallets/mining-rewards/src/lib.rs | 111 +++++-- pallets/mining-rewards/src/mock.rs | 69 ++++- pallets/mining-rewards/src/tests.rs | 402 ++++++++++++++++++-------- runtime/src/configs/mod.rs | 11 +- runtime/src/genesis_config_presets.rs | 11 +- 15 files changed, 556 insertions(+), 276 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f8d3202c..f553ad92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7183,6 +7183,7 @@ dependencies = [ "log", "pallet-balances", "parity-scale-codec", + "qp-poseidon", "qp-wormhole", "scale-info", "sp-consensus-pow", @@ -9194,6 +9195,7 @@ dependencies = [ "parity-scale-codec", "prometheus", "qp-dilithium-crypto", + "qp-poseidon", "qp-rusty-crystals-dilithium", "qp-rusty-crystals-hdwallet", "qp-wormhole-circuit-builder", diff --git a/MINING.md b/MINING.md index 978b0370..70a8d8fb 100644 --- a/MINING.md +++ b/MINING.md @@ -2,6 +2,15 @@ Get started mining on the Quantus Network testnet in minutes. +## Important: Wormhole Address System + +**⚠️ Mining rewards are automatically sent to wormhole addresses derived from your preimage.** + +- You provide a 32-byte preimage when starting mining +- The system derives your wormhole address using Poseidon hashing +- All mining rewards are sent to this derived wormhole address +- This ensures all miners use privacy-preserving wormhole addresses + ## System Requirements ### Minimum Requirements @@ -32,17 +41,17 @@ If you prefer manual installation or the script doesn't work for your system: ./quantus-node key generate-node-key --file ~/.quantus/node_key.p2p ``` -3. **Generate Rewards Address** +3. **Generate Wormhole Address & Preimage** + ```bash - ./quantus-node key quantus + ./quantus-node key quantus --scheme wormhole ``` - - The address is in the output like this: -```sh -... -Address: qzpjg55HuN2vLdQerpZwhsGfRn6b4pc8uh4bdEgsYbJNeu8rn -... -``` + + This generates a wormhole key pair and shows: + - `Address`: Your wormhole address (where rewards will be sent) + - `inner_hash`: Your 32-byte preimage (use this for mining) + + **Save the preimage** - you'll need it for the `--rewards-address` parameter. 4. **Run the node (Dirac testnet)** @@ -52,10 +61,12 @@ Minimal command - see --help for many more options --validator \ --chain dirac \ --node-key-file ~/.quantus/node_key.p2p \ - --rewards-address \ + --rewards-preimage \ --max-blocks-per-request 64 \ --sync full ``` + +**Note:** Use the `inner_hash` from step 3 as your `--rewards-preimage`. The node will derive your wormhole address and log it on startup. ### Docker Installation For users who prefer containerized deployment or have only Docker installed: @@ -92,23 +103,20 @@ docker run --rm --platform linux/amd64 \ Replace `quantus-node:v0.0.4` with your desired image (e.g., `ghcr.io/quantus-network/quantus-node:latest`). This command saves `node_key.p2p` into your local `./quantus_node_data` directory. -**Step 3: Generate and Save Your Rewards Address** +**Step 3: Generate Your Wormhole Address** -Run the following command to generate your unique rewards address: ```bash # If on Apple Silicon, you may need to add --platform linux/amd64 -docker run --rm ghcr.io/quantus-network/quantus-node:latest key quantus +docker run --rm ghcr.io/quantus-network/quantus-node:latest key quantus --scheme wormhole ``` -Replace `quantus-node:v0.0.4` with your desired image. -This command will display your secret phrase, public key, address, and seed. -**Important: Securely back up your secret phrase!** -Next, **copy the displayed `Address`. + +This generates a wormhole key pair. Save the `inner_hash` value - this is your preimage for mining. **Step 4: Run the Validator Node** -Now, run the Docker container with all the necessary parameters: ```bash # If on Apple Silicon, you may need to add --platform linux/amd64 +# Replace YOUR_PREIMAGE with the inner_hash from step 3 docker run -d \ --name quantus-node \ --restart unless-stopped \ @@ -120,7 +128,7 @@ docker run -d \ --base-path /var/lib/quantus \ --chain dirac \ --node-key-file /var/lib/quantus/node_key.p2p \ - --rewards-address + --rewards-preimage ``` *Note for Apple Silicon (M1/M2/M3) users:* As mentioned above, if you are using an `amd64` based Docker image on an ARM-based Mac, you will likely need to add the `--platform linux/amd64` flag to your `docker run` commands. @@ -190,6 +198,49 @@ docker run -d \ - Docker 20.10+ or compatible runtime - All other system requirements same as binary installation +## External Miner Setup + +For high-performance mining, you can offload the QPoW mining process to a separate service, freeing up node resources. + +### Prerequisites + +1. **Build Node:** + ```bash + # From workspace root + cargo build --release -p quantus-node + ``` + +2. **Get External Miner:** + ```bash + git clone https://github.com/Quantus-Network/quantus-miner + cd quantus-miner + cargo build --release + ``` + +### Setup with Wormhole Addresses + +1. **Generate Your Wormhole Address**: + ```bash + ./quantus-node key quantus --scheme wormhole + ``` + Save the `inner_hash` value. + +2. **Start External Miner** (in separate terminal): + ```bash + RUST_LOG=info ./target/release/quantus-miner + ``` + *(Default: `http://127.0.0.1:9833`)* + +3. **Start Node with External Miner** (in another terminal): + ```bash + # Replace with the inner_hash from step 1 + RUST_LOG=info,sc_consensus_pow=debug ./target/release/quantus-node \ + --validator \ + --chain dirac \ + --external-miner-url http://127.0.0.1:9833 \ + --rewards-preimage + ``` + ## Configuration Options ### Node Parameters @@ -197,7 +248,7 @@ docker run -d \ | Parameter | Description | Default | |-----------|-------------|---------| | `--node-key-file` | Path to P2P identity file | Required | -| `--rewards-address` | Path to rewards address file | Required | +| `--rewards-preimage` | Wormhole preimage (inner_hash from key generation) | Required | | `--chain` | Chain specification | `dirac` | | `--port` | P2P networking port | `30333` | | `--prometheus-port` | Metrics endpoint port | `9616` | @@ -234,14 +285,18 @@ curl -H "Content-Type: application/json" \ ### Check Mining Rewards -**View Balance** +**View Balance at Your Wormhole Address** ```bash -# Replace YOUR_ADDRESS with your rewards address +# Replace YOUR_WORMHOLE_ADDRESS with your wormhole address from key generation curl -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","id":1,"method":"faucet_getAccountInfo","params":["YOUR_ADDRESS"]}' \ + -d '{"jsonrpc":"2.0","id":1,"method":"faucet_getAccountInfo","params":["YOUR_WORMHOLE_ADDRESS"]}' \ http://localhost:9944 ``` +**Find Your Wormhole Address** +- From key generation: Use the `Address` field from `./quantus-node key quantus --scheme wormhole` +- From node logs: Check startup logs for "Mining rewards will be sent to wormhole address" + ## Testnet Information - **Chain**: Dirac Testnet @@ -268,9 +323,14 @@ quantus-node purge-chain --chain dirac **Mining Not Working** 1. Check that `--validator` flag is present -2. Verify rewards address file exists and contains valid address +2. Verify your preimage from `inner_hash` field in key generation 3. Ensure node is synchronized (check logs for "Imported #XXXX") +**Wormhole Address Issues** +1. **Can't find rewards**: Check the `Address` field from your key generation +2. **Invalid preimage**: Use the exact `inner_hash` value from key generation +3. **Wrong address**: Rewards go to the wormhole address, not the preimage + **Connection Issues** 1. Check firewall settings (allow port 30333) 2. Verify internet connection @@ -302,9 +362,11 @@ curl -H "Content-Type: application/json" \ ## Mining Economics -### Rewards Structure +### Wormhole Address Rewards System -- **Block Rewards**: Earned by successfully mining blocks +- **Automatic Wormhole Addresses**: All mining rewards go to your wormhole address +- **Privacy by Design**: Your reward address is derived from your preimage +- **Block Rewards**: Earned by successfully mining blocks - **Transaction Fees**: Collected from transactions in mined blocks - **Network Incentives**: Additional rewards for network participation @@ -320,9 +382,9 @@ Mining performance depends on: ### Key Management -- **Backup Your Keys**: Store copies of your node identity and rewards keys safely -- **Secure Storage**: Keep private keys in encrypted storage -- **Regular Rotation**: Consider rotating keys periodically for enhanced security +- **Backup Your Keys**: Securely store your wormhole key pair from key generation +- **Backup Node Keys**: Store copies of your node identity keys safely +- **Secure Storage**: Keep preimages and private keys in encrypted storage ### Node Security @@ -341,8 +403,8 @@ This is testnet software for testing purposes only: ## Next Steps 1. **Join the Community**: Connect with other miners and developers -2. **Monitor Performance**: Track your mining efficiency and rewards -3. **Experiment**: Try different configurations and optimizations +2. **Monitor Performance**: Track your mining efficiency and rewards at your wormhole address +3. **Experiment**: Try different configurations and optimizations 4. **Contribute**: Help improve the network by reporting issues and feedback Happy mining! 🚀 diff --git a/README.md b/README.md index ccd9043b..156734c5 100644 --- a/README.md +++ b/README.md @@ -57,17 +57,9 @@ bip39 wordlist. Seed must be a 64-character hex string ---- - -### Rewards address +## Mining -By providing the optional `--rewards-address` parameter, the node will start sending mining and transaction rewards -after each block confirmation by the runtime. -If this address is not specified, rewards will not be minted. - -```shell -./quantus-node --chain local --validator --rewards-address -``` +For complete mining setup instructions, including wormhole address requirements and external miner configuration, see [MINING.md](MINING.md). ## Local dev run @@ -81,44 +73,6 @@ If this address is not specified, rewards will not be minted. ./target/release/quantus-node --dev ``` -## Run with External Miner - ---- - -This node supports offloading the QPoW mining process to a separate service, freeing up node resources. - -Any service that adheres to the API spec below can be used as miner by the node. We provide a sample implementation in -the 'miner' crate. - -API classes are defined in the 'resonance-miner-api' crate. - -**API Spec: -** [openapi.yaml](https://gitlab.com/resonance-network/backbone/-/blob/b37c4fcdb749ddddc747915b79149e29f537e92f/external-miner/api/openapi.yaml) - -1. **Build Node & Miner:** - ```bash - # From workspace root - cargo build --release -p quantus-node - ``` - -2. **Run External Miner:** (In a separate terminal) - ```bash - git clone https://github.com/Quantus-Network/quantus-miner - cd quantus-miner - cargo build --release - RUST_LOG=info ./target/release/quantus-miner - ``` - *(Listens on `http://127.0.0.1:9833` by default)* - -3. **Run Node:** (In another terminal) - ```bash - # From workspace root (replace ) - RUST_LOG=info,sc_consensus_pow=debug ./target/release/quantus-node \ - --dev \ - --external-miner-url http://127.0.0.1:9833 \ - --rewards-address - ``` - ## Multinode local run --- diff --git a/client/consensus/qpow/src/lib.rs b/client/consensus/qpow/src/lib.rs index 8fc459ee..70814f36 100644 --- a/client/consensus/qpow/src/lib.rs +++ b/client/consensus/qpow/src/lib.rs @@ -7,7 +7,7 @@ use sc_client_api::BlockBackend; use sp_api::ProvideRuntimeApi; use sp_consensus_pow::Seal as RawSeal; use sp_consensus_qpow::QPoWApi; -use sp_runtime::{generic::BlockId, traits::Block as BlockT, AccountId32}; +use sp_runtime::{generic::BlockId, traits::Block as BlockT}; use std::{sync::Arc, time::Duration}; use crate::worker::UntilImportedOrTimeout; @@ -24,7 +24,7 @@ use sp_block_builder::BlockBuilder as BlockBuilderApi; use sp_blockchain::HeaderBackend; use sp_consensus::{Environment, Error as ConsensusError, Proposer, SelectChain, SyncOracle}; use sp_consensus_pow::POW_ENGINE_ID; -use sp_core::ByteArray; + use sp_inherents::{CreateInherentDataProviders, InherentDataProvider}; use sp_runtime::{ generic::{Digest, DigestItem}, @@ -360,7 +360,7 @@ pub fn start_mining_worker( mut env: E, sync_oracle: SO, justification_sync_link: L, - rewards_address: AccountId32, + rewards_preimage: [u8; 32], create_inherent_data_providers: CIDP, timeout: Duration, build_time: Duration, @@ -462,8 +462,8 @@ where }; let mut inherent_digest = Digest::default(); - let rewards_address_bytes = rewards_address.clone().as_slice().to_vec(); - inherent_digest.push(DigestItem::PreRuntime(POW_ENGINE_ID, rewards_address_bytes)); + let rewards_preimage_bytes = rewards_preimage.to_vec(); + inherent_digest.push(DigestItem::PreRuntime(POW_ENGINE_ID, rewards_preimage_bytes)); let proposer = match env.init(&best_header).await { Ok(x) => x, @@ -496,7 +496,7 @@ where metadata: MiningMetadata { best_hash, pre_hash: proposal.block.header().hash(), - rewards_address: rewards_address.clone(), + rewards_preimage, difficulty, }, proposal, diff --git a/client/consensus/qpow/src/worker.rs b/client/consensus/qpow/src/worker.rs index 7fd05f96..b8db8d99 100644 --- a/client/consensus/qpow/src/worker.rs +++ b/client/consensus/qpow/src/worker.rs @@ -33,7 +33,7 @@ use sp_consensus::{BlockOrigin, Proposal}; use sp_consensus_pow::{Seal, POW_ENGINE_ID}; use sp_runtime::{ traits::{Block as BlockT, Header as HeaderT}, - AccountId32, DigestItem, + DigestItem, }; use std::{ pin::Pin, @@ -51,8 +51,8 @@ pub struct MiningMetadata { pub best_hash: H, /// Mining pre-hash. pub pre_hash: H, - /// Rewards address. - pub rewards_address: AccountId32, + /// Rewards preimage (32 bytes) - stored in block headers, hashed to derive wormhole address. + pub rewards_preimage: [u8; 32], /// Mining target difficulty. pub difficulty: D, } diff --git a/node/Cargo.toml b/node/Cargo.toml index 271a7813..bb5bca46 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -33,6 +33,7 @@ pallet-transaction-payment-rpc.default-features = true pallet-transaction-payment-rpc.workspace = true prometheus.workspace = true qp-dilithium-crypto = { workspace = true } +qp-poseidon.workspace = true qp-rusty-crystals-dilithium.workspace = true qp-rusty-crystals-hdwallet.workspace = true qpow-math.workspace = true diff --git a/node/src/cli.rs b/node/src/cli.rs index b9d3003a..edadf239 100644 --- a/node/src/cli.rs +++ b/node/src/cli.rs @@ -9,9 +9,9 @@ pub struct Cli { #[clap(flatten)] pub run: RunCmd, - /// Specify a rewards address for the miner - #[arg(long, value_name = "REWARDS_ADDRESS")] - pub rewards_address: Option, + /// Specify a rewards preimage for the miner (32-byte hex from wormhole key generation) + #[arg(long, value_name = "REWARDS_PREIMAGE")] + pub rewards_preimage: Option, /// Specify the URL of an external QPoW miner service #[arg(long, value_name = "EXTERNAL_MINER_URL")] diff --git a/node/src/command.rs b/node/src/command.rs index 654d8506..a4ed9714 100644 --- a/node/src/command.rs +++ b/node/src/command.rs @@ -448,16 +448,16 @@ pub fn run() -> sc_cli::Result<()> { config.network.network_backend = NetworkBackendType::Libp2p; - let rewards_account = match cli.rewards_address { + let rewards_account = match cli.rewards_preimage { Some(address) => { let account = address.parse::().map_err(|_| { - sc_cli::Error::Input("Invalid rewards address format".into()) + sc_cli::Error::Input("Invalid rewards preimage format".into()) })?; log::info!("⛏️ Using address for rewards: {:?}", account); account }, None => { - // Automatically set rewards_address to Treasury when --dev is used + // Automatically set rewards_preimage to Treasury when --dev is used if cli.run.shared_params.is_dev() { let treasury_account = quantus_runtime::configs::TreasuryPalletId::get() @@ -470,7 +470,7 @@ pub fn run() -> sc_cli::Result<()> { treasury_account } else { // Should never happen - return Err(sc_cli::Error::Input("No rewards address provided".into())); + return Err(sc_cli::Error::Input("No rewards preimage provided".into())); } }, }; @@ -481,7 +481,10 @@ pub fn run() -> sc_cli::Result<()> { ::Hash, >, >( - config, rewards_account, cli.external_miner_url.clone(), cli.enable_peer_sharing + config, + rewards_account.into(), + cli.external_miner_url.clone(), + cli.enable_peer_sharing, ) .map_err(sc_cli::Error::Service) }) diff --git a/node/src/service.rs b/node/src/service.rs index df6f15bd..09fad314 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -151,7 +151,7 @@ pub fn new_full< N: sc_network::NetworkBackend::Hash>, >( config: Configuration, - rewards_address: AccountId32, + rewards_preimage: [u8; 32], external_miner_url: Option, enable_peer_sharing: bool, ) -> Result { @@ -267,6 +267,13 @@ pub fn new_full< >, >; + // Derive wormhole address from preimage using Poseidon2 for internal use/logging + use qp_poseidon::PoseidonHasher; + let rewards_address_bytes = PoseidonHasher::hash_padded(&rewards_preimage); + let rewards_address = AccountId32::from(rewards_address_bytes); + + log::info!("⛏️ Mining rewards will be sent to wormhole address: {}", rewards_address); + let (worker_handle, worker_task) = sc_consensus_qpow::start_mining_worker( Box::new(pow_block_import), client.clone(), @@ -274,7 +281,7 @@ pub fn new_full< proposer, sync_service.clone(), sync_service.clone(), - rewards_address, + rewards_preimage, inherent_data_providers, Duration::from_secs(10), Duration::from_secs(10), diff --git a/pallets/mining-rewards/Cargo.toml b/pallets/mining-rewards/Cargo.toml index edb186b0..c5dee9f8 100644 --- a/pallets/mining-rewards/Cargo.toml +++ b/pallets/mining-rewards/Cargo.toml @@ -22,6 +22,7 @@ frame-benchmarking = { optional = true, workspace = true, default-features = fal frame-support.workspace = true frame-system.workspace = true log.workspace = true +qp-poseidon.workspace = true qp-wormhole.workspace = true scale-info = { workspace = true, default-features = false, features = ["derive"] } sp-consensus-pow.workspace = true @@ -30,6 +31,7 @@ sp-runtime.workspace = true [dev-dependencies] pallet-balances.features = ["std"] pallet-balances.workspace = true +qp-poseidon.workspace = true sp-core.workspace = true sp-io.workspace = true @@ -45,6 +47,7 @@ std = [ "frame-benchmarking?/std", "frame-support/std", "frame-system/std", + "qp-poseidon/std", "qp-wormhole/std", "scale-info/std", "sp-consensus-pow/std", diff --git a/pallets/mining-rewards/src/lib.rs b/pallets/mining-rewards/src/lib.rs index e135ff79..66d985ac 100644 --- a/pallets/mining-rewards/src/lib.rs +++ b/pallets/mining-rewards/src/lib.rs @@ -17,7 +17,7 @@ pub use weights::*; pub mod pallet { use super::*; use codec::Decode; - use core::marker::PhantomData; + use core::{convert::TryInto, marker::PhantomData}; use frame_support::{ pallet_prelude::*, traits::{ @@ -26,11 +26,14 @@ pub mod pallet { }, }; use frame_system::pallet_prelude::*; + use qp_poseidon::PoseidonHasher; + use qp_wormhole::TransferProofs; use qp_wormhole::TransferProofRecorder; use sp_consensus_pow::POW_ENGINE_ID; use sp_runtime::{ generic::DigestItem, traits::{AccountIdConversion, Saturating}, + Permill, }; pub(crate) type BalanceOf = @@ -61,13 +64,21 @@ pub mod pallet { BalanceOf, >; - /// The base block reward given to miners + /// The maximum total supply of tokens #[pallet::constant] - type MinerBlockReward: Get>; + type MaxSupply: Get>; - /// The base block reward given to treasury + /// The divisor used to calculate block rewards from remaining supply #[pallet::constant] - type TreasuryBlockReward: Get>; + type EmissionDivisor: Get>; + + /// The portion of rewards that goes to treasury + #[pallet::constant] + type TreasuryPortion: Get; + + /// The base unit for token amounts (e.g., 1e12 for 12 decimals) + #[pallet::constant] + type Unit: Get>; /// The treasury pallet ID #[pallet::constant] @@ -110,30 +121,71 @@ pub mod pallet { } fn on_finalize(_block_number: BlockNumberFor) { - // Get the block rewards - let miner_reward = T::MinerBlockReward::get(); - let treasury_reward = T::TreasuryBlockReward::get(); + // Calculate dynamic block reward based on remaining supply + let max_supply = T::MaxSupply::get(); + let current_supply = T::Currency::total_issuance(); + let emission_divisor = T::EmissionDivisor::get(); + + let remaining_supply = max_supply.saturating_sub(current_supply); + + if remaining_supply == BalanceOf::::zero() { + log::warn!( + "💰 Emission completed: current supply has reached the configured maximum, \ + no further block rewards will be minted." + ); + } + + let total_reward = remaining_supply + .checked_div(&emission_divisor) + .unwrap_or_else(|| BalanceOf::::zero()); + + // Split the reward between treasury and miner + let treasury_reward = T::TreasuryPortion::get().mul_floor(total_reward); + let miner_reward = total_reward.saturating_sub(treasury_reward); + let tx_fees = >::take(); // Extract miner ID from the pre-runtime digest let miner = Self::extract_miner_from_digest(); - log::debug!(target: "mining-rewards", "💰 Base reward: {:?}", miner_reward); - log::debug!(target: "mining-rewards", "💰 Original Tx_fees: {:?}", tx_fees); + // Log readable amounts (convert to tokens by dividing by unit) + if let (Ok(total), Ok(treasury), Ok(miner_amt), Ok(current), Ok(fees), Ok(unit)) = ( + TryInto::::try_into(total_reward), + TryInto::::try_into(treasury_reward), + TryInto::::try_into(miner_reward), + TryInto::::try_into(current_supply), + TryInto::::try_into(tx_fees), + TryInto::::try_into(T::Unit::get()), + ) { + let remaining: u128 = + TryInto::::try_into(max_supply.saturating_sub(current_supply)) + .unwrap_or(0); + let unit_f64 = unit as f64; + log::debug!( + target: "mining-rewards", + "💰 Rewards: total={:.6}, treasury={:.6}, miner={:.6}, fees={:.6}, supply={:.2}, remaining={:.2}", + total as f64 / unit_f64, + treasury as f64 / unit_f64, + miner_amt as f64 / unit_f64, + fees as f64 / unit_f64, + current as f64 / unit_f64, + remaining as f64 / unit_f64 + ); + } // Send fees to miner if any Self::mint_reward(miner.clone(), tx_fees); - // Send rewards separately for accounting + // Send block rewards to miner Self::mint_reward(miner, miner_reward); - // Send treasury reward + // Send treasury portion to treasury Self::mint_reward(None, treasury_reward); } } impl Pallet { - /// Extract miner account ID from the pre-runtime digest + /// Extract miner wormhole address by hashing the preimage from pre-runtime digest fn extract_miner_from_digest() -> Option { // Get the digest from the current block let digest = >::digest(); @@ -142,10 +194,22 @@ pub mod pallet { for log in digest.logs.iter() { if let DigestItem::PreRuntime(engine_id, data) = log { if engine_id == &POW_ENGINE_ID { - // Try to decode the accountId - // TODO: to enforce miner wormholes, decode inner hash here - if let Ok(miner) = T::AccountId::decode(&mut &data[..]) { - return Some(miner); + // The data is a 32-byte preimage from the incoming block + if data.len() == 32 { + let preimage: [u8; 32] = match data.as_slice().try_into() { + Ok(arr) => arr, + Err(_) => continue, + }; + + // Hash the preimage with Poseidon2 to derive the wormhole address + let wormhole_address_bytes = PoseidonHasher::hash_padded(&preimage); + + // Convert to AccountId + if let Ok(miner) = + T::AccountId::decode(&mut &wormhole_address_bytes[..]) + { + return Some(miner); + } } } } @@ -182,13 +246,6 @@ pub mod pallet { ); Self::deposit_event(Event::MinerRewarded { miner: miner.clone(), reward }); - - log::debug!( - target: "mining-rewards", - "💰 Rewards sent to miner: {:?} {:?}", - reward, - miner - ); }, None => { let treasury = T::TreasuryPalletId::get().into_account_truncating(); @@ -202,12 +259,6 @@ pub mod pallet { ); Self::deposit_event(Event::TreasuryRewarded { reward }); - - log::debug!( - target: "mining-rewards", - "💰 Rewards sent to Treasury: {:?}", - reward - ); }, }; } diff --git a/pallets/mining-rewards/src/mock.rs b/pallets/mining-rewards/src/mock.rs index 1b036245..660ac3ca 100644 --- a/pallets/mining-rewards/src/mock.rs +++ b/pallets/mining-rewards/src/mock.rs @@ -1,10 +1,11 @@ use crate as pallet_mining_rewards; -use codec::Encode; + use frame_support::{ parameter_types, traits::{ConstU32, Everything, Hooks}, PalletId, }; +use qp_poseidon::PoseidonHasher; use sp_consensus_pow::POW_ENGINE_ID; use sp_runtime::{ app_crypto::sp_core, @@ -24,11 +25,13 @@ frame_support::construct_runtime!( pub type Balance = u128; pub type Block = frame_system::mocking::MockBlock; +const UNIT: u128 = 1_000_000_000_000u128; parameter_types! { pub const BlockHashCount: u64 = 250; pub const SS58Prefix: u8 = 189; - pub const BlockReward: u128 = 50; + pub const MaxSupply: u128 = 21_000_000 * UNIT; + pub const EmissionDivisor: u128 = 26_280_000; pub const ExistentialDeposit: Balance = 1; pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); } @@ -84,8 +87,9 @@ impl pallet_balances::Config for Test { } parameter_types! { - pub const TreasuryBlockReward: u128 = 50; + pub const TreasuryPortion: Permill = Permill::from_percent(50); // 50% goes to treasury in tests (matching runtime) pub const MintingAccount: sp_core::crypto::AccountId32 = sp_core::crypto::AccountId32::new([99u8; 32]); + pub const Unit: u128 = UNIT; } // Mock proof recorder that does nothing @@ -110,23 +114,40 @@ impl pallet_mining_rewards::Config for Test { type AssetId = u32; type ProofRecorder = MockProofRecorder; type WeightInfo = (); - type MinerBlockReward = BlockReward; - type TreasuryBlockReward = TreasuryBlockReward; + type MaxSupply = MaxSupply; + type EmissionDivisor = EmissionDivisor; + type TreasuryPortion = TreasuryPortion; type TreasuryPalletId = TreasuryPalletId; type MintingAccount = MintingAccount; + type Unit = Unit; +} + +/// Helper function to convert a u8 to a preimage +pub fn miner_preimage(id: u8) -> [u8; 32] { + [id; 32] +} + +/// Helper function to derive wormhole address from preimage +pub fn wormhole_address_from_preimage(preimage: [u8; 32]) -> sp_core::crypto::AccountId32 { + let hash = PoseidonHasher::hash_padded(&preimage); + sp_core::crypto::AccountId32::from(hash) } -/// Helper function to convert a u8 to an AccountId32 -pub fn account_id(id: u8) -> sp_core::crypto::AccountId32 { - sp_core::crypto::AccountId32::from([id; 32]) +// Configure default miner preimages and addresses for tests +pub fn miner_preimage_1() -> [u8; 32] { + miner_preimage(1) +} + +pub fn miner_preimage_2() -> [u8; 32] { + miner_preimage(2) } -// Configure a default miner account for tests pub fn miner() -> sp_core::crypto::AccountId32 { - account_id(1) + wormhole_address_from_preimage(miner_preimage_1()) } + pub fn miner2() -> sp_core::crypto::AccountId32 { - account_id(2) + wormhole_address_from_preimage(miner_preimage_2()) } // Build genesis storage according to the mock runtime. @@ -145,11 +166,27 @@ pub fn new_test_ext() -> sp_io::TestExternalities { ext } -// Helper function to create a block digest with a miner pre-runtime digest -pub fn set_miner_digest(miner: sp_core::crypto::AccountId32) { - // reset logs - let miner_bytes = miner.encode(); - let pre_digest = DigestItem::PreRuntime(POW_ENGINE_ID, miner_bytes); +// Helper function to create a block digest with a miner preimage +pub fn set_miner_digest(miner_account: sp_core::crypto::AccountId32) { + // Find the preimage that corresponds to this miner address + let preimage = if miner_account == miner() { + miner_preimage_1() + } else if miner_account == miner2() { + miner_preimage_2() + } else { + // For other miners, use their raw bytes as preimage for testing + let mut preimage = [0u8; 32]; + preimage.copy_from_slice(miner_account.as_ref()); + preimage + }; + + set_miner_preimage_digest(preimage); +} + +// Helper function to create a block digest with a specific preimage +pub fn set_miner_preimage_digest(preimage: [u8; 32]) { + let pre_digest = DigestItem::PreRuntime(POW_ENGINE_ID, preimage.to_vec()); + let digest = Digest { logs: vec![pre_digest] }; System::deposit_log(pre_digest); } diff --git a/pallets/mining-rewards/src/tests.rs b/pallets/mining-rewards/src/tests.rs index bd4604d4..d51efa66 100644 --- a/pallets/mining-rewards/src/tests.rs +++ b/pallets/mining-rewards/src/tests.rs @@ -2,8 +2,6 @@ use crate::{mock::*, weights::WeightInfo, Event}; use frame_support::traits::{Currency, Hooks}; use sp_runtime::{testing::Digest, traits::AccountIdConversion}; -const UNIT: u128 = 1_000_000_000_000; - #[test] fn miner_reward_works() { new_test_ext().execute_with(|| { @@ -13,17 +11,26 @@ fn miner_reward_works() { // Add a miner to the pre-runtime digest set_miner_digest(miner()); + // Calculate expected rewards with treasury portion + // Initial supply is just the existential deposits (2 accounts * 1 unit each = 2) + let current_supply = Balances::total_issuance(); + let total_reward = (MaxSupply::get() - current_supply) / EmissionDivisor::get(); + let treasury_reward = TreasuryPortion::get().mul_floor(total_reward); + let miner_reward = total_reward - treasury_reward; + // Run the on_finalize hook MiningRewards::on_finalize(1); - // Check that the miner received the block reward (no fees in this test) - assert_eq!( - Balances::free_balance(miner()), - initial_balance + 50 // Initial + base reward only + // Check that the miner received the calculated block reward (minus treasury portion) + assert_eq!(Balances::free_balance(miner()), initial_balance + miner_reward); + + // Check the miner reward event was emitted + System::assert_has_event( + Event::MinerRewarded { miner: miner(), reward: miner_reward }.into(), ); - // Check the event was emitted - System::assert_has_event(Event::MinerRewarded { miner: miner(), reward: 50 }.into()); + // Check the treasury reward event was emitted + System::assert_has_event(Event::TreasuryRewarded { reward: treasury_reward }.into()); }); } @@ -43,25 +50,20 @@ fn miner_reward_with_transaction_fees_works() { // Check fees collection event System::assert_has_event(Event::FeesCollected { amount: 25, total: 25 }.into()); + // Calculate expected rewards with treasury portion + let current_supply = Balances::total_issuance(); + let total_block_reward = (MaxSupply::get() - current_supply) / EmissionDivisor::get(); + let treasury_reward = TreasuryPortion::get().mul_floor(total_block_reward); + let miner_block_reward = total_block_reward - treasury_reward; + // Run the on_finalize hook MiningRewards::on_finalize(1); - // Check that the miner received the block reward + all fees - // Current implementation: miner gets base reward (50) + all fees (25) - assert_eq!( - Balances::free_balance(miner()), - initial_balance + 50 + 25 // Initial + base + all fees - ); + // Check that the miner received the miner portion of block reward + all fees + assert_eq!(Balances::free_balance(miner()), initial_balance + miner_block_reward + fees); // Check the events were emitted with the correct amounts - // First event: treasury block reward - System::assert_has_event( - Event::TreasuryRewarded { - reward: 50, // treasury block reward - } - .into(), - ); - // Second event: miner reward for fees + // First event: miner reward for fees System::assert_has_event( Event::MinerRewarded { miner: miner(), @@ -69,14 +71,12 @@ fn miner_reward_with_transaction_fees_works() { } .into(), ); - // Third event: miner reward for base reward + // Second event: miner reward for block reward System::assert_has_event( - Event::MinerRewarded { - miner: miner(), - reward: 50, // base reward - } - .into(), + Event::MinerRewarded { miner: miner(), reward: miner_block_reward }.into(), ); + // Third event: treasury reward + System::assert_has_event(Event::TreasuryRewarded { reward: treasury_reward }.into()); }); } @@ -92,17 +92,18 @@ fn on_unbalanced_collects_fees() { // Check that fees were collected assert_eq!(MiningRewards::collected_fees(), 30); + // Calculate expected rewards with treasury portion + let current_supply = Balances::total_issuance(); + let total_block_reward = (MaxSupply::get() - current_supply) / EmissionDivisor::get(); + let treasury_reward = TreasuryPortion::get().mul_floor(total_block_reward); + let miner_block_reward = total_block_reward - treasury_reward; + // Add a miner to the pre-runtime digest and distribute rewards set_miner_digest(miner()); MiningRewards::on_finalize(1); - // Check that the miner received the block reward + all fees - // Check miner received rewards - // Current implementation: miner gets base reward (50) + all fees (30) - assert_eq!( - Balances::free_balance(miner()), - initial_balance + 50 + 30 // Initial + base + all fees - ); + // Check that the miner received the miner portion of block reward + all fees + assert_eq!(Balances::free_balance(miner()), initial_balance + miner_block_reward + 30); }); } @@ -115,22 +116,35 @@ fn multiple_blocks_accumulate_rewards() { // Block 1 set_miner_digest(miner()); MiningRewards::collect_transaction_fees(10); + + // Calculate rewards for block 1 with treasury portion + let current_supply_block1 = Balances::total_issuance(); + let total_block1_reward = + (MaxSupply::get() - current_supply_block1) / EmissionDivisor::get(); + let miner_block1_reward = + total_block1_reward - TreasuryPortion::get().mul_floor(total_block1_reward); + MiningRewards::on_finalize(1); - // Current implementation: miner gets base reward (50) + all fees (10) - let balance_after_block_1 = initial_balance + 50 + 10; // Initial + base + all fees + let balance_after_block_1 = initial_balance + miner_block1_reward + 10; assert_eq!(Balances::free_balance(miner()), balance_after_block_1); - // Block 2 + // Block 2 - supply has increased after block 1, so reward will be different set_miner_digest(miner()); MiningRewards::collect_transaction_fees(15); + + let current_supply_block2 = Balances::total_issuance(); + let total_block2_reward = + (MaxSupply::get() - current_supply_block2) / EmissionDivisor::get(); + let miner_block2_reward = + total_block2_reward - TreasuryPortion::get().mul_floor(total_block2_reward); + MiningRewards::on_finalize(2); // Check total rewards for both blocks - // Block 1: 50 + 10 = 60, Block 2: 50 + 15 = 65, Total: 125 assert_eq!( Balances::free_balance(miner()), - initial_balance + 50 + 10 + 50 + 15 // Initial + block1 + block2 + initial_balance + miner_block1_reward + 10 + miner_block2_reward + 15 ); }); } @@ -145,11 +159,16 @@ fn different_miners_get_different_rewards() { // Block 1 - First miner set_miner_digest(miner()); MiningRewards::collect_transaction_fees(10); + + let current_supply_block1 = Balances::total_issuance(); + let total_block1_reward = + (MaxSupply::get() - current_supply_block1) / EmissionDivisor::get(); + let miner_block1_reward = + total_block1_reward - TreasuryPortion::get().mul_floor(total_block1_reward); + MiningRewards::on_finalize(1); - // Check first miner balance - // Current implementation: miner gets base reward (50) + all fees (10) - let balance_after_block_1 = initial_balance_miner1 + 50 + 10; // Initial + base + all fees + let balance_after_block_1 = initial_balance_miner1 + miner_block1_reward + 10; assert_eq!(Balances::free_balance(miner()), balance_after_block_1); // Block 2 - Second miner @@ -158,15 +177,21 @@ fn different_miners_get_different_rewards() { System::initialize(&2, &block_1.hash(), &Digest { logs: vec![] }); set_miner_digest(miner2()); MiningRewards::collect_transaction_fees(20); + + let current_supply_block2 = Balances::total_issuance(); + let total_block2_reward = + (MaxSupply::get() - current_supply_block2) / EmissionDivisor::get(); + let miner_block2_reward = + total_block2_reward - TreasuryPortion::get().mul_floor(total_block2_reward); + MiningRewards::on_finalize(2); println!("Balnce {}", Balances::free_balance(miner())); // Check second miner balance - // Current implementation: miner gets base reward (50) + all fees (20) assert_eq!( Balances::free_balance(miner2()), - initial_balance_miner2 + 50 + 20 // Initial + base + all fees + initial_balance_miner2 + miner_block2_reward + 20 ); // First miner balance should remain unchanged @@ -188,17 +213,18 @@ fn transaction_fees_collector_works() { // Check accumulated fees assert_eq!(MiningRewards::collected_fees(), 30); + // Calculate expected rewards with treasury portion + let current_supply = Balances::total_issuance(); + let total_block_reward = (MaxSupply::get() - current_supply) / EmissionDivisor::get(); + let miner_block_reward = + total_block_reward - TreasuryPortion::get().mul_floor(total_block_reward); + // Reward miner set_miner_digest(miner()); MiningRewards::on_finalize(1); - // Check miner got base reward + 90% of all fees - // Check that the miner received the block reward + all collected fees - // Base reward: 50, Fees: 30 (from the collect_transaction_fees call) - assert_eq!( - Balances::free_balance(miner()), - initial_balance + 50 + 30 // Initial + base + all fees - ); + // Check that the miner received the miner portion of block reward + all collected fees + assert_eq!(Balances::free_balance(miner()), initial_balance + miner_block_reward + 30); }); } @@ -217,16 +243,18 @@ fn block_lifecycle_works() { // 2. Add some transaction fees during block execution MiningRewards::collect_transaction_fees(15); + // Calculate expected rewards with treasury portion + let current_supply = Balances::total_issuance(); + let total_block_reward = (MaxSupply::get() - current_supply) / EmissionDivisor::get(); + let miner_block_reward = + total_block_reward - TreasuryPortion::get().mul_floor(total_block_reward); + // 3. on_finalize - should reward the miner set_miner_digest(miner()); MiningRewards::on_finalize(1); // Check miner received rewards - // Current implementation: miner gets base reward (50) + all fees (15 in this test) - assert_eq!( - Balances::free_balance(miner()), - initial_balance + 50 + 15 // Initial + base reward + fees - ); + assert_eq!(Balances::free_balance(miner()), initial_balance + miner_block_reward + 15); }); } @@ -242,20 +270,23 @@ fn test_run_to_block_helper() { // Add fees for block 1 MiningRewards::collect_transaction_fees(10); + // Note: This test is complex with run_to_block as rewards change with supply + // We'll just verify the mechanism works and final balance is reasonable + let initial_supply = Balances::total_issuance(); + // Run to block 3 (this should process blocks 1 and 2) run_to_block(3); - // Check that miner received rewards for blocks 1 and 2 - // Block 1: 50 (base) + 10 (fees) = 60 - // Block 2: 50 (base) + 0 (no new fees) = 50 - // Total: 110 - assert_eq!( - Balances::free_balance(miner()), - initial_balance + 110 // Initial + 50 + 10 + 50 - ); - // Verify we're at the expected block number assert_eq!(System::block_number(), 3); + + // Check that miner balance increased (should have rewards from both blocks + fees) + let final_balance = Balances::free_balance(miner()); + assert!(final_balance > initial_balance, "Miner should have received rewards"); + + // Verify supply increased due to minted rewards + let final_supply = Balances::total_issuance(); + assert!(final_supply > initial_supply, "Total supply should have increased"); }); } @@ -266,106 +297,225 @@ fn rewards_go_to_treasury_when_no_miner() { let treasury_account = TreasuryPalletId::get().into_account_truncating(); let initial_treasury_balance = Balances::free_balance(&treasury_account); - // Fund Treasury - let treasury_funding = 1000 * UNIT; - let _ = Balances::deposit_creating(&treasury_account, treasury_funding); + // Calculate expected rewards - when no miner, all rewards go to treasury + let current_supply = Balances::total_issuance(); + let total_reward = (MaxSupply::get() - current_supply) / EmissionDivisor::get(); + let treasury_portion_reward = TreasuryPortion::get().mul_floor(total_reward); + let miner_portion_reward = total_reward - treasury_portion_reward; - // Create a block without a miner + // Create a block without a miner (no digest set) System::set_block_number(1); MiningRewards::on_finalize(System::block_number()); - // Check that Treasury received the rewards - // When no miner, treasury gets both miner reward and treasury block reward - let expected_reward = BlockReward::get() + TreasuryBlockReward::get(); // 50 + 50 = 100 + // Check that Treasury received both its portion and the miner's portion (since no miner) assert_eq!( Balances::free_balance(treasury_account), - initial_treasury_balance + treasury_funding + expected_reward + initial_treasury_balance + treasury_portion_reward + miner_portion_reward ); - // Check that the events were emitted - treasury gets both miner reward and treasury reward + // Check that the events were emitted System::assert_has_event( - Event::TreasuryRewarded { - reward: 50, // treasury block reward - } - .into(), - ); - System::assert_has_event( - Event::TreasuryRewarded { - reward: 50, // miner reward (goes to treasury when no miner) - } - .into(), + Event::TreasuryRewarded { reward: treasury_portion_reward }.into(), ); + System::assert_has_event(Event::TreasuryRewarded { reward: miner_portion_reward }.into()); }); } #[test] -fn test_fees_split_between_treasury_and_miner() { +fn test_fees_and_rewards_to_miner() { new_test_ext().execute_with(|| { - // Set up initial balances - let miner = account_id(1); - let _ = Balances::deposit_creating(&miner, 0); // Create account, balance might become ExistentialDeposit - let actual_initial_balance_after_creation = Balances::free_balance(&miner); + // Use a test preimage and derive the wormhole address + let test_preimage = [42u8; 32]; // Use a distinct preimage for this test + let miner_wormhole_address = wormhole_address_from_preimage(test_preimage); + let _ = Balances::deposit_creating(&miner_wormhole_address, 0); // Create account + let actual_initial_balance_after_creation = Balances::free_balance(&miner_wormhole_address); // Set transaction fees let tx_fees = 100; MiningRewards::collect_transaction_fees(tx_fees); - // Create a block with a miner + // Calculate expected rewards with treasury portion + let current_supply = Balances::total_issuance(); + let total_block_reward = (MaxSupply::get() - current_supply) / EmissionDivisor::get(); + let treasury_reward = TreasuryPortion::get().mul_floor(total_block_reward); + let miner_block_reward = total_block_reward - treasury_reward; + + // Create a block with the preimage System::set_block_number(1); - set_miner_digest(miner.clone()); + set_miner_preimage_digest(test_preimage); // Run on_finalize MiningRewards::on_finalize(System::block_number()); - // Get Treasury account - let treasury_account: sp_core::crypto::AccountId32 = - TreasuryPalletId::get().into_account_truncating(); - // Get actual values from the system AFTER on_finalize - let treasury_balance_after_finalize = Balances::free_balance(&treasury_account); - let miner_balance_after_finalize = Balances::free_balance(&miner); - - // Calculate expected values using the same method as in the implementation - // Current implementation: miner gets all fees, treasury gets block reward - let expected_reward_component_for_miner = BlockReward::get().saturating_add(tx_fees); + let miner_balance_after_finalize = Balances::free_balance(&miner_wormhole_address); - // Check Treasury balance - it should have the treasury block reward - assert_eq!( - treasury_balance_after_finalize, - 50, // TreasuryBlockReward - "Treasury should receive block reward" - ); - - // Check miner balance + // Check miner balance - should get miner portion of block reward + all fees assert_eq!( miner_balance_after_finalize, - actual_initial_balance_after_creation + expected_reward_component_for_miner, - "Miner should receive base reward + all fees" + actual_initial_balance_after_creation + miner_block_reward + tx_fees, + "Miner should receive miner portion of block reward + all fees" ); // Verify events - // Check events for proper reward distribution - System::assert_has_event( - Event::TreasuryRewarded { - reward: 50, // treasury block reward - } - .into(), - ); - System::assert_has_event( Event::MinerRewarded { - miner: miner.clone(), + miner: miner_wormhole_address.clone(), reward: 100, // all fees go to miner } .into(), ); System::assert_has_event( - Event::MinerRewarded { - miner, - reward: BlockReward::get(), // base reward - } - .into(), + Event::MinerRewarded { miner: miner_wormhole_address, reward: miner_block_reward } + .into(), ); + + System::assert_has_event(Event::TreasuryRewarded { reward: treasury_reward }.into()); + }); +} + +#[test] +fn test_emission_simulation_120m_blocks() { + new_test_ext().execute_with(|| { + // Add realistic initial supply similar to genesis + let treasury_account = TreasuryPalletId::get().into_account_truncating(); + let _ = Balances::deposit_creating(&treasury_account, 3_600_000 * UNIT); + + println!("=== Mining Rewards Emission Simulation ==="); + println!("Max Supply: {:.0} tokens", MaxSupply::get() as f64 / UNIT as f64); + println!("Emission Divisor: {:?}", EmissionDivisor::get()); + println!("Treasury Portion: {:?}", TreasuryPortion::get()); + println!(); + + const MAX_BLOCKS: u32 = 130_000_000; + const REPORT_INTERVAL: u32 = 1_000_000; // Report every 1M blocks + const UNIT: u128 = 1_000_000_000_000; // For readable output + + let initial_supply = Balances::total_issuance(); + let mut current_supply = initial_supply; + let mut total_miner_rewards = 0u128; + let mut total_treasury_rewards = 0u128; + let mut block = 0u32; + + println!("Block Supply %MaxSupply BlockReward ToTreasury ToMiner Remaining"); + println!("{}", "-".repeat(90)); + + // Print initial state + let remaining = MaxSupply::get() - current_supply; + let block_reward = if remaining > 0 { remaining / EmissionDivisor::get() } else { 0 }; + let treasury_reward = TreasuryPortion::get().mul_floor(block_reward); + let miner_reward = block_reward - treasury_reward; + + println!( + "{:<11} {:<13} {:<11.2}% {:<13.6} {:<12.6} {:<12.6} {:<13}", + block, + current_supply / UNIT, + (current_supply as f64 / MaxSupply::get() as f64) * 100.0, + block_reward as f64 / UNIT as f64, + treasury_reward as f64 / UNIT as f64, + miner_reward as f64 / UNIT as f64, + remaining / UNIT + ); + + // Set up a consistent miner + set_miner_digest(miner()); + + while block < MAX_BLOCKS && current_supply < MaxSupply::get() { + // Simulate REPORT_INTERVAL blocks + for _ in 0..REPORT_INTERVAL { + if current_supply >= MaxSupply::get() { + break; + } + + // Calculate reward for this block + let remaining_supply = MaxSupply::get().saturating_sub(current_supply); + if remaining_supply == 0 { + break; + } + + let block_reward = remaining_supply / EmissionDivisor::get(); + let treasury_reward = TreasuryPortion::get().mul_floor(block_reward); + let miner_reward = block_reward - treasury_reward; + + // Update totals (simulate the minting) + current_supply += block_reward; + total_treasury_rewards += treasury_reward; + total_miner_rewards += miner_reward; + block += 1; + + // Early exit if rewards become negligible + if block_reward < 1000 { // Less than 1000 raw units (very small) + break; + } + } + + // Print progress report + let remaining = MaxSupply::get().saturating_sub(current_supply); + let next_block_reward = if remaining > 0 { remaining / EmissionDivisor::get() } else { 0 }; + let next_treasury = TreasuryPortion::get().mul_floor(next_block_reward); + let next_miner = next_block_reward - next_treasury; + + println!( + "{:<11} {:<13} {:<11.2}% {:<13.6} {:<12.6} {:<12.6} {:<13}", + block, + current_supply / UNIT, + (current_supply as f64 / MaxSupply::get() as f64) * 100.0, + next_block_reward as f64 / UNIT as f64, + next_treasury as f64 / UNIT as f64, + next_miner as f64 / UNIT as f64, + remaining / UNIT + ); + + // Stop if rewards become negligible or we've reached max supply + if current_supply >= MaxSupply::get() || next_block_reward < 1000 { + break; + } + } + + println!("{}", "-".repeat(90)); + println!(); + println!("=== Final Summary ==="); + println!("Total Blocks Processed: {}", block); + println!("Final Supply: {:.6} tokens", current_supply as f64 / UNIT as f64); + println!("Percentage of Max Supply: {:.4}%", (current_supply as f64 / MaxSupply::get() as f64) * 100.0); + println!("Remaining Supply: {:.6} tokens", (MaxSupply::get() - current_supply) as f64 / UNIT as f64); + println!(); + println!("Total Miner Rewards: {:.6} tokens", total_miner_rewards as f64 / UNIT as f64); + println!("Total Treasury Rewards: {:.6} tokens", total_treasury_rewards as f64 / UNIT as f64); + println!("Total Rewards Distributed: {:.6} tokens", (total_miner_rewards + total_treasury_rewards) as f64 / UNIT as f64); + println!(); + println!("Miner Share: {:.1}%", (total_miner_rewards as f64 / (total_miner_rewards + total_treasury_rewards) as f64) * 100.0); + println!("Treasury Share: {:.1}%", (total_treasury_rewards as f64 / (total_miner_rewards + total_treasury_rewards) as f64) * 100.0); + + // Time estimates (assuming 12 second blocks) + let total_seconds = block as f64 * 12.0; + let days = total_seconds / (24.0 * 3600.0); + let years = days / 365.25; + + println!(); + println!("=== Time Estimates (12s blocks) ==="); + println!("Total Time: {:.1} days ({:.1} years)", days, years); + + // === Comprehensive Emission Validation === + + assert!(current_supply >= initial_supply, "Supply should have increased"); + assert!(current_supply <= MaxSupply::get(), "Supply should not exceed max supply"); + + let emitted_tokens = current_supply - initial_supply; + let emission_percentage = (emitted_tokens as f64 / (MaxSupply::get() - initial_supply) as f64) * 100.0; + assert!(emission_percentage > 99.0, "Should have emitted >99% of available supply, got {:.2}%", emission_percentage); + + assert!(total_miner_rewards > 0, "Miners should have received rewards"); + assert!(total_treasury_rewards > 0, "Treasury should have received rewards"); + assert_eq!(total_miner_rewards + total_treasury_rewards, emitted_tokens, "Total rewards should equal emitted tokens"); + + let remaining_percentage = ((MaxSupply::get() - current_supply) as f64 / MaxSupply::get() as f64) * 100.0; + assert!(remaining_percentage < 1.0, "Should have <10% supply remaining, got {:.2}%", remaining_percentage); + assert!(remaining_percentage > 0.0, "Should still have some supply remaining for future emission"); + + println!(); + println!("✅ All emission validation checks passed!"); + println!("✅ Emission simulation completed successfully!"); }); } diff --git a/runtime/src/configs/mod.rs b/runtime/src/configs/mod.rs index 5b2e7ec8..8e669118 100644 --- a/runtime/src/configs/mod.rs +++ b/runtime/src/configs/mod.rs @@ -128,15 +128,22 @@ parameter_types! { pub const DefaultMintAmount: Balance = 10 * UNIT; } +parameter_types! { + pub const TreasuryPortion: Permill = Permill::from_percent(50); + pub const MiningUnit: Balance = UNIT; +} + impl pallet_mining_rewards::Config for Runtime { type Currency = Balances; type AssetId = AssetId; type ProofRecorder = Wormhole; type WeightInfo = pallet_mining_rewards::weights::SubstrateWeight; - type MinerBlockReward = ConstU128<{ 10 * UNIT }>; // 10 tokens - type TreasuryBlockReward = ConstU128<0>; // 0 tokens + type MaxSupply = ConstU128<{ 21_000_000 * UNIT }>; // 21 million tokens + type EmissionDivisor = ConstU128<26_280_000>; // Divide remaining supply by this amount + type TreasuryPortion = TreasuryPortion; type TreasuryPalletId = TreasuryPalletId; type MintingAccount = MintingAccount; + type Unit = MiningUnit; } parameter_types! { diff --git a/runtime/src/genesis_config_presets.rs b/runtime/src/genesis_config_presets.rs index c86a093c..5a4783ae 100644 --- a/runtime/src/genesis_config_presets.rs +++ b/runtime/src/genesis_config_presets.rs @@ -55,12 +55,15 @@ fn dilithium_default_accounts() -> Vec { } // Returns the genesis config presets populated with given parameters. fn genesis_template(endowed_accounts: Vec, root: AccountId) -> Value { - let mut balances = - endowed_accounts.iter().cloned().map(|k| (k, 1u128 << 60)).collect::>(); + let mut balances = endowed_accounts + .iter() + .cloned() + .map(|k| (k, 100_000 * UNIT)) + .collect::>(); - const ONE_BILLION: u128 = 1_000_000_000; + const INITIAL_TREASURY: u128 = 21_000_000 * 30 * UNIT / 100; // 30% tokens go to investors let treasury_account = TreasuryPalletId::get().into_account_truncating(); - balances.push((treasury_account, ONE_BILLION * UNIT)); + balances.push((treasury_account, INITIAL_TREASURY)); let config = RuntimeGenesisConfig { balances: BalancesConfig { balances, dev_accounts: None }, From 33c4e3e0e862777655c5be698e6e16e76ba05005 Mon Sep 17 00:00:00 2001 From: illuzen Date: Mon, 19 Jan 2026 16:30:11 +0800 Subject: [PATCH 11/27] update qp-poseidon version --- Cargo.lock | 41 ++++++++++++++--------------------------- Cargo.toml | 4 ++-- 2 files changed, 16 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f8d3202c..749680ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7234,7 +7234,7 @@ dependencies = [ "pallet-timestamp", "parity-scale-codec", "primitive-types 0.13.1", - "qp-poseidon-core 1.0.6 (git+https://github.com/Quantus-Network/qp-poseidon?branch=feat%2Ffunding_amount_quantization)", + "qp-poseidon-core", "qpow-math", "scale-info", "sp-arithmetic", @@ -7617,7 +7617,7 @@ dependencies = [ "qp-header", "qp-plonky2", "qp-poseidon", - "qp-poseidon-core 1.0.6 (git+https://github.com/Quantus-Network/qp-poseidon?branch=feat%2Ffunding_amount_quantization)", + "qp-poseidon-core", "qp-wormhole", "qp-wormhole-circuit", "qp-wormhole-circuit-builder", @@ -8842,7 +8842,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ "heck 0.5.0", - "itertools 0.10.5", + "itertools 0.14.0", "log", "multimap", "once_cell", @@ -8875,7 +8875,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.110", @@ -8927,7 +8927,7 @@ dependencies = [ "p3-goldilocks", "parity-scale-codec", "qp-poseidon", - "qp-poseidon-core 1.0.6 (git+https://github.com/Quantus-Network/qp-poseidon?branch=feat%2Ffunding_amount_quantization)", + "qp-poseidon-core", "scale-info", "serde", "serde_json", @@ -8984,14 +8984,15 @@ dependencies = [ [[package]] name = "qp-poseidon" -version = "1.0.6" -source = "git+https://github.com/Quantus-Network/qp-poseidon?branch=feat%2Ffunding_amount_quantization#acd1eec5bf54abc1d07c7046c90a4bae339eeb84" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4214ec389bff0c21c6ef815cf0ff00656586344dbe20f6441d23a1a6a7f56e84" dependencies = [ "log", "p3-field", "p3-goldilocks", "parity-scale-codec", - "qp-poseidon-core 1.0.6 (git+https://github.com/Quantus-Network/qp-poseidon?branch=feat%2Ffunding_amount_quantization)", + "qp-poseidon-core", "scale-info", "serde", "sp-core", @@ -9015,23 +9016,9 @@ dependencies = [ [[package]] name = "qp-poseidon-core" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b3fb9032ac9b197265da8fc8bdedd21ba76592ceefbf4a591286d47baec2d" -dependencies = [ - "p3-field", - "p3-goldilocks", - "p3-poseidon2", - "p3-symmetric", - "qp-plonky2", - "qp-poseidon-constants", - "rand_chacha 0.9.0", -] - -[[package]] -name = "qp-poseidon-core" -version = "1.0.6" -source = "git+https://github.com/Quantus-Network/qp-poseidon?branch=feat%2Ffunding_amount_quantization#acd1eec5bf54abc1d07c7046c90a4bae339eeb84" +checksum = "0f65766d6de64eff741c7f402002a3322f5e563d53e0e9040aeab4921ff24f2b" dependencies = [ "p3-field", "p3-goldilocks", @@ -9064,7 +9051,7 @@ dependencies = [ "hex", "hex-literal", "nam-tiny-hderive", - "qp-poseidon-core 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "qp-poseidon-core", "qp-rusty-crystals-dilithium", "rand_chacha 0.9.0", "rand_core 0.9.3", @@ -9139,7 +9126,7 @@ dependencies = [ "anyhow", "hex", "qp-plonky2", - "qp-poseidon-core 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "qp-poseidon-core", "serde", ] @@ -9152,7 +9139,7 @@ dependencies = [ "num-bigint", "num-traits", "primitive-types 0.13.1", - "qp-poseidon-core 1.0.6 (git+https://github.com/Quantus-Network/qp-poseidon?branch=feat%2Ffunding_amount_quantization)", + "qp-poseidon-core", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0b5a330b..f422a970 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -149,8 +149,8 @@ sp-consensus-qpow = { path = "./primitives/consensus/qpow", default-features = f # Quantus network dependencies qp-plonky2 = { version = "1.1.3", default-features = false } -qp-poseidon = { git = "https://github.com/Quantus-Network/qp-poseidon", branch = "feat/funding_amount_quantization", default-features = false } -qp-poseidon-core = { git = "https://github.com/Quantus-Network/qp-poseidon", package = "qp-poseidon-core", branch = "feat/funding_amount_quantization", default-features = false, features = [ +qp-poseidon = { version = "1.0.7", default-features = false } +qp-poseidon-core = { version = "1.0.7", package = "qp-poseidon-core", default-features = false, features = [ "p2", "p3", ] } From d584746eabf5f9f46ad51a2170edabf116f3b327 Mon Sep 17 00:00:00 2001 From: illuzen Date: Tue, 20 Jan 2026 11:37:35 +0800 Subject: [PATCH 12/27] made transfer count per-recipient --- pallets/mining-rewards/src/lib.rs | 1 - pallets/mining-rewards/src/mock.rs | 4 +--- pallets/wormhole/src/lib.rs | 7 ++++--- pallets/wormhole/src/tests.rs | 8 ++++---- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/pallets/mining-rewards/src/lib.rs b/pallets/mining-rewards/src/lib.rs index 66d985ac..e5a77546 100644 --- a/pallets/mining-rewards/src/lib.rs +++ b/pallets/mining-rewards/src/lib.rs @@ -27,7 +27,6 @@ pub mod pallet { }; use frame_system::pallet_prelude::*; use qp_poseidon::PoseidonHasher; - use qp_wormhole::TransferProofs; use qp_wormhole::TransferProofRecorder; use sp_consensus_pow::POW_ENGINE_ID; use sp_runtime::{ diff --git a/pallets/mining-rewards/src/mock.rs b/pallets/mining-rewards/src/mock.rs index 660ac3ca..b90560bc 100644 --- a/pallets/mining-rewards/src/mock.rs +++ b/pallets/mining-rewards/src/mock.rs @@ -11,7 +11,7 @@ use sp_runtime::{ app_crypto::sp_core, testing::H256, traits::{BlakeTwo256, IdentityLookup}, - BuildStorage, DigestItem, + BuildStorage, DigestItem, Permill, }; // Configure a mock runtime to test the pallet @@ -186,8 +186,6 @@ pub fn set_miner_digest(miner_account: sp_core::crypto::AccountId32) { // Helper function to create a block digest with a specific preimage pub fn set_miner_preimage_digest(preimage: [u8; 32]) { let pre_digest = DigestItem::PreRuntime(POW_ENGINE_ID, preimage.to_vec()); - let digest = Digest { logs: vec![pre_digest] }; - System::deposit_log(pre_digest); } diff --git a/pallets/wormhole/src/lib.rs b/pallets/wormhole/src/lib.rs index 260d732c..2f8076ac 100644 --- a/pallets/wormhole/src/lib.rs +++ b/pallets/wormhole/src/lib.rs @@ -179,7 +179,8 @@ pub mod pallet { /// Transfer count for all wormhole transfers #[pallet::storage] #[pallet::getter(fn transfer_count)] - pub type TransferCount = StorageValue<_, T::TransferCount, ValueQuery>; + pub type TransferCount = + StorageMap<_, Blake2_128Concat, T::WormholeAccountId, T::TransferCount, ValueQuery>; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] @@ -602,12 +603,12 @@ pub mod pallet { to: ::WormholeAccountId, amount: BalanceOf, ) -> DispatchResult { - let current_count = TransferCount::::get(); + let current_count = TransferCount::::get(&to); TransferProof::::insert( (asset_id.clone(), current_count, from.clone(), to.clone(), amount), (), ); - TransferCount::::put(current_count.saturating_add(T::TransferCount::one())); + TransferCount::::insert(&to, current_count.saturating_add(T::TransferCount::one())); if asset_id == AssetIdOf::::default() { Self::deposit_event(Event::::NativeTransferred { diff --git a/pallets/wormhole/src/tests.rs b/pallets/wormhole/src/tests.rs index 3454e681..7a8a648c 100644 --- a/pallets/wormhole/src/tests.rs +++ b/pallets/wormhole/src/tests.rs @@ -246,7 +246,7 @@ mod wormhole_tests { assert_ok!(Balances::mint_into(&alice, amount * 2)); - let count_before = Wormhole::transfer_count(); + let count_before = Wormhole::transfer_count(&bob); assert_ok!(Wormhole::transfer_native( frame_system::RawOrigin::Signed(alice.clone()).into(), bob.clone(), @@ -255,7 +255,7 @@ mod wormhole_tests { assert_eq!(Balances::balance(&alice), amount); assert_eq!(Balances::balance(&bob), amount); - assert_eq!(Wormhole::transfer_count(), count_before + 1); + assert_eq!(Wormhole::transfer_count(&bob), count_before + 1); assert!(Wormhole::transfer_proof((0u32, count_before, alice, bob, amount)).is_some()); }); } @@ -302,7 +302,7 @@ mod wormhole_tests { amount * 2, )); - let count_before = Wormhole::transfer_count(); + let count_before = Wormhole::transfer_count(&bob); assert_ok!(Wormhole::transfer_asset( frame_system::RawOrigin::Signed(alice.clone()).into(), asset_id, @@ -312,7 +312,7 @@ mod wormhole_tests { assert_eq!(Assets::balance(asset_id, &alice), amount); assert_eq!(Assets::balance(asset_id, &bob), amount); - assert_eq!(Wormhole::transfer_count(), count_before + 1); + assert_eq!(Wormhole::transfer_count(&bob), count_before + 1); assert!( Wormhole::transfer_proof((asset_id, count_before, alice, bob, amount)).is_some() ); From de5553a1214b901749b63d31a0191efd30cc22a0 Mon Sep 17 00:00:00 2001 From: Ethan Cemer Date: Tue, 20 Jan 2026 02:58:07 -0600 Subject: [PATCH 13/27] feat: enable wormhole verifier tests (#356) * bring back wormhole transfer proof generation tests * fmt --------- Co-authored-by: illuzen --- pallets/wormhole/Cargo.toml | 8 ++------ pallets/wormhole/src/tests.rs | 2 -- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/pallets/wormhole/Cargo.toml b/pallets/wormhole/Cargo.toml index 2c2da565..be13c325 100644 --- a/pallets/wormhole/Cargo.toml +++ b/pallets/wormhole/Cargo.toml @@ -20,12 +20,8 @@ qp-header = { workspace = true, features = ["serde"] } qp-poseidon.workspace = true qp-wormhole.workspace = true qp-wormhole-circuit = { workspace = true, default-features = false } -qp-wormhole-verifier = { workspace = true, default-features = false, features = [ - "no_random", -] } -qp-zk-circuits-common = { workspace = true, default-features = false, features = [ - "no_random", -] } +qp-wormhole-verifier = { workspace = true, default-features = false } +qp-zk-circuits-common = { workspace = true, default-features = false } scale-info = { workspace = true, default-features = false, features = [ "derive", ] } diff --git a/pallets/wormhole/src/tests.rs b/pallets/wormhole/src/tests.rs index 7a8a648c..ffc3e084 100644 --- a/pallets/wormhole/src/tests.rs +++ b/pallets/wormhole/src/tests.rs @@ -30,9 +30,7 @@ mod wormhole_tests { proof } - // Ignoring for now, will fix once the no_random feature issue is resolved for test dependencies #[test] - #[ignore] fn test_wormhole_transfer_proof_generation() { let alice = account_id(1); let secret: BytesDigest = [1u8; 32].try_into().expect("valid secret"); From 40978d62ee125c918c0093773fd3ec3cab902baf Mon Sep 17 00:00:00 2001 From: illuzen Date: Wed, 21 Jan 2026 00:48:54 +0800 Subject: [PATCH 14/27] remove painful test, we sent it to quantus-cli --- pallets/wormhole/common.bin | Bin 1905 -> 1905 bytes pallets/wormhole/src/lib.rs | 215 +++++++++++++++++++++---------- pallets/wormhole/src/mock.rs | 10 +- pallets/wormhole/src/tests.rs | 231 +--------------------------------- pallets/wormhole/verifier.bin | Bin 552 -> 552 bytes runtime/src/configs/mod.rs | 7 +- 6 files changed, 163 insertions(+), 300 deletions(-) diff --git a/pallets/wormhole/common.bin b/pallets/wormhole/common.bin index 7890c51d4946a4235c5af2aa7372969afe09f679..450d440e438b0c0b814fa2b9511ae2e41474f0c2 100644 GIT binary patch delta 14 Vcmey!_mOXdC^MtTW-;by762xu1P1^B delta 14 Vcmey!_mOXdC^Mt*W-;by762xo1O@;A diff --git a/pallets/wormhole/src/lib.rs b/pallets/wormhole/src/lib.rs index 2f8076ac..d4e7af76 100644 --- a/pallets/wormhole/src/lib.rs +++ b/pallets/wormhole/src/lib.rs @@ -45,6 +45,12 @@ pub fn get_aggregated_verifier() -> Result<&'static WormholeVerifier, &'static s AGGREGATED_VERIFIER.as_ref().ok_or("Aggregated verifier not available") } +/// Scale factor for quantizing amounts from 12 to 2 decimal places (10^10). +/// Amounts in the circuit are stored as u32 with 2 decimal places of precision. +/// On-chain amounts use 12 decimal places, so we multiply by this factor when +/// converting from circuit amounts to on-chain amounts. +pub const SCALE_DOWN_FACTOR: u128 = 10_000_000_000; + // We use a generic struct so we can pass the specific Key type to the hasher pub struct PoseidonStorageHasher(PhantomData); @@ -74,9 +80,8 @@ pub mod pallet { fungible::{Mutate, Unbalanced}, fungibles::{self, Inspect as FungiblesInspect, Mutate as FungiblesMutate}, tokens::Preservation, - Currency, ExistenceRequirement, WithdrawReasons, + Currency, }, - weights::WeightToFee, }; use frame_system::pallet_prelude::*; use qp_wormhole_circuit::inputs::{AggregatedPublicCircuitInputs, PublicCircuitInputs}; @@ -88,7 +93,6 @@ pub mod pallet { InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity, ValidTransaction, }, - Perbill, }; pub type BalanceOf = @@ -142,11 +146,18 @@ pub mod pallet { #[pallet::constant] type MintingAccount: Get<::AccountId>; + /// Minimum transfer amount required for proof verification + #[pallet::constant] + type MinimumTransferAmount: Get>; + + /// Volume fee rate in basis points (1 basis point = 0.01%). + /// This must match the fee rate used in proof generation. + #[pallet::constant] + type VolumeFeeRateBps: Get; + /// Weight information for pallet operations. type WeightInfo: WeightInfo; - type WeightToFee: WeightToFee>; - /// Override system AccountId to make it felts encodable type WormholeAccountId: Parameter + Member @@ -221,6 +232,9 @@ pub mod pallet { AggregatedProofDeserializationFailed, AggregatedVerificationFailed, InvalidAggregatedPublicInputs, + TransferAmountBelowMinimum, + /// The volume fee rate in the proof doesn't match the configured rate + InvalidVolumeFeeRate, } #[pallet::call] @@ -229,10 +243,6 @@ pub mod pallet { #[pallet::weight(::WeightInfo::verify_wormhole_proof())] pub fn verify_wormhole_proof(origin: OriginFor, proof_bytes: Vec) -> DispatchResult { ensure_none(origin)?; - // Note: The funding_amount in public inputs is expected to be quantized (i.e., scaled - // down from 12 to 2 decimals points of precision) so we need to scale it back up - // here to get the actual amount the chain expects with 12 decimal places of precision. - const SCALE_DOWN_FACTOR: u128 = 10_000_000_000; // 10^10; let verifier = crate::get_wormhole_verifier().map_err(|_| Error::::VerifierNotAvailable)?; @@ -281,8 +291,15 @@ pub mod pallet { // Mark nullifier as used UsedNullifiers::::insert(nullifier_bytes, true); + // Verify the volume fee rate matches our configured rate + ensure!( + public_inputs.volume_fee_bps == T::VolumeFeeRateBps::get(), + Error::::InvalidVolumeFeeRate + ); + + // The output_amount is what the user receives after fee deduction (already enforced by circuit) let exit_balance_u128 = - (public_inputs.funding_amount as u128).saturating_mul(SCALE_DOWN_FACTOR); + (public_inputs.output_amount as u128).saturating_mul(crate::SCALE_DOWN_FACTOR); // Convert to Balance type let exit_balance: BalanceOf = @@ -298,12 +315,21 @@ pub mod pallet { let asset_id_u32 = public_inputs.asset_id; let asset_id: AssetIdOf = asset_id_u32.into(); - // Calculate fees first - let weight = ::WeightInfo::verify_wormhole_proof(); - let weight_fee = T::WeightToFee::weight_to_fee(&weight); - let volume_fee_perbill = Perbill::from_rational(1u32, 1000u32); - let volume_fee = volume_fee_perbill * exit_balance; - let total_fee = weight_fee.saturating_add(volume_fee); + // Ensure transfer amount meets minimum requirement + ensure!( + exit_balance >= T::MinimumTransferAmount::get(), + Error::::TransferAmountBelowMinimum + ); + + // Compute the fee to mint to the block author + // fee = output_amount * volume_fee_bps / (10000 - volume_fee_bps) + let fee_bps = T::VolumeFeeRateBps::get() as u128; + let fee_u128 = exit_balance_u128 + .saturating_mul(fee_bps) + .checked_div(10000u128.saturating_sub(fee_bps)) + .unwrap_or(0); + let total_fee: BalanceOf = + fee_u128.try_into().map_err(|_| Error::::InvalidPublicInputs)?; // Handle native (asset_id = 0) or asset transfers if asset_id == AssetIdOf::::default() { @@ -316,16 +342,21 @@ pub mod pallet { frame_support::traits::tokens::Precision::Exact, )?; - // Withdraw fee from exit account if fees are non-zero - // This creates a negative imbalance that will be handled by the transaction payment - // pallet + // Mint volume fee to block author if !total_fee.is_zero() { - let _fee_imbalance = T::Currency::withdraw( - &exit_account, - total_fee, - WithdrawReasons::TRANSACTION_PAYMENT, - ExistenceRequirement::KeepAlive, - )?; + if let Some(author) = frame_system::Pallet::::digest() + .logs + .iter() + .find_map(|item| item.as_pre_runtime()) + .and_then(|(_, data)| { + ::AccountId::decode(&mut &data[..]).ok() + }) { + >::increase_balance( + &author, + total_fee, + frame_support::traits::tokens::Precision::Exact, + )?; + } } } else { // Asset transfer @@ -336,15 +367,21 @@ pub mod pallet { asset_balance, )?; - // For assets, we still need to charge fees in native currency - // The exit account must have enough native balance to pay fees + // Mint volume fee to block author (fee is in native currency) if !total_fee.is_zero() { - let _fee_imbalance = T::Currency::withdraw( - &exit_account, - total_fee, - WithdrawReasons::TRANSACTION_PAYMENT, - ExistenceRequirement::AllowDeath, - )?; + if let Some(author) = frame_system::Pallet::::digest() + .logs + .iter() + .find_map(|item| item.as_pre_runtime()) + .and_then(|(_, data)| { + ::AccountId::decode(&mut &data[..]).ok() + }) { + >::increase_balance( + &author, + total_fee, + frame_support::traits::tokens::Precision::Exact, + )?; + } } } @@ -486,18 +523,30 @@ pub mod pallet { Error::::InvalidPublicInputs ); - // For now, aggregated proofs only support native token (asset_id = 0) - // TODO: Add asset_id support when the circuit is updated - let asset_id = AssetIdOf::::default(); + // Extract asset_id from aggregated public inputs + let asset_id: AssetIdOf = aggregated_inputs.asset_id.into(); + + // Verify the volume fee rate matches our configured rate + ensure!( + aggregated_inputs.volume_fee_bps == T::VolumeFeeRateBps::get(), + Error::::InvalidVolumeFeeRate + ); // Get the minting account for recording transfer proofs let mint_account = T::MintingAccount::get(); - // Process each exit account + // First pass: compute total exit amount and prepare account data + let mut total_exit_amount: BalanceOf = Zero::zero(); + let mut processed_accounts: Vec<( + ::AccountId, + BalanceOf, + )> = Vec::with_capacity(aggregated_inputs.account_data.len()); + for account_data in &aggregated_inputs.account_data { - // Convert funding amount to Balance type - let exit_balance: BalanceOf = account_data - .summed_funding_amount + // Convert output amount to Balance type (scale up from quantized value) + let exit_balance_u128 = (account_data.summed_output_amount as u128) + .saturating_mul(crate::SCALE_DOWN_FACTOR); + let exit_balance: BalanceOf = exit_balance_u128 .try_into() .map_err(|_| Error::::InvalidAggregatedPublicInputs)?; @@ -510,30 +559,47 @@ pub mod pallet { ::AccountId::decode(&mut &exit_account_bytes[..]) .map_err(|_| Error::::InvalidAggregatedPublicInputs)?; - // Calculate fees - let weight = ::WeightInfo::verify_wormhole_proof(); - let weight_fee = T::WeightToFee::weight_to_fee(&weight); - let volume_fee_perbill = Perbill::from_rational(1u32, 1000u32); - let volume_fee = volume_fee_perbill * exit_balance; - let total_fee = weight_fee.saturating_add(volume_fee); - - // Native token transfer - mint tokens to the exit account - // Note: Aggregated proofs currently only support native tokens (asset_id = 0) - // TODO: Add asset support when the circuit includes asset_id in - // AggregatedPublicCircuitInputs - >::increase_balance( - &exit_account, - exit_balance, - frame_support::traits::tokens::Precision::Exact, - )?; + total_exit_amount = total_exit_amount.saturating_add(exit_balance); + processed_accounts.push((exit_account, exit_balance)); + } - // Withdraw fee from exit account if fees are non-zero - if !total_fee.is_zero() { - let _fee_imbalance = T::Currency::withdraw( - &exit_account, - total_fee, - WithdrawReasons::TRANSACTION_PAYMENT, - ExistenceRequirement::KeepAlive, + // Check minimum against total aggregated amount + ensure!( + total_exit_amount >= T::MinimumTransferAmount::get(), + Error::::TransferAmountBelowMinimum + ); + + // Compute the total fee to mint to the block author + // fee = total_output_amount * volume_fee_bps / (10000 - volume_fee_bps) + let fee_bps = T::VolumeFeeRateBps::get() as u128; + let total_exit_u128: u128 = total_exit_amount + .try_into() + .map_err(|_| Error::::InvalidAggregatedPublicInputs)?; + let total_fee_u128 = total_exit_u128 + .saturating_mul(fee_bps) + .checked_div(10000u128.saturating_sub(fee_bps)) + .unwrap_or(0); + let total_fee: BalanceOf = total_fee_u128 + .try_into() + .map_err(|_| Error::::InvalidAggregatedPublicInputs)?; + + // Second pass: process transfers and record proofs + for (exit_account, exit_balance) in &processed_accounts { + // Handle native (asset_id = 0) or asset transfers + if asset_id == AssetIdOf::::default() { + // Native token transfer - mint tokens to the exit account + >::increase_balance( + exit_account, + *exit_balance, + frame_support::traits::tokens::Precision::Exact, + )?; + } else { + // Asset transfer + let asset_balance: AssetBalanceOf = (*exit_balance).into(); + >::mint_into( + asset_id.clone(), + exit_account, + asset_balance, )?; } @@ -541,12 +607,29 @@ pub mod pallet { Self::record_transfer( asset_id.clone(), mint_account.clone().into(), - exit_account.into(), - exit_balance, + exit_account.clone().into(), + *exit_balance, )?; // Emit event for each exit account - Self::deposit_event(Event::ProofVerified { exit_amount: exit_balance }); + Self::deposit_event(Event::ProofVerified { exit_amount: *exit_balance }); + } + + // Mint volume fee to block author + if !total_fee.is_zero() { + if let Some(author) = frame_system::Pallet::::digest() + .logs + .iter() + .find_map(|item| item.as_pre_runtime()) + .and_then(|(_, data)| { + ::AccountId::decode(&mut &data[..]).ok() + }) { + >::increase_balance( + &author, + total_fee, + frame_support::traits::tokens::Precision::Exact, + )?; + } } Ok(()) diff --git a/pallets/wormhole/src/mock.rs b/pallets/wormhole/src/mock.rs index 8527f4ff..b4219374 100644 --- a/pallets/wormhole/src/mock.rs +++ b/pallets/wormhole/src/mock.rs @@ -1,8 +1,7 @@ -use crate as pallet_wormhole; +use crate::{self as pallet_wormhole, SCALE_DOWN_FACTOR}; use frame_support::{ construct_runtime, parameter_types, traits::{ConstU128, ConstU32, Everything}, - weights::IdentityFee, }; use frame_system::mocking::MockUncheckedExtrinsic; use qp_poseidon::PoseidonHasher; @@ -118,15 +117,20 @@ parameter_types! { 231, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]); + /// Minimum transfer amount: 1 token (100 quantized units × SCALE_DOWN_FACTOR) + pub const MinimumTransferAmount: Balance = 100 * SCALE_DOWN_FACTOR; + /// Volume fee rate in basis points (10 bps = 0.1%) + pub const VolumeFeeRateBps: u32 = 10; } impl pallet_wormhole::Config for Test { type WeightInfo = crate::weights::SubstrateWeight; - type WeightToFee = IdentityFee; type Currency = Balances; type Assets = Assets; type TransferCount = u64; type MintingAccount = MintingAccount; + type MinimumTransferAmount = MinimumTransferAmount; + type VolumeFeeRateBps = VolumeFeeRateBps; type WormholeAccountId = AccountId; } diff --git a/pallets/wormhole/src/tests.rs b/pallets/wormhole/src/tests.rs index ffc3e084..37a96598 100644 --- a/pallets/wormhole/src/tests.rs +++ b/pallets/wormhole/src/tests.rs @@ -1,239 +1,10 @@ #[cfg(test)] mod wormhole_tests { - use crate::{get_wormhole_verifier, mock::*}; - use codec::Encode; + use crate::mock::*; use frame_support::{ assert_ok, traits::fungible::{Inspect, Mutate}, }; - use plonky2::plonk::circuit_data::CircuitConfig; - use qp_poseidon::PoseidonHasher; - use qp_wormhole_circuit::{ - inputs::{CircuitInputs, PrivateCircuitInputs, PublicCircuitInputs}, - nullifier::Nullifier, - }; - use qp_wormhole_prover::WormholeProver; - use qp_wormhole_verifier::ProofWithPublicInputs; - use qp_zk_circuits_common::{ - circuit::{C, F}, - storage_proof::prepare_proof_for_circuit, - utils::{digest_felts_to_bytes, BytesDigest, Digest}, - }; - use sp_runtime::{traits::Header, DigestItem}; - const SCALE_DOWN_FACTOR: u128 = 10_000_000_000; // 10^10; - - fn generate_proof(inputs: CircuitInputs) -> ProofWithPublicInputs { - let config = CircuitConfig::standard_recursion_zk_config(); - let prover = WormholeProver::new(config); - let prover_next = prover.commit(&inputs).expect("proof failed"); - let proof = prover_next.prove().expect("valid proof"); - proof - } - - #[test] - fn test_wormhole_transfer_proof_generation() { - let alice = account_id(1); - let secret: BytesDigest = [1u8; 32].try_into().expect("valid secret"); - let unspendable_account = - qp_wormhole_circuit::unspendable_account::UnspendableAccount::from_secret(secret) - .account_id; - let unspendable_account_bytes_digest = digest_felts_to_bytes(unspendable_account); - let unspendable_account_bytes: [u8; 32] = unspendable_account_bytes_digest - .as_ref() - .try_into() - .expect("BytesDigest is always 32 bytes"); - let unspendable_account_id = AccountId::new(unspendable_account_bytes); - let exit_account_id = AccountId::new([42u8; 32]); - let funding_amount = 1_000_000_000_001u128; - - let mut ext = new_test_ext(); - - let (storage_key, state_root, leaf_hash, event_transfer_count, header) = - ext.execute_with(|| { - System::set_block_number(1); - - let pre_runtime_data = vec![ - 233, 182, 183, 107, 158, 1, 115, 19, 219, 126, 253, 86, 30, 208, 176, 70, 21, - 45, 180, 229, 9, 62, 91, 4, 6, 53, 245, 52, 48, 38, 123, 225, - ]; - let seal_data = vec![ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 77, 142, - ]; - - System::deposit_log(DigestItem::PreRuntime(*b"pow_", pre_runtime_data)); - System::deposit_log(DigestItem::Seal(*b"pow_", seal_data)); - - assert_ok!(Balances::mint_into(&alice, funding_amount)); - assert_ok!(Wormhole::transfer_native( - frame_system::RawOrigin::Signed(alice.clone()).into(), - unspendable_account_id.clone(), - funding_amount, - )); - - let event_transfer_count = 0u64; - - let leaf_hash = PoseidonHasher::hash_storage::>( - &( - 0u32, - event_transfer_count, - alice.clone(), - unspendable_account_id.clone(), - funding_amount, - ) - .encode(), - ); - - let proof_address = crate::pallet::TransferProof::::hashed_key_for(&( - 0u32, - event_transfer_count, - alice.clone(), - unspendable_account_id.clone(), - funding_amount, - )); - let mut storage_key = proof_address; - storage_key.extend_from_slice(&leaf_hash); - - let header = System::finalize(); - let state_root = *header.state_root(); - - (storage_key, state_root, leaf_hash, event_transfer_count, header) - }); - - use sp_state_machine::prove_read; - let proof = prove_read(ext.as_backend(), &[&storage_key]) - .expect("failed to generate storage proof"); - - let proof_nodes_vec: Vec> = proof.iter_nodes().map(|n| n.to_vec()).collect(); - - let processed_storage_proof = - prepare_proof_for_circuit(proof_nodes_vec, hex::encode(&state_root), leaf_hash) - .expect("failed to prepare proof for circuit"); - - let parent_hash = *header.parent_hash(); - let extrinsics_root = *header.extrinsics_root(); - let digest = header.digest().encode(); - let digest_array: [u8; 110] = digest.try_into().expect("digest should be 110 bytes"); - let block_number: u32 = (*header.number()).try_into().expect("block number fits in u32"); - - let block_hash = header.hash(); - - let funding_amount_quantized: u32 = (funding_amount / SCALE_DOWN_FACTOR as u128) - .try_into() - .expect("funding amount fits in u32 after scaling down"); - - let circuit_inputs = CircuitInputs { - private: PrivateCircuitInputs { - secret, - storage_proof: processed_storage_proof, - transfer_count: event_transfer_count, - funding_account: BytesDigest::try_from(alice.as_ref() as &[u8]) - .expect("account is 32 bytes"), - unspendable_account: Digest::from(unspendable_account).into(), - state_root: BytesDigest::try_from(state_root.as_ref()) - .expect("state root is 32 bytes"), - extrinsics_root: BytesDigest::try_from(extrinsics_root.as_ref()) - .expect("extrinsics root is 32 bytes"), - digest: digest_array, - }, - public: PublicCircuitInputs { - asset_id: 0u32, - funding_amount: funding_amount_quantized, - nullifier: Nullifier::from_preimage(secret, event_transfer_count).hash.into(), - exit_account: BytesDigest::try_from(exit_account_id.as_ref() as &[u8]) - .expect("account is 32 bytes"), - block_hash: BytesDigest::try_from(block_hash.as_ref()) - .expect("block hash is 32 bytes"), - parent_hash: BytesDigest::try_from(parent_hash.as_ref()) - .expect("parent hash is 32 bytes"), - block_number, - }, - }; - - let proof = generate_proof(circuit_inputs); - - let public_inputs = - PublicCircuitInputs::try_from(&proof).expect("failed to parse public inputs"); - - assert_eq!(public_inputs.funding_amount, funding_amount_quantized); - assert_eq!( - public_inputs.exit_account, - BytesDigest::try_from(exit_account_id.as_ref() as &[u8]).unwrap() - ); - - let verifier = get_wormhole_verifier().expect("verifier should be available"); - verifier.verify(proof.clone()).expect("proof should verify"); - - let proof_bytes = proof.to_bytes(); - - new_test_ext().execute_with(|| { - System::set_block_number(1); - - let pre_runtime_data = vec![ - 233, 182, 183, 107, 158, 1, 115, 19, 219, 126, 253, 86, 30, 208, 176, 70, 21, 45, - 180, 229, 9, 62, 91, 4, 6, 53, 245, 52, 48, 38, 123, 225, - ]; - let seal_data = vec![ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 30, 77, 142, - ]; - - System::deposit_log(DigestItem::PreRuntime(*b"pow_", pre_runtime_data)); - System::deposit_log(DigestItem::Seal(*b"pow_", seal_data)); - - assert_ok!(Balances::mint_into(&alice, funding_amount)); - assert_ok!(Wormhole::transfer_native( - frame_system::RawOrigin::Signed(alice.clone()).into(), - unspendable_account_id.clone(), - funding_amount, - )); - - let block_1_header = System::finalize(); - - System::reset_events(); - System::initialize(&2, &block_1_header.hash(), block_1_header.digest()); - - let balance_before = Balances::balance(&exit_account_id); - assert_eq!(balance_before, 0); - - assert_ok!(Wormhole::verify_wormhole_proof( - frame_system::RawOrigin::None.into(), - proof_bytes.clone() - )); - - let balance_after = Balances::balance(&exit_account_id); - - assert!(balance_after > 0, "Exit account should have received funds"); - assert!( - balance_after < funding_amount, - "Exit account balance should be less than funding amount due to fees" - ); - }); - - new_test_ext().execute_with(|| { - System::set_block_number(1); - - let pre_runtime_data = vec![1u8; 32]; - let seal_data = vec![2u8; 64]; - - System::deposit_log(DigestItem::PreRuntime(*b"pow_", pre_runtime_data)); - System::deposit_log(DigestItem::Seal(*b"pow_", seal_data)); - - let different_header = System::finalize(); - - System::reset_events(); - System::initialize(&2, &different_header.hash(), different_header.digest()); - - let result = Wormhole::verify_wormhole_proof( - frame_system::RawOrigin::None.into(), - proof_bytes.clone(), - ); - - assert!(result.is_err(), "Proof verification should fail with mismatched state"); - }); - } #[test] fn transfer_native_works() { diff --git a/pallets/wormhole/verifier.bin b/pallets/wormhole/verifier.bin index 53dffca148097185ec20ee30223c2fa2466a7051..2ea545d3a4bac6ae74bdcc98cf3f35ad8ced6d00 100644 GIT binary patch literal 552 zcmV+@0@wWn0000000011em_y*qDT`s9_o!l_)%M7@KiK7_3QEz;216^SRD%D$L?Y- zO}XkWT(G&A$V!dh2lj@$L0A@OCZv@Q&>Go1m_gK_z`t8DG+8bduF$rA4h8Q*{JlY{ zS1;#Hk1{;2x1jg6HK$P^kSjY()19Y|>^lSiL=am9YZaGZ26TYiBvNKT z?|oeAm4T1448tI1W?+`taTBz~`nSQj_s@eqzl%nv(%<9#qOowW>d6)nsAf|ik_A!9 zjLxi#I8E2f*DcNgbsL&$$2D&$J`U0lve- z4ouvvBD&+rFI>`$aCypS?o<{h22X13Pefah`xIRTpO&PYr8@10W&;SCENM2T*6VPdwx3h$3fq;ts>-*p)5r^+9IiGZ_k#BW zZQG$mu)+#c1HC478ETFmTn=hv$~s7a*gbK*XiYoEx4MEwAG1FwEAXXgO;_eo^U;@k zskWFcEuO@VN)DUk1mGAvhkE|~N?Bl#^hl6hDTWrUMOWJ>hx$md_}1^K&Lf&Ni0o%u q5o-$ZV|I0|fN{|Q3ejg}wDcPTel2C9Md}1t1lwwDK6{=NL#Q-BT@Gge literal 552 zcmV+@0@wWn000000001+bd1qbfvYTdvH&*2(43A?Jm`=gbY4f60*zOeTN83#IsYoc z^z>v8o#$n^vlrngH;dc8ceMCZerJZ0H2$ZxN)$1SjFd zsyBCIT+tFckUbXSN02e4a7*mTNKm(Kc%J($nDO?JCNJ^8JuAzq4&kYeHpNKK7fPD! zw&iA-2!0y*jV;c+3fBAF#9k{V)btkzzaj)>U{`sJ(S#?d%|_KzeW2m|cRe}x($ArB z86~y1M!R+E)#A5 diff --git a/runtime/src/configs/mod.rs b/runtime/src/configs/mod.rs index 8e669118..eaced7a4 100644 --- a/runtime/src/configs/mod.rs +++ b/runtime/src/configs/mod.rs @@ -634,14 +634,19 @@ impl TryFrom for pallet_assets::Call { parameter_types! { pub WormholeMintingAccount: AccountId = PalletId(*b"wormhole").into_account_truncating(); + /// Minimum transfer amount: 1 token (100 quantized units × SCALE_DOWN_FACTOR) + pub const MinimumTransferAmount: Balance = 100 * pallet_wormhole::SCALE_DOWN_FACTOR; + /// Volume fee rate in basis points (10 bps = 0.1%) + pub const VolumeFeeRateBps: u32 = 10; } impl pallet_wormhole::Config for Runtime { type MintingAccount = WormholeMintingAccount; + type MinimumTransferAmount = MinimumTransferAmount; + type VolumeFeeRateBps = VolumeFeeRateBps; type WeightInfo = (); type Currency = Balances; type Assets = Assets; type TransferCount = u64; type WormholeAccountId = AccountId32; - type WeightToFee = IdentityFee; } From 5df799ff8c39a6247b96bc4564535787295354c0 Mon Sep 17 00:00:00 2001 From: illuzen Date: Wed, 21 Jan 2026 01:16:13 +0800 Subject: [PATCH 15/27] burn half the volume fee --- Cargo.lock | 20 ++++----- Cargo.toml | 10 ++--- pallets/wormhole/src/lib.rs | 82 ++++++++++++++++++++++++++++++------ pallets/wormhole/src/mock.rs | 4 ++ runtime/src/configs/mod.rs | 3 ++ 5 files changed, 91 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b57f657d..ea7321b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9077,8 +9077,8 @@ version = "0.1.0" [[package]] name = "qp-wormhole-circuit" -version = "0.1.7" -source = "git+https://github.com/Quantus-Network/qp-zk-circuits?branch=feat%2Fu32_funding_amount#507d2dcefd5fd7d87d42a57b49b8bd7e40200a0e" +version = "0.1.8" +source = "git+https://github.com/Quantus-Network/qp-zk-circuits?branch=illuzen%2Fagg-fees#e5cd5d36544afe233dcc22312e65530a310263f7" dependencies = [ "anyhow", "hex", @@ -9088,8 +9088,8 @@ dependencies = [ [[package]] name = "qp-wormhole-circuit-builder" -version = "0.1.7" -source = "git+https://github.com/Quantus-Network/qp-zk-circuits?branch=feat%2Fu32_funding_amount#507d2dcefd5fd7d87d42a57b49b8bd7e40200a0e" +version = "0.1.8" +source = "git+https://github.com/Quantus-Network/qp-zk-circuits?branch=illuzen%2Fagg-fees#e5cd5d36544afe233dcc22312e65530a310263f7" dependencies = [ "anyhow", "qp-plonky2", @@ -9099,8 +9099,8 @@ dependencies = [ [[package]] name = "qp-wormhole-prover" -version = "0.1.7" -source = "git+https://github.com/Quantus-Network/qp-zk-circuits?branch=feat%2Fu32_funding_amount#507d2dcefd5fd7d87d42a57b49b8bd7e40200a0e" +version = "0.1.8" +source = "git+https://github.com/Quantus-Network/qp-zk-circuits?branch=illuzen%2Fagg-fees#e5cd5d36544afe233dcc22312e65530a310263f7" dependencies = [ "anyhow", "qp-plonky2", @@ -9110,8 +9110,8 @@ dependencies = [ [[package]] name = "qp-wormhole-verifier" -version = "0.1.7" -source = "git+https://github.com/Quantus-Network/qp-zk-circuits?branch=feat%2Fu32_funding_amount#507d2dcefd5fd7d87d42a57b49b8bd7e40200a0e" +version = "0.1.8" +source = "git+https://github.com/Quantus-Network/qp-zk-circuits?branch=illuzen%2Fagg-fees#e5cd5d36544afe233dcc22312e65530a310263f7" dependencies = [ "anyhow", "qp-plonky2", @@ -9121,8 +9121,8 @@ dependencies = [ [[package]] name = "qp-zk-circuits-common" -version = "0.1.7" -source = "git+https://github.com/Quantus-Network/qp-zk-circuits?branch=feat%2Fu32_funding_amount#507d2dcefd5fd7d87d42a57b49b8bd7e40200a0e" +version = "0.1.8" +source = "git+https://github.com/Quantus-Network/qp-zk-circuits?branch=illuzen%2Fagg-fees#e5cd5d36544afe233dcc22312e65530a310263f7" dependencies = [ "anyhow", "hex", diff --git a/Cargo.toml b/Cargo.toml index f422a970..faadbfc2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -156,11 +156,11 @@ qp-poseidon-core = { version = "1.0.7", package = "qp-poseidon-core", default-fe ] } qp-rusty-crystals-dilithium = { version = "2.0.0", default-features = false } qp-rusty-crystals-hdwallet = { version = "1.0.0" } -qp-wormhole-circuit = { git = "https://github.com/Quantus-Network/qp-zk-circuits", package = "qp-wormhole-circuit", branch = "feat/u32_funding_amount", default-features = false } -qp-wormhole-circuit-builder = { git = "https://github.com/Quantus-Network/qp-zk-circuits", package = "qp-wormhole-circuit-builder", branch = "feat/u32_funding_amount", default-features = false } -qp-wormhole-prover = { git = "https://github.com/Quantus-Network/qp-zk-circuits", package = "qp-wormhole-prover", branch = "feat/u32_funding_amount", default-features = false } -qp-wormhole-verifier = { git = "https://github.com/Quantus-Network/qp-zk-circuits", package = "qp-wormhole-verifier", branch = "feat/u32_funding_amount", default-features = false } -qp-zk-circuits-common = { git = "https://github.com/Quantus-Network/qp-zk-circuits", package = "qp-zk-circuits-common", branch = "feat/u32_funding_amount", default-features = false } +qp-wormhole-circuit = { git = "https://github.com/Quantus-Network/qp-zk-circuits", branch = "illuzen/agg-fees", default-features = false } +qp-wormhole-circuit-builder = { git = "https://github.com/Quantus-Network/qp-zk-circuits", branch = "illuzen/agg-fees", default-features = false } +qp-wormhole-prover = { git = "https://github.com/Quantus-Network/qp-zk-circuits", branch = "illuzen/agg-fees", default-features = false } +qp-wormhole-verifier = { git = "https://github.com/Quantus-Network/qp-zk-circuits", branch = "illuzen/agg-fees", default-features = false } +qp-zk-circuits-common = { git = "https://github.com/Quantus-Network/qp-zk-circuits", branch = "illuzen/agg-fees", default-features = false } # polkadot-sdk dependencies frame-benchmarking = { version = "41.0.0", default-features = false } diff --git a/pallets/wormhole/src/lib.rs b/pallets/wormhole/src/lib.rs index d4e7af76..6bc6b434 100644 --- a/pallets/wormhole/src/lib.rs +++ b/pallets/wormhole/src/lib.rs @@ -93,6 +93,7 @@ pub mod pallet { InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity, ValidTransaction, }, + Permill, }; pub type BalanceOf = @@ -155,6 +156,11 @@ pub mod pallet { #[pallet::constant] type VolumeFeeRateBps: Get; + /// Proportion of volume fees to burn (not mint). The remainder goes to the block author. + /// Example: Permill::from_percent(50) means 50% burned, 50% to miner. + #[pallet::constant] + type VolumeFeesBurnRate: Get; + /// Weight information for pallet operations. type WeightInfo: WeightInfo; @@ -321,15 +327,30 @@ pub mod pallet { Error::::TransferAmountBelowMinimum ); - // Compute the fee to mint to the block author + // Compute the total fee that was deducted from input to get output // fee = output_amount * volume_fee_bps / (10000 - volume_fee_bps) let fee_bps = T::VolumeFeeRateBps::get() as u128; let fee_u128 = exit_balance_u128 .saturating_mul(fee_bps) .checked_div(10000u128.saturating_sub(fee_bps)) .unwrap_or(0); - let total_fee: BalanceOf = - fee_u128.try_into().map_err(|_| Error::::InvalidPublicInputs)?; + + // Fee distribution: configurable portion burned, remainder to miner + // burn_rate determines what proportion is burned (reduces total issuance) + let burn_rate = T::VolumeFeesBurnRate::get(); + let burn_amount_u128 = burn_rate * fee_u128; + let miner_fee_u128 = fee_u128.saturating_sub(burn_amount_u128); + let miner_fee: BalanceOf = + miner_fee_u128.try_into().map_err(|_| Error::::InvalidPublicInputs)?; + let burn_amount: BalanceOf = + burn_amount_u128.try_into().map_err(|_| Error::::InvalidPublicInputs)?; + + // Burn the burned portion by reducing total issuance + // This offsets the supply increase from minting exit_balance + miner_fee + // The PositiveImbalance is dropped, which is a no-op (already reduced issuance) + if !burn_amount.is_zero() { + let _ = >::burn(burn_amount); + } // Handle native (asset_id = 0) or asset transfers if asset_id == AssetIdOf::::default() { @@ -342,8 +363,9 @@ pub mod pallet { frame_support::traits::tokens::Precision::Exact, )?; - // Mint volume fee to block author - if !total_fee.is_zero() { + // Mint miner's portion of volume fee to block author + // The remaining portion (fee - miner_fee) is not minted, effectively burned + if !miner_fee.is_zero() { if let Some(author) = frame_system::Pallet::::digest() .logs .iter() @@ -353,7 +375,7 @@ pub mod pallet { }) { >::increase_balance( &author, - total_fee, + miner_fee, frame_support::traits::tokens::Precision::Exact, )?; } @@ -367,8 +389,9 @@ pub mod pallet { asset_balance, )?; - // Mint volume fee to block author (fee is in native currency) - if !total_fee.is_zero() { + // Mint miner's portion of volume fee to block author (fee is in native currency) + // The remaining portion (fee - miner_fee) is not minted, effectively burned + if !miner_fee.is_zero() { if let Some(author) = frame_system::Pallet::::digest() .logs .iter() @@ -378,7 +401,7 @@ pub mod pallet { }) { >::increase_balance( &author, - total_fee, + miner_fee, frame_support::traits::tokens::Precision::Exact, )?; } @@ -569,8 +592,9 @@ pub mod pallet { Error::::TransferAmountBelowMinimum ); - // Compute the total fee to mint to the block author + // Compute the total fee from the input amounts // fee = total_output_amount * volume_fee_bps / (10000 - volume_fee_bps) + // This is the fee that was deducted from input to get output. let fee_bps = T::VolumeFeeRateBps::get() as u128; let total_exit_u128: u128 = total_exit_amount .try_into() @@ -583,6 +607,37 @@ pub mod pallet { .try_into() .map_err(|_| Error::::InvalidAggregatedPublicInputs)?; + // Fee distribution: configurable portion burned, remainder to miner + // + // Original deposit locked `input_amount` in an unspendable account (tokens still exist). + // On exit we mint `output_amount` to user, where: input = output + fee + // + // Fee split (controlled by VolumeFeesBurnRate): + // - burn_amount = fee * burn_rate (reduces total issuance via Currency::burn) + // - miner_fee = fee - burn_amount (minted to block author via increase_balance) + // + // Supply accounting: + // - Minting exit amounts: increases total issuance by sum(output_amounts) + // - Minting miner fee: increases balance but NOT issuance (increase_balance) + // - Burning: decreases total issuance by burn_amount + // - Net change: +sum(output_amounts) - burn_amount + let burn_rate = T::VolumeFeesBurnRate::get(); + let burn_amount_u128 = burn_rate * total_fee_u128; + let miner_fee_u128 = total_fee_u128.saturating_sub(burn_amount_u128); + let miner_fee: BalanceOf = miner_fee_u128 + .try_into() + .map_err(|_| Error::::InvalidAggregatedPublicInputs)?; + let burn_amount: BalanceOf = burn_amount_u128 + .try_into() + .map_err(|_| Error::::InvalidAggregatedPublicInputs)?; + + // Burn the burned portion by reducing total issuance + // This offsets the supply increase from minting exit amounts + miner_fee + // The PositiveImbalance is dropped, which is a no-op (already reduced issuance) + if !burn_amount.is_zero() { + let _ = >::burn(burn_amount); + } + // Second pass: process transfers and record proofs for (exit_account, exit_balance) in &processed_accounts { // Handle native (asset_id = 0) or asset transfers @@ -615,8 +670,9 @@ pub mod pallet { Self::deposit_event(Event::ProofVerified { exit_amount: *exit_balance }); } - // Mint volume fee to block author - if !total_fee.is_zero() { + // Mint miner's portion of volume fee to block author + // The remaining portion (fee - miner_fee) is not minted, effectively burned + if !miner_fee.is_zero() { if let Some(author) = frame_system::Pallet::::digest() .logs .iter() @@ -626,7 +682,7 @@ pub mod pallet { }) { >::increase_balance( &author, - total_fee, + miner_fee, frame_support::traits::tokens::Precision::Exact, )?; } diff --git a/pallets/wormhole/src/mock.rs b/pallets/wormhole/src/mock.rs index b4219374..9c56aa7e 100644 --- a/pallets/wormhole/src/mock.rs +++ b/pallets/wormhole/src/mock.rs @@ -6,6 +6,7 @@ use frame_support::{ use frame_system::mocking::MockUncheckedExtrinsic; use qp_poseidon::PoseidonHasher; use sp_core::H256; +use sp_runtime::Permill; use sp_runtime::{traits::IdentityLookup, BuildStorage}; construct_runtime!( @@ -121,6 +122,8 @@ parameter_types! { pub const MinimumTransferAmount: Balance = 100 * SCALE_DOWN_FACTOR; /// Volume fee rate in basis points (10 bps = 0.1%) pub const VolumeFeeRateBps: u32 = 10; + /// Proportion of volume fees to burn (50% burned, 50% to miner) + pub const VolumeFeesBurnRate: Permill = Permill::from_percent(50); } impl pallet_wormhole::Config for Test { @@ -131,6 +134,7 @@ impl pallet_wormhole::Config for Test { type MintingAccount = MintingAccount; type MinimumTransferAmount = MinimumTransferAmount; type VolumeFeeRateBps = VolumeFeeRateBps; + type VolumeFeesBurnRate = VolumeFeesBurnRate; type WormholeAccountId = AccountId; } diff --git a/runtime/src/configs/mod.rs b/runtime/src/configs/mod.rs index eaced7a4..4455857c 100644 --- a/runtime/src/configs/mod.rs +++ b/runtime/src/configs/mod.rs @@ -638,12 +638,15 @@ parameter_types! { pub const MinimumTransferAmount: Balance = 100 * pallet_wormhole::SCALE_DOWN_FACTOR; /// Volume fee rate in basis points (10 bps = 0.1%) pub const VolumeFeeRateBps: u32 = 10; + /// Proportion of volume fees to burn (50% burned, 50% to miner) + pub const VolumeFeesBurnRate: Permill = Permill::from_percent(50); } impl pallet_wormhole::Config for Runtime { type MintingAccount = WormholeMintingAccount; type MinimumTransferAmount = MinimumTransferAmount; type VolumeFeeRateBps = VolumeFeeRateBps; + type VolumeFeesBurnRate = VolumeFeesBurnRate; type WeightInfo = (); type Currency = Balances; type Assets = Assets; From a837d442e181df8f872b931b90f2cfffc3cb7a27 Mon Sep 17 00:00:00 2001 From: illuzen Date: Wed, 21 Jan 2026 01:22:45 +0800 Subject: [PATCH 16/27] fmt --- pallets/wormhole/src/lib.rs | 7 ++++--- pallets/wormhole/src/mock.rs | 3 +-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pallets/wormhole/src/lib.rs b/pallets/wormhole/src/lib.rs index 6bc6b434..3e47426b 100644 --- a/pallets/wormhole/src/lib.rs +++ b/pallets/wormhole/src/lib.rs @@ -303,7 +303,8 @@ pub mod pallet { Error::::InvalidVolumeFeeRate ); - // The output_amount is what the user receives after fee deduction (already enforced by circuit) + // The output_amount is what the user receives after fee deduction (already enforced by + // circuit) let exit_balance_u128 = (public_inputs.output_amount as u128).saturating_mul(crate::SCALE_DOWN_FACTOR); @@ -609,8 +610,8 @@ pub mod pallet { // Fee distribution: configurable portion burned, remainder to miner // - // Original deposit locked `input_amount` in an unspendable account (tokens still exist). - // On exit we mint `output_amount` to user, where: input = output + fee + // Original deposit locked `input_amount` in an unspendable account (tokens still + // exist). On exit we mint `output_amount` to user, where: input = output + fee // // Fee split (controlled by VolumeFeesBurnRate): // - burn_amount = fee * burn_rate (reduces total issuance via Currency::burn) diff --git a/pallets/wormhole/src/mock.rs b/pallets/wormhole/src/mock.rs index 9c56aa7e..52100336 100644 --- a/pallets/wormhole/src/mock.rs +++ b/pallets/wormhole/src/mock.rs @@ -6,8 +6,7 @@ use frame_support::{ use frame_system::mocking::MockUncheckedExtrinsic; use qp_poseidon::PoseidonHasher; use sp_core::H256; -use sp_runtime::Permill; -use sp_runtime::{traits::IdentityLookup, BuildStorage}; +use sp_runtime::{traits::IdentityLookup, BuildStorage, Permill}; construct_runtime!( pub enum Test { From ca7524d8f7476e7970bf04aecaef6a5e85d7e2b4 Mon Sep 17 00:00:00 2001 From: illuzen Date: Thu, 22 Jan 2026 18:55:24 +0800 Subject: [PATCH 17/27] burn high-security fee instead of sending to treasury (#357) --- pallets/reversible-transfers/src/lib.rs | 28 ++++++------------- .../reversible-transfers/src/tests/mock.rs | 8 ------ .../src/tests/test_high_security_account.rs | 14 +++++----- .../src/tests/test_reversible_transfers.rs | 23 ++++++++------- runtime/src/configs/mod.rs | 7 ++--- 5 files changed, 29 insertions(+), 51 deletions(-) diff --git a/pallets/reversible-transfers/src/lib.rs b/pallets/reversible-transfers/src/lib.rs index 2867692c..31d52084 100644 --- a/pallets/reversible-transfers/src/lib.rs +++ b/pallets/reversible-transfers/src/lib.rs @@ -8,8 +8,8 @@ //! ## Volume Fee for High-Security Accounts //! //! When high-security accounts reverse transactions, a configurable volume fee -//! (expressed as a Permill) is deducted from the transaction amount and sent -//! to the treasury. Regular accounts do not incur any fees when reversing transactions. +//! (expressed as a Permill) is deducted from the transaction amount and burned. +//! Regular accounts do not incur any fees when reversing transactions. #![cfg_attr(not(feature = "std"), no_std)] @@ -189,12 +189,9 @@ pub mod pallet { /// Volume fee taken from reversed transactions for high-security accounts only, /// expressed as a Permill (e.g., Permill::from_percent(1) = 1%). Regular accounts incur no - /// fees. + /// fees. The fee is burned (removed from total issuance). #[pallet::constant] type VolumeFee: Get; - - /// Treasury account ID where volume fees are sent. - type TreasuryAccountId: Get; } /// Maps accounts to their chosen reversibility delay period (in milliseconds). @@ -833,11 +830,8 @@ pub mod pallet { // No fee for regular accounts (Zero::zero(), pending.amount) }; - let treasury_account = T::TreasuryAccountId::get(); - - // For assets, transfer held funds to treasury (fee) and interceptor (remaining) - // For native balances, transfer held funds to treasury (fee) and interceptor - // (remaining) + // For assets, burn held funds (fee) and transfer remaining to interceptor + // For native balances, burn held funds (fee) and transfer remaining to interceptor if let Ok((call, _)) = T::Preimages::peek::>(&pending.call) { if let Ok(pallet_assets::Call::transfer_keep_alive { id, .. }) = call.clone().try_into() @@ -845,15 +839,13 @@ pub mod pallet { let reason = Self::asset_hold_reason(); let asset_id = id.into(); - // Transfer fee to treasury if fee_amount > 0 - let _ = as AssetsHold>>::transfer_on_hold( + // Burn fee amount if fee_amount > 0 + let _ = as AssetsHold>>::burn_held( asset_id.clone(), &reason, &pending.from, - &treasury_account, fee_amount, Precision::Exact, - Restriction::Free, Fortitude::Polite, )?; @@ -872,14 +864,12 @@ pub mod pallet { if let Ok(pallet_balances::Call::transfer_keep_alive { .. }) = call.clone().try_into() { - // Transfer fee to treasury - pallet_balances::Pallet::::transfer_on_hold( + // Burn fee amount + pallet_balances::Pallet::::burn_held( &HoldReason::ScheduledTransfer.into(), &pending.from, - &treasury_account, fee_amount, Precision::Exact, - Restriction::Free, Fortitude::Polite, )?; diff --git a/pallets/reversible-transfers/src/tests/mock.rs b/pallets/reversible-transfers/src/tests/mock.rs index 39aa6052..031955ab 100644 --- a/pallets/reversible-transfers/src/tests/mock.rs +++ b/pallets/reversible-transfers/src/tests/mock.rs @@ -49,9 +49,6 @@ pub fn eve() -> AccountId { pub fn ferdie() -> AccountId { account_id(255) } -pub fn treasury() -> AccountId { - account_id(99) -} /// Helper function for interceptor account (avoiding + 100 calculations) pub fn interceptor_1() -> AccountId { @@ -195,8 +192,6 @@ parameter_types! { pub const MaxReversibleTransfers: u32 = 100; pub const MaxInterceptorAccounts: u32 = 10; pub const HighSecurityVolumeFee: Permill = Permill::from_percent(1); - /// Mock treasury account ID for tests - pub const TreasuryAccount: AccountId = AccountId::new([99u8; 32]); } impl pallet_reversible_transfers::Config for Test { @@ -215,7 +210,6 @@ impl pallet_reversible_transfers::Config for Test { type TimeProvider = MockTimestamp; type MaxInterceptorAccounts = MaxInterceptorAccounts; type VolumeFee = HighSecurityVolumeFee; - type TreasuryAccountId = TreasuryAccount; } parameter_types! { @@ -346,8 +340,6 @@ pub fn new_test_ext() -> sp_io::TestExternalities { (account_id(109), 100_000_000_000), (account_id(110), 100_000_000_000), (account_id(111), 100_000_000_000), - // Treasury account for fee collection tests (must meet existential deposit) - (account_id(99), 1), ], } .assimilate_storage(&mut t) diff --git a/pallets/reversible-transfers/src/tests/test_high_security_account.rs b/pallets/reversible-transfers/src/tests/test_high_security_account.rs index eb6f92d8..9ec8dec2 100644 --- a/pallets/reversible-transfers/src/tests/test_high_security_account.rs +++ b/pallets/reversible-transfers/src/tests/test_high_security_account.rs @@ -3,6 +3,7 @@ use crate::tests::{ test_reversible_transfers::{calculate_tx_id, transfer_call}, }; use frame_support::assert_ok; +use pallet_balances::TotalIssuance; // NOTE: Many of the high security / reversibility behaviors are enforced via SignedExtension or // external pallets (Recovery/Proxy). They are covered by integration tests in runtime. @@ -13,12 +14,11 @@ fn guardian_can_cancel_reversible_transactions_for_hs_account() { let hs_user = alice(); // reversible from genesis with interceptor=2 let guardian = bob(); let dest = charlie(); - let treasury = treasury(); let amount = 10_000u128; // Use larger amount so volume fee is visible // Record initial balances let initial_guardian_balance = Balances::free_balance(&guardian); - let initial_treasury_balance = Balances::free_balance(&treasury); + let initial_total_issuance = TotalIssuance::::get(); // Compute tx_id BEFORE scheduling (matches pallet logic using current GlobalNonce) let call = transfer_call(dest.clone(), amount); @@ -36,7 +36,7 @@ fn guardian_can_cancel_reversible_transactions_for_hs_account() { assert!(ReversibleTransfers::pending_dispatches(tx_id).is_none()); // Verify volume fee was applied for high-security account - // Expected fee: 10,000 * 100 / 10,000 = 100 tokens + // Expected fee: 10,000 * 1% = 100 tokens let expected_fee = 100; let expected_remaining = amount - expected_fee; @@ -47,11 +47,11 @@ fn guardian_can_cancel_reversible_transactions_for_hs_account() { "Guardian should receive remaining amount after volume fee deduction" ); - // Check that treasury received the fee + // Check that fee was burned (total issuance decreased) assert_eq!( - Balances::free_balance(&treasury), - initial_treasury_balance + expected_fee, - "Treasury should receive volume fee from high-security account cancellation" + TotalIssuance::::get(), + initial_total_issuance - expected_fee, + "Volume fee should be burned from total issuance" ); }); } diff --git a/pallets/reversible-transfers/src/tests/test_reversible_transfers.rs b/pallets/reversible-transfers/src/tests/test_reversible_transfers.rs index 002ce3b3..21ca06b5 100644 --- a/pallets/reversible-transfers/src/tests/test_reversible_transfers.rs +++ b/pallets/reversible-transfers/src/tests/test_reversible_transfers.rs @@ -592,7 +592,6 @@ fn cancel_dispatch_works() { System::set_block_number(1); let user = alice(); // High-security account from genesis let interceptor = bob(); - let treasury = treasury(); let amount = 10_000; let call = transfer_call(interceptor.clone(), amount); let tx_id = calculate_tx_id::(user.clone(), &call); @@ -604,7 +603,7 @@ fn cancel_dispatch_works() { // Record initial balances let initial_interceptor_balance = Balances::free_balance(&interceptor); - let initial_treasury_balance = Balances::free_balance(&treasury); + let initial_total_issuance = pallet_balances::TotalIssuance::::get(); assert_eq!(Agenda::::get(execute_block).len(), 0); @@ -633,7 +632,7 @@ fn cancel_dispatch_works() { assert_eq!(Agenda::::get(execute_block).len(), 0); // Verify volume fee was applied for high-security account - // Expected fee: 10,000 * 100 / 10,000 = 100 tokens + // Expected fee: 10,000 * 1% = 100 tokens let expected_fee = 100; let expected_remaining = amount - expected_fee; @@ -645,10 +644,11 @@ fn cancel_dispatch_works() { "High-security account should have volume fee deducted" ); + // Check that fee was burned (total issuance decreased) assert_eq!( - Balances::free_balance(&treasury), - initial_treasury_balance + expected_fee, - "Treasury should receive volume fee from high-security account cancellation" + pallet_balances::TotalIssuance::::get(), + initial_total_issuance - expected_fee, + "Volume fee should be burned from total issuance" ); // Check event @@ -662,13 +662,12 @@ fn no_volume_fee_for_regular_reversible_accounts() { System::set_block_number(1); let user = charlie(); // Regular account (not high-security) let recipient = dave(); - let treasury = treasury(); let amount = 10_000; // Check initial balances let initial_user_balance = Balances::free_balance(&user); let initial_recipient_balance = Balances::free_balance(&recipient); - let initial_treasury_balance = Balances::free_balance(&treasury); + let initial_total_issuance = pallet_balances::TotalIssuance::::get(); let call = transfer_call(recipient.clone(), amount); let tx_id = calculate_tx_id::(user.clone(), &call); @@ -700,11 +699,11 @@ fn no_volume_fee_for_regular_reversible_accounts() { "Recipient should not receive funds when transaction is cancelled" ); - // Verify treasury balance unchanged + // Verify total issuance unchanged (no fee burned for regular accounts) assert_eq!( - Balances::free_balance(&treasury), - initial_treasury_balance, - "Treasury should not receive fee from regular account cancellation" + pallet_balances::TotalIssuance::::get(), + initial_total_issuance, + "Total issuance should not change for regular account cancellation" ); // Should still have TransactionCancelled event diff --git a/runtime/src/configs/mod.rs b/runtime/src/configs/mod.rs index e76063a3..e6958e48 100644 --- a/runtime/src/configs/mod.rs +++ b/runtime/src/configs/mod.rs @@ -56,7 +56,7 @@ use pallet_transaction_payment::{ConstFeeMultiplier, FungibleAdapter, Multiplier use qp_poseidon::PoseidonHasher; use qp_scheduler::BlockNumberOrTimestamp; use sp_runtime::{ - traits::{AccountIdConversion, ConvertInto, One}, + traits::{ConvertInto, One}, FixedU128, Perbill, Permill, }; use sp_version::RuntimeVersion; @@ -480,10 +480,8 @@ parameter_types! { pub const MinDelayPeriodBlocks: BlockNumber = 2; pub const MaxReversibleTransfers: u32 = 10; pub const MaxInterceptorAccounts: u32 = 32; - /// Volume fee for reversed transactions from high-security accounts only, in basis points (10 = 0.1%) + /// Volume fee for reversed transactions from high-security accounts only (1% fee is burned) pub const HighSecurityVolumeFee: Permill = Permill::from_percent(1); - /// Treasury account ID - pub TreasuryAccountId: AccountId = TreasuryPalletId::get().into_account_truncating(); } impl pallet_reversible_transfers::Config for Runtime { @@ -502,7 +500,6 @@ impl pallet_reversible_transfers::Config for Runtime { type TimeProvider = Timestamp; type MaxInterceptorAccounts = MaxInterceptorAccounts; type VolumeFee = HighSecurityVolumeFee; - type TreasuryAccountId = TreasuryAccountId; } parameter_types! { From 153e46deb138db40d328a5ec161a4d5e399682f4 Mon Sep 17 00:00:00 2001 From: illuzen Date: Fri, 23 Jan 2026 10:50:42 +0800 Subject: [PATCH 18/27] Continuous Mining (#358) * new block trigger * dedupe logic, remove unnecessary field * simplify again * fmt * clippy * fmt --- client/consensus/qpow/src/lib.rs | 45 +++++++--- client/consensus/qpow/src/worker.rs | 128 +++++++++++++++++++++------- node/src/service.rs | 33 ++++--- 3 files changed, 148 insertions(+), 58 deletions(-) diff --git a/client/consensus/qpow/src/lib.rs b/client/consensus/qpow/src/lib.rs index 8fc459ee..4a8e36ac 100644 --- a/client/consensus/qpow/src/lib.rs +++ b/client/consensus/qpow/src/lib.rs @@ -10,9 +10,9 @@ use sp_consensus_qpow::QPoWApi; use sp_runtime::{generic::BlockId, traits::Block as BlockT, AccountId32}; use std::{sync::Arc, time::Duration}; -use crate::worker::UntilImportedOrTimeout; -pub use crate::worker::{MiningBuild, MiningHandle, MiningMetadata}; -use futures::{Future, StreamExt}; +use crate::worker::UntilImportedOrTransaction; +pub use crate::worker::{MiningBuild, MiningHandle, MiningMetadata, RebuildTrigger}; +use futures::{Future, Stream, StreamExt}; use log::*; use prometheus_endpoint::Registry; use sc_client_api::{self, backend::AuxStore, BlockOf, BlockchainEvents}; @@ -342,6 +342,10 @@ where Ok(BasicQueue::new(verifier, block_import, justification_import, spawner, registry)) } +/// Maximum transaction-triggered rebuilds per second. +/// Hardcoded for now but could be made configurable later. +const MAX_REBUILDS_PER_SEC: u32 = 2; + /// Start the mining worker for QPoW. This function provides the necessary helper functions that can /// be used to implement a miner. However, it does not do the CPU-intensive mining itself. /// @@ -349,11 +353,17 @@ where /// mining metadata and submitting mined blocks, and a future, which must be polled to fill in /// information in the worker. /// -/// `pre_runtime` is a parameter that allows a custom additional pre-runtime digest to be inserted -/// for blocks being built. This can encode authorship information, or just be a graffiti. +/// The worker will rebuild blocks when: +/// - A new block is imported from the network +/// - New transactions arrive (rate limited to MAX_REBUILDS_PER_SEC) +/// +/// This allows transactions to be included faster since we don't wait for the next block import +/// to rebuild. Mining on a new block vs the old block has the same probability of success per +/// nonce, so the only cost is the overhead of rebuilding (which is minimal compared to mining +/// time). #[allow(clippy::too_many_arguments)] #[allow(clippy::type_complexity)] -pub fn start_mining_worker( +pub fn start_mining_worker( block_import: BoxBlockImport, client: Arc, select_chain: S, @@ -362,7 +372,7 @@ pub fn start_mining_worker( justification_sync_link: L, rewards_address: AccountId32, create_inherent_data_providers: CIDP, - timeout: Duration, + tx_notifications: TxStream, build_time: Duration, ) -> (MiningHandle>::Proof>, impl Future) where @@ -381,17 +391,22 @@ where SO: SyncOracle + Clone + Send + Sync + 'static, L: JustificationSyncLink, CIDP: CreateInherentDataProviders, + TxHash: Send + 'static, + TxStream: Stream + Send + Unpin + 'static, { - let mut timer = UntilImportedOrTimeout::new(client.import_notification_stream(), timeout); + let mut trigger_stream = UntilImportedOrTransaction::new( + client.import_notification_stream(), + tx_notifications, + MAX_REBUILDS_PER_SEC, + ); let worker = MiningHandle::new(client.clone(), block_import, justification_sync_link); let worker_ret = worker.clone(); let task = async move { - loop { - if timer.next().await.is_none() { - break; - } - + // Main block building loop - runs until trigger stream closes + // Wait for a trigger (Initial, BlockImported, or NewTransactions) + // continue skips to the next iteration to wait for another trigger + while let Some(trigger) = trigger_stream.next().await { if sync_oracle.is_major_syncing() { debug!(target: LOG_TARGET, "Skipping proposal due to sync."); worker.on_major_syncing(); @@ -412,7 +427,9 @@ where }; let best_hash = best_header.hash(); - if worker.best_hash() == Some(best_hash) { + // Skip redundant block import triggers if we're already building on this hash. + // Initial and NewTransactions triggers should proceed to rebuild. + if trigger == RebuildTrigger::BlockImported && worker.best_hash() == Some(best_hash) { continue; } diff --git a/client/consensus/qpow/src/worker.rs b/client/consensus/qpow/src/worker.rs index 7fd05f96..e819b8c4 100644 --- a/client/consensus/qpow/src/worker.rs +++ b/client/consensus/qpow/src/worker.rs @@ -198,51 +198,117 @@ where } } -/// A stream that waits for a block import or timeout. -pub struct UntilImportedOrTimeout { +/// Reason why the stream fired - either a block was imported or enough transactions arrived. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RebuildTrigger { + /// Initial trigger to bootstrap mining (fires once on first poll). + Initial, + /// A new block was imported from the network. + BlockImported, + /// Enough new transactions arrived to trigger a rebuild. + NewTransactions, +} + +/// A stream that waits for a block import or new transactions (with rate limiting). +/// +/// This enables block producers to include new transactions faster by rebuilding +/// the block being mined when transactions arrive, rather than waiting for the +/// next block import or timeout. +/// +/// Rate limiting prevents excessive rebuilds - we limit to `max_rebuilds_per_sec`. +pub struct UntilImportedOrTransaction { + /// Block import notifications stream. import_notifications: ImportNotifications, - timeout: Duration, - inner_delay: Option, + /// Transaction pool import notifications stream. + tx_notifications: Pin + Send>>, + /// Minimum interval between transaction-triggered rebuilds. + min_rebuild_interval: Duration, + /// Rate limit delay - if set, we're waiting before we can fire again. + rate_limit_delay: Option, + /// Whether we've fired the initial trigger yet. + initial_fired: bool, + /// Whether we have pending transactions waiting to trigger a rebuild. + has_pending_tx: bool, } -impl UntilImportedOrTimeout { - /// Create a new stream using the given import notification and timeout duration. - pub fn new(import_notifications: ImportNotifications, timeout: Duration) -> Self { - Self { import_notifications, timeout, inner_delay: None } +impl UntilImportedOrTransaction { + /// Create a new stream. + /// + /// # Arguments + /// * `import_notifications` - Stream of block import notifications + /// * `tx_notifications` - Stream of transaction import notifications + /// * `max_rebuilds_per_sec` - Maximum transaction-triggered rebuilds per second + pub fn new( + import_notifications: ImportNotifications, + tx_notifications: impl Stream + Send + 'static, + max_rebuilds_per_sec: u32, + ) -> Self { + let min_rebuild_interval = if max_rebuilds_per_sec > 0 { + Duration::from_millis(1000 / max_rebuilds_per_sec as u64) + } else { + Duration::from_secs(u64::MAX) // Effectively disable tx-triggered rebuilds + }; + + Self { + import_notifications, + tx_notifications: Box::pin(tx_notifications), + min_rebuild_interval, + rate_limit_delay: None, + initial_fired: false, + has_pending_tx: false, + } } } -impl Stream for UntilImportedOrTimeout { - type Item = (); +impl Stream for UntilImportedOrTransaction { + type Item = RebuildTrigger; - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { - let mut fire = false; + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + // Fire immediately on first poll to bootstrap mining at genesis + if !self.initial_fired { + self.initial_fired = true; + debug!(target: LOG_TARGET, "Initial trigger, bootstrapping block production"); + return Poll::Ready(Some(RebuildTrigger::Initial)); + } - loop { - match Stream::poll_next(Pin::new(&mut self.import_notifications), cx) { - Poll::Pending => break, - Poll::Ready(Some(_)) => { - fire = true; + // Check for block imports first - these always trigger immediately + if let Poll::Ready(notification) = + Stream::poll_next(Pin::new(&mut self.import_notifications), cx) + { + match notification { + Some(_) => { + // Block import resets pending state since we'll build fresh + self.has_pending_tx = false; + self.rate_limit_delay = None; + debug!(target: LOG_TARGET, "Block imported, triggering rebuild"); + return Poll::Ready(Some(RebuildTrigger::BlockImported)); }, - Poll::Ready(None) => return Poll::Ready(None), + None => return Poll::Ready(None), } } - let timeout = self.timeout; - let inner_delay = self.inner_delay.get_or_insert_with(|| Delay::new(timeout)); - - match Future::poll(Pin::new(inner_delay), cx) { - Poll::Pending => (), - Poll::Ready(()) => { - fire = true; - }, + // Drain all pending transaction notifications + while let Poll::Ready(Some(_)) = Stream::poll_next(Pin::new(&mut self.tx_notifications), cx) + { + self.has_pending_tx = true; } - if fire { - self.inner_delay = None; - Poll::Ready(Some(())) - } else { - Poll::Pending + // If we have pending transactions, check rate limit + if self.has_pending_tx { + // Check if rate limit allows firing (no delay or delay expired) + let can_fire = match self.rate_limit_delay.as_mut() { + None => true, + Some(delay) => Future::poll(Pin::new(delay), cx).is_ready(), + }; + + if can_fire { + self.has_pending_tx = false; + self.rate_limit_delay = Some(Delay::new(self.min_rebuild_interval)); + debug!(target: LOG_TARGET, "New transaction(s), triggering rebuild"); + return Poll::Ready(Some(RebuildTrigger::NewTransactions)); + } } + + Poll::Pending } } diff --git a/node/src/service.rs b/node/src/service.rs index df6f15bd..27a9b662 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -166,7 +166,8 @@ pub fn new_full< other: (pow_block_import, mut telemetry), } = new_partial(&config)?; - let mut tx_stream = transaction_pool.clone().import_notification_stream(); + let tx_stream_for_worker = transaction_pool.clone().import_notification_stream(); + let tx_stream_for_logger = transaction_pool.clone().import_notification_stream(); let net_config = sc_network::config::FullNetworkConfiguration::< Block, @@ -276,7 +277,7 @@ pub fn new_full< sync_service.clone(), rewards_address, inherent_data_providers, - Duration::from_secs(10), + tx_stream_for_worker, Duration::from_secs(10), ); @@ -357,17 +358,22 @@ pub fn new_full< // If external miner URL is provided, use external mining if let Some(miner_url) = &external_miner_url { - // Cancel previous job if metadata has changed - if let Some(job_id) = ¤t_job_id { - if let Err(e) = external_miner_client::cancel_mining_job( - &http_client, - miner_url, - job_id, - ) - .await - { - log::warn!("⛏️Failed to cancel previous mining job: {}", e); - } + // Fire-and-forget cancellation of previous job - don't wait for confirmation + // This reduces latency when switching to a new block + if let Some(old_job_id) = current_job_id.take() { + let cancel_client = http_client.clone(); + let cancel_url = miner_url.clone(); + tokio::spawn(async move { + if let Err(e) = external_miner_client::cancel_mining_job( + &cancel_client, + &cancel_url, + &old_job_id, + ) + .await + { + log::debug!("⛏️ Failed to cancel previous mining job {}: {}", old_job_id, e); + } + }); } // Get current distance_threshold from runtime @@ -514,6 +520,7 @@ pub fn new_full< }); task_manager.spawn_handle().spawn("tx-logger", None, async move { + let mut tx_stream = tx_stream_for_logger; while let Some(tx_hash) = tx_stream.next().await { if let Some(tx) = transaction_pool.ready_transaction(&tx_hash) { log::trace!(target: "miner", "New transaction: Hash = {:?}", tx_hash); From bd56dcc794a7b079e5c9cf39e896ef53d8a7c0c5 Mon Sep 17 00:00:00 2001 From: Cezary Olborski Date: Fri, 23 Jan 2026 14:04:24 +0800 Subject: [PATCH 19/27] feat: Vesting and MerkleAirdrop removed (#360) * feat: Merkle Airdrop - removed * feat: Vesting pallet - removed * fix: Clippy for header --- Cargo.lock | 36 -- Cargo.toml | 3 - pallets/merkle-airdrop/Cargo.toml | 63 --- pallets/merkle-airdrop/README.md | 14 - pallets/merkle-airdrop/src/benchmarking.rs | 196 ------- pallets/merkle-airdrop/src/lib.rs | 584 ------------------- pallets/merkle-airdrop/src/mock.rs | 129 ----- pallets/merkle-airdrop/src/tests.rs | 591 -------------------- pallets/merkle-airdrop/src/weights.rs | 193 ------- primitives/header/src/lib.rs | 7 +- runtime/Cargo.toml | 7 - runtime/src/benchmarks.rs | 1 - runtime/src/configs/mod.rs | 44 +- runtime/src/lib.rs | 7 - runtime/tests/governance/mod.rs | 1 - runtime/tests/governance/vesting.rs | 619 --------------------- 16 files changed, 8 insertions(+), 2487 deletions(-) delete mode 100644 pallets/merkle-airdrop/Cargo.toml delete mode 100644 pallets/merkle-airdrop/README.md delete mode 100644 pallets/merkle-airdrop/src/benchmarking.rs delete mode 100644 pallets/merkle-airdrop/src/lib.rs delete mode 100644 pallets/merkle-airdrop/src/mock.rs delete mode 100644 pallets/merkle-airdrop/src/tests.rs delete mode 100644 pallets/merkle-airdrop/src/weights.rs delete mode 100644 runtime/tests/governance/vesting.rs diff --git a/Cargo.lock b/Cargo.lock index 35560863..a5147e1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7119,25 +7119,6 @@ dependencies = [ "sp-runtime", ] -[[package]] -name = "pallet-merkle-airdrop" -version = "0.1.0" -dependencies = [ - "binary-merkle-tree", - "frame-benchmarking", - "frame-support", - "frame-system", - "log", - "pallet-balances 40.0.1", - "pallet-vesting", - "parity-scale-codec", - "scale-info", - "sha2 0.10.9", - "sp-core", - "sp-io", - "sp-runtime", -] - [[package]] name = "pallet-message-queue" version = "44.0.0" @@ -7570,21 +7551,6 @@ dependencies = [ "sp-runtime", ] -[[package]] -name = "pallet-vesting" -version = "41.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "305b437f4832bb563b660afa6549c0f0d446b668b4f098edc48d04e803badb9f" -dependencies = [ - "frame-benchmarking", - "frame-support", - "frame-system", - "log", - "parity-scale-codec", - "scale-info", - "sp-runtime", -] - [[package]] name = "pallet-wormhole" version = "0.1.0" @@ -9191,7 +9157,6 @@ dependencies = [ "pallet-assets-holder", "pallet-balances 40.0.1", "pallet-conviction-voting", - "pallet-merkle-airdrop", "pallet-mining-rewards", "pallet-preimage", "pallet-qpow", @@ -9206,7 +9171,6 @@ dependencies = [ "pallet-transaction-payment-rpc-runtime-api", "pallet-treasury", "pallet-utility", - "pallet-vesting", "parity-scale-codec", "primitive-types 0.13.1", "qp-dilithium-crypto", diff --git a/Cargo.toml b/Cargo.toml index b6c3abde..bd8bcfd9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,6 @@ members = [ "miner-api", "node", "pallets/balances", - "pallets/merkle-airdrop", "pallets/mining-rewards", "pallets/qpow", "pallets/reversible-transfers", @@ -131,7 +130,6 @@ zeroize = { version = "1.7.0", default-features = false } # Own dependencies pallet-balances = { path = "./pallets/balances", default-features = false } -pallet-merkle-airdrop = { path = "./pallets/merkle-airdrop", default-features = false } pallet-mining-rewards = { path = "./pallets/mining-rewards", default-features = false } pallet-qpow = { path = "./pallets/qpow", default-features = false } pallet-reversible-transfers = { path = "./pallets/reversible-transfers", default-features = false } @@ -186,7 +184,6 @@ pallet-transaction-payment-rpc = { version = "44.0.0", default-features = false pallet-transaction-payment-rpc-runtime-api = { version = "41.0.0", default-features = false } pallet-treasury = { version = "40.0.0", default-features = false } pallet-utility = { version = "41.0.0", default-features = false } -pallet-vesting = { version = "41.0.0", default-features = false } prometheus-endpoint = { version = "0.17.2", default-features = false, package = "substrate-prometheus-endpoint" } sc-basic-authorship = { version = "0.50.0", default-features = false } sc-block-builder = { version = "0.45.0", default-features = true } diff --git a/pallets/merkle-airdrop/Cargo.toml b/pallets/merkle-airdrop/Cargo.toml deleted file mode 100644 index 3c88c4bb..00000000 --- a/pallets/merkle-airdrop/Cargo.toml +++ /dev/null @@ -1,63 +0,0 @@ -[package] -authors.workspace = true -description = "A pallet for distributing tokens via Merkle proofs" -edition.workspace = true -homepage.workspace = true -license = "MIT-0" -name = "pallet-merkle-airdrop" -publish = false -repository.workspace = true -version = "0.1.0" - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] - -[dependencies] -binary-merkle-tree.workspace = true -codec = { workspace = true, default-features = false, features = ["derive"] } -frame-benchmarking = { optional = true, workspace = true } -frame-support.workspace = true -frame-system.workspace = true -log.workspace = true -pallet-vesting = { workspace = true, optional = true } -scale-info = { workspace = true, default-features = false, features = ["derive"] } -sha2.workspace = true -sp-core.workspace = true -sp-io.workspace = true -sp-runtime.workspace = true - -[dev-dependencies] -pallet-balances.features = ["std"] -pallet-balances.workspace = true -pallet-vesting.workspace = true -sp-core.workspace = true -sp-io.workspace = true -sp-runtime.workspace = true - -[features] -default = ["std"] -runtime-benchmarks = [ - "frame-benchmarking/runtime-benchmarks", - "frame-support/runtime-benchmarks", - "frame-system/runtime-benchmarks", - "pallet-vesting", -] -std = [ - "binary-merkle-tree/std", - "codec/std", - "frame-benchmarking?/std", - "frame-support/std", - "frame-system/std", - "log/std", - "pallet-balances/std", - "pallet-vesting?/std", - "scale-info/std", - "sha2/std", - "sp-core/std", - "sp-io/std", - "sp-runtime/std", -] -try-runtime = [ - "frame-support/try-runtime", - "frame-system/try-runtime", -] diff --git a/pallets/merkle-airdrop/README.md b/pallets/merkle-airdrop/README.md deleted file mode 100644 index 2e8495fb..00000000 --- a/pallets/merkle-airdrop/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# Merkle Airdrop Pallet - -A Substrate pallet for distributing tokens via Merkle proofs with optional vesting of the airdropped tokens. - -## Testing & Usage - -For testing and interacting with this pallet, please refer to the CLI tool and example in the [resonance-api-client](https://github.com/Quantus-Network/resonance-api-client/blob/master/examples/async/examples/merkle-airdrop-README.md) repository: -- `examples/ac-examples-async/examples/merkle_airdrop_cli.rs` -- `examples/ac-examples-async/examples/merkle_airdrop_cli-README.md` for the documentation - -These tool demonstrates how to: -- Generate Merkle trees and proofs -- Create and fund airdrops -- Claim tokens using proofs diff --git a/pallets/merkle-airdrop/src/benchmarking.rs b/pallets/merkle-airdrop/src/benchmarking.rs deleted file mode 100644 index d965bd8d..00000000 --- a/pallets/merkle-airdrop/src/benchmarking.rs +++ /dev/null @@ -1,196 +0,0 @@ -//! Benchmarking setup for pallet-merkle-airdrop - -extern crate alloc; - -use super::*; -use crate::Pallet as MerkleAirdrop; -use frame_benchmarking::v2::*; -use frame_support::BoundedVec; -use frame_system::RawOrigin; -use sp_io::hashing::blake2_256; -use sp_runtime::traits::{Get, Saturating}; - -// Helper function to mirror pallet's Merkle proof verification logic -fn calculate_expected_root_for_benchmark( - initial_leaf_hash: MerkleHash, - proof_elements: &[MerkleHash], -) -> MerkleHash { - let mut computed_hash = initial_leaf_hash; - for proof_element in proof_elements.iter() { - // The comparison logic must match how MerkleHash is ordered in your pallet - if computed_hash.as_ref() < proof_element.as_ref() { - // This replicates Self::calculate_parent_hash_blake2(&computed_hash, proof_element) - let mut combined_data = computed_hash.as_ref().to_vec(); - combined_data.extend_from_slice(proof_element.as_ref()); - computed_hash = blake2_256(&combined_data); - } else { - // This replicates Self::calculate_parent_hash_blake2(proof_element, &computed_hash) - let mut combined_data = proof_element.as_ref().to_vec(); - combined_data.extend_from_slice(computed_hash.as_ref()); - computed_hash = blake2_256(&combined_data); - } - } - computed_hash -} - -#[benchmarks( - where - T: Send + Sync, - T: Config + pallet_vesting::Config>, -)] -mod benchmarks { - use super::*; - - #[benchmark] - fn create_airdrop() { - let caller: T::AccountId = whitelisted_caller(); - let merkle_root = [0u8; 32]; - let vesting_period = None; - let vesting_schedule = None; - - #[extrinsic_call] - create_airdrop(RawOrigin::Signed(caller), merkle_root, vesting_period, vesting_schedule); - } - - #[benchmark] - fn fund_airdrop() { - let caller: T::AccountId = whitelisted_caller(); - let merkle_root = [0u8; 32]; - - let airdrop_id = MerkleAirdrop::::next_airdrop_id(); - AirdropInfo::::insert( - airdrop_id, - AirdropMetadata { - merkle_root, - balance: 0u32.into(), - creator: caller.clone(), - vesting_period: None, - vesting_delay: None, - }, - ); - - NextAirdropId::::put(airdrop_id + 1); - - let amount: BalanceOf = ::MinVestedTransfer::get(); - - // Get ED and ensure caller has sufficient balance - let ed = CurrencyOf::::minimum_balance(); - - let caller_balance = ed.saturating_mul(10u32.into()).saturating_add(amount); - CurrencyOf::::make_free_balance_be(&caller, caller_balance); - - CurrencyOf::::make_free_balance_be(&MerkleAirdrop::::account_id(), ed); - - #[extrinsic_call] - fund_airdrop(RawOrigin::Signed(caller), airdrop_id, amount); - } - - #[benchmark] - fn claim(p: Linear<0, { T::MaxProofs::get() }>) { - let caller: T::AccountId = whitelisted_caller(); - let recipient: T::AccountId = account("recipient", 0, 0); - - let amount: BalanceOf = ::MinVestedTransfer::get(); - - // 1. Calculate the initial leaf hash - let leaf_hash = MerkleAirdrop::::calculate_leaf_hash_blake2(&recipient, amount); - - // 2. Generate `p` dummy proof elements that will be passed to the extrinsic - let proof_elements_for_extrinsic: alloc::vec::Vec = (0..p) - .map(|i| { - let mut dummy_data = [0u8; 32]; - dummy_data[0] = i as u8; // Make them slightly different for each proof element - blake2_256(&dummy_data) // Hash it to make it a valid MerkleHash type - }) - .collect(); - - let merkle_root_to_store = - calculate_expected_root_for_benchmark(leaf_hash, &proof_elements_for_extrinsic); - - let airdrop_id = MerkleAirdrop::::next_airdrop_id(); - - AirdropInfo::::insert( - airdrop_id, - AirdropMetadata { - merkle_root: merkle_root_to_store, - balance: amount.saturating_mul(2u32.into()), // Ensure enough balance for the claim - creator: caller.clone(), - vesting_period: None, // Simplest case: no vesting period - vesting_delay: None, // Simplest case: no vesting delay - }, - ); - - let large_balance = - amount.saturating_mul(T::MaxProofs::get().into()).saturating_add(amount); - - // Creator might not be strictly needed for `claim` from `None` origin, but good practice - CurrencyOf::::make_free_balance_be(&caller, large_balance); - // Recipient starts with minimal balance or nothing, will receive the airdrop - CurrencyOf::::make_free_balance_be(&recipient, amount); - // Pallet's account needs funds to make the transfer - CurrencyOf::::make_free_balance_be( - &MerkleAirdrop::::account_id(), - large_balance, // Pallet account needs enough to cover the claim - ); - - AirdropInfo::::mutate(airdrop_id, |maybe_info| { - if let Some(info) = maybe_info { - info.balance = large_balance; - } - }); - - // Prepare the Merkle proof argument for the extrinsic call - let merkle_proof_arg = - BoundedVec::::try_from(proof_elements_for_extrinsic) - .expect("Proof elements vector should fit into BoundedVec"); - - // Ensure recipient hasn't claimed yet (benchmark state should be clean) - assert!(!Claimed::::contains_key(airdrop_id, &recipient)); - - #[extrinsic_call] - claim(RawOrigin::None, airdrop_id, recipient.clone(), amount, merkle_proof_arg); - - // Verify successful claim - assert!(Claimed::::contains_key(airdrop_id, &recipient)); - } - - #[benchmark] - fn delete_airdrop() { - let caller: T::AccountId = whitelisted_caller(); - let merkle_root = [0u8; 32]; - - // Create an airdrop first - let airdrop_id = MerkleAirdrop::::next_airdrop_id(); - - AirdropInfo::::insert( - airdrop_id, - AirdropMetadata { - merkle_root, - balance: 0u32.into(), - creator: caller.clone(), - vesting_period: None, - vesting_delay: None, - }, - ); - - NextAirdropId::::put(airdrop_id + 1); - - let ed = CurrencyOf::::minimum_balance(); - let tiny_amount: BalanceOf = 1u32.into(); - let large_balance = ed.saturating_mul(1_000_000u32.into()); - - CurrencyOf::::make_free_balance_be(&caller, large_balance); - CurrencyOf::::make_free_balance_be(&MerkleAirdrop::::account_id(), large_balance); - - AirdropInfo::::mutate(airdrop_id, |info| { - if let Some(info) = info { - info.balance = tiny_amount; - } - }); - - #[extrinsic_call] - delete_airdrop(RawOrigin::Signed(caller), airdrop_id); - } - - impl_benchmark_test_suite!(MerkleAirdrop, crate::mock::new_test_ext(), crate::mock::Test); -} diff --git a/pallets/merkle-airdrop/src/lib.rs b/pallets/merkle-airdrop/src/lib.rs deleted file mode 100644 index b3b2651d..00000000 --- a/pallets/merkle-airdrop/src/lib.rs +++ /dev/null @@ -1,584 +0,0 @@ -//! # Merkle Airdrop Pallet -//! -//! A pallet for distributing tokens via Merkle proofs, allowing efficient token airdrops -//! where recipients can claim their tokens by providing cryptographic proofs of eligibility. -//! -//! ## Overview -//! -//! This pallet provides functionality for: -//! - Creating airdrops with a Merkle root representing all valid claims, and optional vesting -//! parameters -//! - Funding airdrops with tokens to be distributed -//! - Allowing users to claim tokens by providing Merkle proofs -//! - Allowing creators to delete airdrops and reclaim any unclaimed tokens -//! -//! The use of Merkle trees allows for gas-efficient verification of eligibility without -//! storing the complete list of recipients on-chain. -//! -//! ## Interface -//! -//! ### Dispatchable Functions -//! -//! * `create_airdrop` - Create a new airdrop with a Merkle root and vesting parameters -//! * `fund_airdrop` - Fund an existing airdrop with tokens -//! * `claim` - Claim tokens from an airdrop by providing a Merkle proof -//! * `delete_airdrop` - Delete an airdrop and reclaim any remaining tokens (creator only) - -#![cfg_attr(not(feature = "std"), no_std)] - -use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; -use frame_system::pallet_prelude::BlockNumberFor; -pub use pallet::*; - -#[cfg(test)] -mod mock; - -#[cfg(test)] -mod tests; - -#[cfg(feature = "runtime-benchmarks")] -mod benchmarking; -pub mod weights; -use scale_info::TypeInfo; -use sp_core::RuntimeDebug; -pub use weights::*; - -use frame_support::traits::{Currency, VestedTransfer}; - -/// NOTE: Vesting traits still use deprecated `Currency` trait. -type CurrencyOf = - <::Vesting as VestedTransfer<::AccountId>>::Currency; - -/// NOTE: Vesting traits still use deprecated `Currency` trait. -type BalanceOf = as Currency<::AccountId>>::Balance; - -/// Type alias for airdrop info for this pallet -type AirdropMetadataFor = - AirdropMetadata, BalanceOf, ::AccountId>; - -/// Type for storing a Merkle root hash -pub type MerkleRoot = [u8; 32]; - -/// Type for Merkle hash values -pub type MerkleHash = [u8; 32]; - -/// Airdrop ID type -pub type AirdropId = u32; - -#[derive( - Encode, - Decode, - PartialEq, - Eq, - Clone, - TypeInfo, - RuntimeDebug, - MaxEncodedLen, - DecodeWithMemTracking, -)] -pub struct AirdropMetadata { - /// Merkle root of the airdrop - pub merkle_root: MerkleHash, - /// Creator of the airdrop - pub creator: AccountId, - /// Current airdrop balance - pub balance: Balance, - /// Vesting period for the airdrop. `None` for immediate release. - pub vesting_period: Option, - /// Vesting start delay. `None` for immediate start - pub vesting_delay: Option, -} - -#[frame_support::pallet] -pub mod pallet { - use crate::{ - AirdropId, AirdropMetadata, AirdropMetadataFor, BalanceOf, CurrencyOf, MerkleHash, - MerkleRoot, - }; - - use super::weights::WeightInfo; - use frame_support::{ - pallet_prelude::*, - traits::{Currency, Get, VestedTransfer, VestingSchedule}, - }; - use frame_system::pallet_prelude::{BlockNumberFor, *}; - use sp_io::hashing::blake2_256; - use sp_runtime::{ - traits::{AccountIdConversion, BlockNumberProvider, Convert, Saturating}, - transaction_validity::{ - InvalidTransaction, TransactionLongevity, TransactionSource, TransactionValidity, - ValidTransaction, - }, - }; - extern crate alloc; - use alloc::vec; - - #[pallet::pallet] - pub struct Pallet(_); - - /// Configuration trait for the Merkle airdrop pallet. - #[pallet::config] - pub trait Config: frame_system::Config { - /// The vesting mechanism. - type Vesting: VestedTransfer> - + VestingSchedule>; - - /// Convert the block number into a balance. - type BlockNumberToBalance: Convert, BalanceOf>; - - /// The maximum number of proof elements allowed in a Merkle proof. - #[pallet::constant] - type MaxProofs: Get; - - /// The pallet id, used for deriving its sovereign account ID. - #[pallet::constant] - type PalletId: Get; - - /// Priority for unsigned claim transactions. - #[pallet::constant] - type UnsignedClaimPriority: Get; - - /// Weight information for the extrinsics in this pallet. - type WeightInfo: WeightInfo; - - /// Block number provider. - type BlockNumberProvider: BlockNumberProvider>; - } - - /// Stores general info about an airdrop - #[pallet::storage] - #[pallet::getter(fn airdrop_info)] - pub type AirdropInfo = StorageMap< - _, - Blake2_128Concat, - AirdropId, - AirdropMetadata, BalanceOf, T::AccountId>, - >; - - /// Storage for claimed status - #[pallet::storage] - #[pallet::getter(fn is_claimed)] - #[allow(clippy::unused_unit)] - pub type Claimed = StorageDoubleMap< - _, - Blake2_128Concat, - AirdropId, - Blake2_128Concat, - T::AccountId, - (), - ValueQuery, - >; - - /// Counter for airdrop IDs - #[pallet::storage] - #[pallet::getter(fn next_airdrop_id)] - pub type NextAirdropId = StorageValue<_, AirdropId, ValueQuery>; - - #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { - /// A new airdrop has been created. - /// - /// Parameters: [airdrop_id, merkle_root] - AirdropCreated { - /// The ID of the created airdrop - airdrop_id: AirdropId, - /// Airdrop metadata - airdrop_metadata: AirdropMetadataFor, - }, - /// An airdrop has been funded with tokens. - /// - /// Parameters: [airdrop_id, amount] - AirdropFunded { - /// The ID of the funded airdrop - airdrop_id: AirdropId, - /// The amount of tokens added to the airdrop - amount: BalanceOf, - }, - /// A user has claimed tokens from an airdrop. - /// - /// Parameters: [airdrop_id, account, amount] - Claimed { - /// The ID of the airdrop claimed from - airdrop_id: AirdropId, - /// The account that claimed the tokens - account: T::AccountId, - /// The amount of tokens claimed - amount: BalanceOf, - }, - /// An airdrop has been deleted. - /// - /// Parameters: [airdrop_id] - AirdropDeleted { - /// The ID of the deleted airdrop - airdrop_id: AirdropId, - }, - } - - #[pallet::error] - #[repr(u8)] - pub enum Error { - /// The specified airdrop does not exist. - AirdropNotFound, - /// The airdrop does not have sufficient balance for this operation. - InsufficientAirdropBalance, - /// The user has already claimed from this airdrop. - AlreadyClaimed, - /// The provided Merkle proof is invalid. - InvalidProof, - /// Only the creator of an airdrop can delete it. - NotAirdropCreator, - } - - impl Error { - /// Convert the error to its underlying code - pub fn to_code(&self) -> u8 { - match self { - Error::::AirdropNotFound => 1, - Error::::InsufficientAirdropBalance => 2, - Error::::AlreadyClaimed => 3, - Error::::InvalidProof => 4, - Error::::NotAirdropCreator => 5, - _ => 0, - } - } - } - - impl Pallet { - /// Returns the account ID of the pallet. - /// - /// This account is used to hold the funds for all airdrops. - pub fn account_id() -> T::AccountId { - T::PalletId::get().into_account_truncating() - } - - /// Verifies a Merkle proof against a Merkle root using Blake2 hash. - /// - /// This function checks if an account is eligible to claim a specific amount from an - /// airdrop by verifying a Merkle proof against the stored Merkle root. - /// - /// # Parameters - /// - /// * `account` - The account ID claiming tokens - /// * `amount` - The amount of tokens being claimed - /// * `merkle_root` - The Merkle root to verify against - /// * `merkle_proof` - The proof path from the leaf to the root - /// - /// # Returns - /// - /// `true` if the proof is valid, `false` otherwise - pub fn verify_merkle_proof( - account: &T::AccountId, - amount: BalanceOf, - merkle_root: &MerkleRoot, - merkle_proof: &[MerkleHash], - ) -> bool { - let leaf = Self::calculate_leaf_hash_blake2(account, amount); - - // Verify the proof by walking up the tree - let mut computed_hash = leaf; - for proof_element in merkle_proof.iter() { - computed_hash = if computed_hash < *proof_element { - Self::calculate_parent_hash_blake2(&computed_hash, proof_element) - } else { - Self::calculate_parent_hash_blake2(proof_element, &computed_hash) - }; - } - computed_hash == *merkle_root - } - - /// Calculates the leaf hash for a Merkle tree using Blake2. - /// - /// This function creates a leaf node hash from an account and amount using the - /// Blake2 hash function, which is optimized for zero-knowledge proofs. - /// - /// # Parameters - /// - /// * `account` - The account ID to include in the leaf - /// * `amount` - The token amount to include in the leaf - /// - /// # Returns - /// - /// A 32-byte array containing the Blake2 hash of the account and amount - pub fn calculate_leaf_hash_blake2( - account: &T::AccountId, - amount: BalanceOf, - ) -> MerkleHash { - let bytes = (account, amount).encode(); - blake2_256(&bytes) - } - - /// Calculates the parent hash in a Merkle tree using Blake2. - /// - /// This function combines two child hashes to create their parent hash in the Merkle tree. - /// The children are ordered lexicographically before hashing to ensure consistency. - /// - /// # Parameters - /// - /// * `left` - The first child hash - /// * `right` - The second child hash - /// - /// # Returns - /// - /// A 32-byte array containing the Blake2 hash of the combined children - pub fn calculate_parent_hash_blake2(left: &MerkleHash, right: &MerkleHash) -> MerkleHash { - // Ensure consistent ordering of inputs (important for verification) - let combined = if left < right { - [left.as_slice(), right.as_slice()].concat() - } else { - [right.as_slice(), left.as_slice()].concat() - }; - - blake2_256(&combined) - } - } - - #[pallet::call] - impl Pallet { - /// Create a new airdrop with a Merkle root. - /// - /// The Merkle root is a cryptographic hash that represents all valid claims - /// for this airdrop. Users will later provide Merkle proofs to verify their - /// eligibility to claim tokens. - /// - /// # Parameters - /// - /// * `origin` - The origin of the call (must be signed) - /// * `merkle_root` - The Merkle root hash representing all valid claims - /// * `vesting_period` - Optional vesting period for the airdrop - /// * `vesting_delay` - Optional delay before vesting starts - #[pallet::call_index(0)] - #[pallet::weight(T::WeightInfo::create_airdrop())] - pub fn create_airdrop( - origin: OriginFor, - merkle_root: MerkleRoot, - vesting_period: Option>, - vesting_delay: Option>, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - - let airdrop_id = Self::next_airdrop_id(); - - let airdrop_metadata = AirdropMetadata { - merkle_root, - creator: who.clone(), - balance: Zero::zero(), - vesting_period, - vesting_delay, - }; - - AirdropInfo::::insert(airdrop_id, &airdrop_metadata); - NextAirdropId::::put(airdrop_id.saturating_add(1)); - - Self::deposit_event(Event::AirdropCreated { airdrop_id, airdrop_metadata }); - - Ok(()) - } - - /// Fund an existing airdrop with tokens. - /// - /// This function transfers tokens from the caller to the airdrop's account, - /// making them available for users to claim. - /// - /// # Parameters - /// - /// * `origin` - The origin of the call (must be signed) - /// * `airdrop_id` - The ID of the airdrop to fund - /// * `amount` - The amount of tokens to add to the airdrop - /// - /// # Errors - /// - /// * `AirdropNotFound` - If the specified airdrop does not exist - #[pallet::call_index(1)] - #[pallet::weight(T::WeightInfo::fund_airdrop())] - pub fn fund_airdrop( - origin: OriginFor, - airdrop_id: AirdropId, - amount: BalanceOf, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - - ensure!(AirdropInfo::::contains_key(airdrop_id), Error::::AirdropNotFound); - - CurrencyOf::::transfer( - &who, - &Self::account_id(), - amount, - frame_support::traits::ExistenceRequirement::KeepAlive, - )?; - - AirdropInfo::::mutate(airdrop_id, |maybe_metadata| { - if let Some(metadata) = maybe_metadata { - metadata.balance = metadata.balance.saturating_add(amount); - } - }); - - Self::deposit_event(Event::AirdropFunded { airdrop_id, amount }); - - Ok(()) - } - - /// Claim tokens from an airdrop by providing a Merkle proof. - /// - /// Users can claim their tokens by providing a proof of their eligibility. - /// The proof is verified against the airdrop's Merkle root. - /// Anyone can trigger a claim for any eligible recipient. - /// - /// # Parameters - /// - /// * `origin` - The origin of the call - /// * `airdrop_id` - The ID of the airdrop to claim from - /// * `amount` - The amount of tokens to claim - /// * `merkle_proof` - The Merkle proof verifying eligibility - /// - /// # Errors - /// - /// * `AirdropNotFound` - If the specified airdrop does not exist - /// * `AlreadyClaimed` - If the recipient has already claimed from this airdrop - /// * `InvalidProof` - If the provided Merkle proof is invalid - /// * `InsufficientAirdropBalance` - If the airdrop doesn't have enough tokens - #[pallet::call_index(2)] - #[pallet::weight(T::WeightInfo::claim(merkle_proof.len() as u32))] - pub fn claim( - origin: OriginFor, - airdrop_id: AirdropId, - recipient: T::AccountId, - amount: BalanceOf, - merkle_proof: BoundedVec, - ) -> DispatchResult { - ensure_none(origin)?; - - ensure!( - !Claimed::::contains_key(airdrop_id, &recipient), - Error::::AlreadyClaimed - ); - - let airdrop_metadata = - AirdropInfo::::get(airdrop_id).ok_or(Error::::AirdropNotFound)?; - - ensure!( - Self::verify_merkle_proof( - &recipient, - amount, - &airdrop_metadata.merkle_root, - &merkle_proof - ), - Error::::InvalidProof - ); - - ensure!(airdrop_metadata.balance >= amount, Error::::InsufficientAirdropBalance); - - // Mark as claimed before performing the transfer - Claimed::::insert(airdrop_id, &recipient, ()); - - AirdropInfo::::mutate(airdrop_id, |maybe_metadata| { - if let Some(metadata) = maybe_metadata { - metadata.balance = metadata.balance.saturating_sub(amount); - } - }); - - let per_block = if let Some(vesting_period) = airdrop_metadata.vesting_period { - amount - .checked_div(&T::BlockNumberToBalance::convert(vesting_period)) - .ok_or(Error::::InsufficientAirdropBalance)? - } else { - amount - }; - - let current_block = T::BlockNumberProvider::current_block_number(); - let vesting_start = - current_block.saturating_add(airdrop_metadata.vesting_delay.unwrap_or_default()); - - T::Vesting::vested_transfer( - &Self::account_id(), - &recipient, - amount, - per_block, - vesting_start, - )?; - - Self::deposit_event(Event::Claimed { airdrop_id, account: recipient, amount }); - - Ok(()) - } - - /// Delete an airdrop and reclaim any remaining funds. - /// - /// This function allows the creator of an airdrop to delete it and reclaim - /// any remaining tokens that haven't been claimed. - /// - /// # Parameters - /// - /// * `origin` - The origin of the call (must be the airdrop creator) - /// * `airdrop_id` - The ID of the airdrop to delete - /// - /// # Errors - /// - /// * `AirdropNotFound` - If the specified airdrop does not exist - /// * `NotAirdropCreator` - If the caller is not the creator of the airdrop - #[pallet::call_index(3)] - #[pallet::weight(T::WeightInfo::delete_airdrop())] - pub fn delete_airdrop(origin: OriginFor, airdrop_id: AirdropId) -> DispatchResult { - let who = ensure_signed(origin)?; - - let airdrop_metadata = - AirdropInfo::::take(airdrop_id).ok_or(Error::::AirdropNotFound)?; - - ensure!(airdrop_metadata.creator == who, Error::::NotAirdropCreator); - - CurrencyOf::::transfer( - &Self::account_id(), - &airdrop_metadata.creator, - airdrop_metadata.balance, - frame_support::traits::ExistenceRequirement::KeepAlive, - )?; - - Self::deposit_event(Event::AirdropDeleted { airdrop_id }); - - Ok(()) - } - } - - #[pallet::validate_unsigned] - impl ValidateUnsigned for Pallet { - type Call = Call; - - fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity { - if let Call::claim { airdrop_id, recipient, amount, merkle_proof } = call { - // 1. Check if airdrop exists - let airdrop_metadata = AirdropInfo::::get(airdrop_id).ok_or_else(|| { - let error = Error::::AirdropNotFound; - InvalidTransaction::Custom(error.to_code()) - })?; - - // 2. Check if already claimed - if Claimed::::contains_key(airdrop_id, recipient) { - let error = Error::::AlreadyClaimed; - return InvalidTransaction::Custom(error.to_code()).into(); - } - - // 3. Verify Merkle Proof - if !Self::verify_merkle_proof( - recipient, - *amount, - &airdrop_metadata.merkle_root, - merkle_proof, - ) { - let error = Error::::InvalidProof; - return InvalidTransaction::Custom(error.to_code()).into(); - } - - Ok(ValidTransaction { - priority: T::UnsignedClaimPriority::get(), - requires: vec![], - provides: vec![(airdrop_id, recipient, amount).encode()], - longevity: TransactionLongevity::MAX, - propagate: true, - }) - } else { - log::error!(target: "merkle-airdrop", "ValidateUnsigned: Received non-claim transaction or unexpected call structure"); - InvalidTransaction::Call.into() - } - } - } -} diff --git a/pallets/merkle-airdrop/src/mock.rs b/pallets/merkle-airdrop/src/mock.rs deleted file mode 100644 index 0a5c865c..00000000 --- a/pallets/merkle-airdrop/src/mock.rs +++ /dev/null @@ -1,129 +0,0 @@ -use crate as pallet_merkle_airdrop; -use frame_support::{ - parameter_types, - traits::{ConstU32, Everything, WithdrawReasons}, - PalletId, -}; -use frame_system::{self as system}; -use sp_core::H256; -use sp_runtime::{ - traits::{BlakeTwo256, ConvertInto, IdentityLookup}, - BuildStorage, -}; - -type Block = frame_system::mocking::MockBlock; - -// Configure a mock runtime to test the pallet. -frame_support::construct_runtime!( - pub enum Test { - System: frame_system, - Vesting: pallet_vesting, - Balances: pallet_balances, - MerkleAirdrop: pallet_merkle_airdrop, - } -); - -parameter_types! { - pub const BlockHashCount: u64 = 250; - pub const SS58Prefix: u8 = 189; -} - -impl system::Config for Test { - type BaseCallFilter = Everything; - type BlockWeights = (); - type BlockLength = (); - type DbWeight = (); - type RuntimeOrigin = RuntimeOrigin; - type RuntimeCall = RuntimeCall; - type Nonce = u64; - type Hash = H256; - type Hashing = BlakeTwo256; - type AccountId = u64; - type Lookup = IdentityLookup; - type Block = Block; - type BlockHashCount = BlockHashCount; - type Version = (); - type PalletInfo = PalletInfo; - type AccountData = pallet_balances::AccountData; - type OnNewAccount = (); - type OnKilledAccount = (); - type SystemWeightInfo = (); - type SS58Prefix = SS58Prefix; - type OnSetCode = (); - type MaxConsumers = ConstU32<16>; - type RuntimeTask = (); - type ExtensionsWeightInfo = (); - type SingleBlockMigrations = (); - type MultiBlockMigrator = (); - type PreInherents = (); - type PostInherents = (); - type PostTransactions = (); - type RuntimeEvent = RuntimeEvent; -} - -parameter_types! { - pub const ExistentialDeposit: u64 = 1; - pub const MaxLocks: u32 = 50; -} - -impl pallet_balances::Config for Test { - type Balance = u64; - type DustRemoval = (); - type ExistentialDeposit = ExistentialDeposit; - type AccountStore = System; - type WeightInfo = (); - type MaxLocks = MaxLocks; - type MaxReserves = (); - type ReserveIdentifier = [u8; 8]; - type RuntimeHoldReason = (); - type FreezeIdentifier = (); - type MaxFreezes = (); - type RuntimeFreezeReason = (); - type DoneSlashHandler = (); -} - -parameter_types! { - pub const MinVestedTransfer: u64 = 1; - pub UnvestedFundsAllowedWithdrawReasons: WithdrawReasons = - WithdrawReasons::except(WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE); -} - -impl pallet_vesting::Config for Test { - type RuntimeEvent = RuntimeEvent; - type Currency = Balances; - type WeightInfo = (); - type BlockNumberProvider = System; - type MinVestedTransfer = MinVestedTransfer; - type BlockNumberToBalance = ConvertInto; - type UnvestedFundsAllowedWithdrawReasons = UnvestedFundsAllowedWithdrawReasons; - - const MAX_VESTING_SCHEDULES: u32 = 3; -} - -parameter_types! { - pub const MaxProofs: u32 = 100; - pub const MerkleAirdropPalletId: PalletId = PalletId(*b"airdrop!"); - pub const UnsignedClaimPriority: u64 = 100; -} - -impl pallet_merkle_airdrop::Config for Test { - type Vesting = Vesting; - type MaxProofs = MaxProofs; - type PalletId = MerkleAirdropPalletId; - type UnsignedClaimPriority = UnsignedClaimPriority; - type WeightInfo = (); - type BlockNumberProvider = System; - type BlockNumberToBalance = ConvertInto; -} - -// Build genesis storage according to the mock runtime. -pub fn new_test_ext() -> sp_io::TestExternalities { - let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); - pallet_balances::GenesisConfig:: { - balances: vec![(1, 10_000_000), (MerkleAirdrop::account_id(), 1)], - } - .assimilate_storage(&mut t) - .unwrap(); - - t.into() -} diff --git a/pallets/merkle-airdrop/src/tests.rs b/pallets/merkle-airdrop/src/tests.rs deleted file mode 100644 index a142b6c5..00000000 --- a/pallets/merkle-airdrop/src/tests.rs +++ /dev/null @@ -1,591 +0,0 @@ -#![allow(clippy::unit_cmp)] - -use crate::{mock::*, Error, Event}; -use codec::Encode; -use frame_support::{ - assert_noop, assert_ok, - traits::{InspectLockableCurrency, LockIdentifier}, - BoundedVec, -}; -use sp_core::blake2_256; -use sp_runtime::TokenError; - -fn bounded_proof(proof: Vec<[u8; 32]>) -> BoundedVec<[u8; 32], MaxProofs> { - proof.try_into().expect("Proof exceeds maximum size") -} - -// Helper function to calculate a leaf hash for testing -fn calculate_leaf_hash(account: &u64, amount: u64) -> [u8; 32] { - let account_bytes = account.encode(); - let amount_bytes = amount.encode(); - let leaf_data = [&account_bytes[..], &amount_bytes[..]].concat(); - - blake2_256(&leaf_data) -} - -// Helper function to calculate a parent hash for testing -fn calculate_parent_hash(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] { - let combined = if left < right { - [&left[..], &right[..]].concat() - } else { - [&right[..], &left[..]].concat() - }; - - blake2_256(&combined) -} - -const VESTING_ID: LockIdentifier = *b"vesting "; - -#[test] -fn create_airdrop_works() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - let merkle_root = [0u8; 32]; - assert_ok!(MerkleAirdrop::create_airdrop( - RuntimeOrigin::signed(1), - merkle_root, - Some(100), - Some(10) - )); - - let airdrop_metadata = crate::AirdropMetadata { - merkle_root, - creator: 1, - balance: 0, - vesting_period: Some(100), - vesting_delay: Some(10), - }; - - System::assert_last_event( - Event::AirdropCreated { airdrop_id: 0, airdrop_metadata: airdrop_metadata.clone() } - .into(), - ); - - assert_eq!(MerkleAirdrop::airdrop_info(0), Some(airdrop_metadata)); - }); -} - -#[test] -fn fund_airdrop_works() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - let merkle_root = [0u8; 32]; - let amount = 100; - - assert_ok!(MerkleAirdrop::create_airdrop( - RuntimeOrigin::signed(1), - merkle_root, - Some(10), - Some(10) - )); - - assert_eq!(MerkleAirdrop::airdrop_info(0).unwrap().balance, 0); - - // fund airdrop with insufficient balance should fail - assert_noop!( - MerkleAirdrop::fund_airdrop(RuntimeOrigin::signed(123456), 0, amount * 10000), - TokenError::FundsUnavailable, - ); - - assert_ok!(MerkleAirdrop::fund_airdrop(RuntimeOrigin::signed(1), 0, amount)); - - System::assert_last_event(Event::AirdropFunded { airdrop_id: 0, amount }.into()); - - // Check that the airdrop balance was updated - assert_eq!(MerkleAirdrop::airdrop_info(0).unwrap().balance, amount); - - // Check that the balance was transferred - assert_eq!(Balances::free_balance(1), 9999900); // 10000000 - 100 - assert_eq!(Balances::free_balance(MerkleAirdrop::account_id()), 101); - - assert_ok!(MerkleAirdrop::fund_airdrop(RuntimeOrigin::signed(1), 0, amount)); - - assert_eq!(MerkleAirdrop::airdrop_info(0).unwrap().balance, amount * 2); - assert_eq!(Balances::free_balance(1), 9999800); // 9999900 - 100 - assert_eq!(Balances::free_balance(MerkleAirdrop::account_id()), 201); // locked for vesting - }); -} - -#[test] -fn claim_works() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - let account1: u64 = 2; // Account that will claim - let amount1: u64 = 500; - let account2: u64 = 3; - let amount2: u64 = 300; - - let leaf1 = calculate_leaf_hash(&account1, amount1); - let leaf2 = calculate_leaf_hash(&account2, amount2); - let merkle_root = calculate_parent_hash(&leaf1, &leaf2); - - assert_ok!(MerkleAirdrop::create_airdrop( - RuntimeOrigin::signed(1), - merkle_root, - Some(100), - Some(2) - )); - assert_ok!(MerkleAirdrop::fund_airdrop(RuntimeOrigin::signed(1), 0, 1000)); - - // Create proof for account1d - let merkle_proof = bounded_proof(vec![leaf2]); - - assert_ok!(MerkleAirdrop::claim(RuntimeOrigin::none(), 0, 2, 500, merkle_proof.clone())); - - System::assert_last_event(Event::Claimed { airdrop_id: 0, account: 2, amount: 500 }.into()); - - assert_eq!(MerkleAirdrop::is_claimed(0, 2), ()); - assert_eq!(Balances::balance_locked(VESTING_ID, &2), 500); // Unlocked - - assert_eq!(Balances::free_balance(2), 500); - assert_eq!(Balances::free_balance(MerkleAirdrop::account_id()), 501); // 1 (initial) + 1000 - // (funded) - 500 (claimed) - }); -} - -#[test] -fn create_airdrop_requires_signed_origin() { - new_test_ext().execute_with(|| { - let merkle_root = [0u8; 32]; - - assert_noop!( - MerkleAirdrop::create_airdrop(RuntimeOrigin::none(), merkle_root, None, None), - frame_support::error::BadOrigin - ); - }); -} - -#[test] -fn fund_airdrop_fails_for_nonexistent_airdrop() { - new_test_ext().execute_with(|| { - assert_noop!( - MerkleAirdrop::fund_airdrop(RuntimeOrigin::signed(1), 999, 1000), - Error::::AirdropNotFound - ); - }); -} - -#[test] -fn claim_fails_for_nonexistent_airdrop() { - new_test_ext().execute_with(|| { - let merkle_proof = bounded_proof(vec![[0u8; 32]]); - - assert_noop!( - MerkleAirdrop::claim(RuntimeOrigin::none(), 999, 1, 500, merkle_proof), - Error::::AirdropNotFound - ); - }); -} - -#[test] -fn claim_already_claimed() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - let account1: u64 = 2; // Account that will claim - let amount1: u64 = 500; - let account2: u64 = 3; - let amount2: u64 = 300; - - let leaf1 = calculate_leaf_hash(&account1, amount1); - let leaf2 = calculate_leaf_hash(&account2, amount2); - let merkle_root = calculate_parent_hash(&leaf1, &leaf2); - - assert_ok!(MerkleAirdrop::create_airdrop( - RuntimeOrigin::signed(1), - merkle_root, - Some(100), - Some(10) - )); - assert_ok!(MerkleAirdrop::fund_airdrop(RuntimeOrigin::signed(1), 0, 1000)); - - let merkle_proof = bounded_proof(vec![leaf2]); - - assert_ok!(MerkleAirdrop::claim(RuntimeOrigin::none(), 0, 2, 500, merkle_proof.clone())); - - // Try to claim again - assert_noop!( - MerkleAirdrop::claim(RuntimeOrigin::none(), 0, 2, 500, merkle_proof.clone()), - Error::::AlreadyClaimed - ); - }); -} - -#[test] -fn verify_merkle_proof_works() { - new_test_ext().execute_with(|| { - // Create test accounts and amounts - let account1: u64 = 1; - let amount1: u64 = 500; - let account2: u64 = 2; - let amount2: u64 = 300; - - // Calculate leaf hashes - let leaf1 = calculate_leaf_hash(&account1, amount1); - let leaf2 = calculate_leaf_hash(&account2, amount2); - - // Calculate the Merkle root (hash of the two leaves) - let merkle_root = calculate_parent_hash(&leaf1, &leaf2); - - // Create proofs - let proof_for_account1 = vec![leaf2]; - let proof_for_account2 = vec![leaf1]; - - // Test the verify_merkle_proof function directly - assert!( - MerkleAirdrop::verify_merkle_proof( - &account1, - amount1, - &merkle_root, - &proof_for_account1 - ), - "Proof for account1 should be valid" - ); - - assert!( - MerkleAirdrop::verify_merkle_proof( - &account2, - amount2, - &merkle_root, - &proof_for_account2 - ), - "Proof for account2 should be valid" - ); - - assert!( - !MerkleAirdrop::verify_merkle_proof( - &account1, - 400, // Wrong amount - &merkle_root, - &proof_for_account1 - ), - "Proof with wrong amount should be invalid" - ); - - let wrong_proof = vec![[1u8; 32]]; - assert!( - !MerkleAirdrop::verify_merkle_proof(&account1, amount1, &merkle_root, &wrong_proof), - "Wrong proof should be invalid" - ); - - assert!( - !MerkleAirdrop::verify_merkle_proof( - &3, // Wrong account - amount1, - &merkle_root, - &proof_for_account1 - ), - "Proof with wrong account should be invalid" - ); - }); -} - -#[test] -fn claim_invalid_proof_fails() { - new_test_ext().execute_with(|| { - let account1: u64 = 2; - let amount1: u64 = 500; - let account2: u64 = 3; - let amount2: u64 = 300; - - let leaf1 = calculate_leaf_hash(&account1, amount1); - let leaf2 = calculate_leaf_hash(&account2, amount2); - let merkle_root = calculate_parent_hash(&leaf1, &leaf2); - - assert_ok!(MerkleAirdrop::create_airdrop( - RuntimeOrigin::signed(1), - merkle_root, - Some(100), - Some(10) - )); - assert_ok!(MerkleAirdrop::fund_airdrop(RuntimeOrigin::signed(1), 0, 1000)); - - let invalid_proof = bounded_proof(vec![[1u8; 32]]); // Different from the actual leaf2 - - assert_noop!( - MerkleAirdrop::claim(RuntimeOrigin::none(), 0, 2, 500, invalid_proof), - Error::::InvalidProof - ); - }); -} - -#[test] -fn claim_insufficient_airdrop_balance_fails() { - new_test_ext().execute_with(|| { - // Create a valid merkle tree - let account1: u64 = 2; - let amount1: u64 = 500; - let account2: u64 = 3; - let amount2: u64 = 300; - - let leaf1 = calculate_leaf_hash(&account1, amount1); - let leaf2 = calculate_leaf_hash(&account2, amount2); - let merkle_root = calculate_parent_hash(&leaf1, &leaf2); - - assert_ok!(MerkleAirdrop::create_airdrop( - RuntimeOrigin::signed(1), - merkle_root, - Some(1000), - Some(100) - )); - assert_ok!(MerkleAirdrop::fund_airdrop(RuntimeOrigin::signed(1), 0, 400)); // Fund less than claim amount - - let merkle_proof = bounded_proof(vec![leaf2]); - - // Attempt to claim more than available - assert_noop!( - MerkleAirdrop::claim(RuntimeOrigin::none(), 0, 2, 500, merkle_proof), - Error::::InsufficientAirdropBalance - ); - }); -} - -#[test] -fn claim_nonexistent_airdrop_fails() { - new_test_ext().execute_with(|| { - // Attempt to claim from a nonexistent airdrop - assert_noop!( - MerkleAirdrop::claim( - RuntimeOrigin::none(), - 999, - 2, - 500, - bounded_proof(vec![[0u8; 32]]) - ), - Error::::AirdropNotFound - ); - }); -} - -#[test] -fn claim_updates_balances_correctly() { - new_test_ext().execute_with(|| { - // Create a valid merkle tree - let account1: u64 = 2; - let amount1: u64 = 500; - let account2: u64 = 3; - let amount2: u64 = 300; - - let leaf1 = calculate_leaf_hash(&account1, amount1); - let leaf2 = calculate_leaf_hash(&account2, amount2); - let merkle_root = calculate_parent_hash(&leaf1, &leaf2); - - assert_ok!(MerkleAirdrop::create_airdrop( - RuntimeOrigin::signed(1), - merkle_root, - Some(100), - Some(10) - )); - assert_ok!(MerkleAirdrop::fund_airdrop(RuntimeOrigin::signed(1), 0, 1000)); - - let initial_account_balance = Balances::free_balance(2); - let initial_pallet_balance = Balances::free_balance(MerkleAirdrop::account_id()); - - assert_ok!(MerkleAirdrop::claim( - RuntimeOrigin::none(), - 0, - 2, - 500, - bounded_proof(vec![leaf2]) - )); - - assert_eq!(Balances::free_balance(2), initial_account_balance + 500); - assert_eq!( - Balances::free_balance(MerkleAirdrop::account_id()), - initial_pallet_balance - 500 - ); - - assert_eq!(MerkleAirdrop::airdrop_info(0).unwrap().balance, 500); - assert_eq!(MerkleAirdrop::is_claimed(0, 2), ()); - }); -} - -#[test] -fn multiple_users_can_claim() { - new_test_ext().execute_with(|| { - let account1: u64 = 2; - let amount1: u64 = 5000; - let account2: u64 = 3; - let amount2: u64 = 3000; - let account3: u64 = 4; - let amount3: u64 = 2000; - - let leaf1 = calculate_leaf_hash(&account1, amount1); - let leaf2 = calculate_leaf_hash(&account2, amount2); - let leaf3 = calculate_leaf_hash(&account3, amount3); - let parent1 = calculate_parent_hash(&leaf1, &leaf2); - let merkle_root = calculate_parent_hash(&parent1, &leaf3); - - assert_ok!(MerkleAirdrop::create_airdrop( - RuntimeOrigin::signed(1), - merkle_root, - Some(1000), - Some(10) - )); - assert_ok!(MerkleAirdrop::fund_airdrop(RuntimeOrigin::signed(1), 0, 10001)); - - // User 1 claims - let proof1 = bounded_proof(vec![leaf2, leaf3]); - assert_ok!(MerkleAirdrop::claim(RuntimeOrigin::none(), 0, 2, 5000, proof1)); - assert_eq!(Balances::free_balance(2), 5000); // free balance but it's locked for vesting - assert_eq!(Balances::balance_locked(VESTING_ID, &2), 5000); - - // User 2 claims - let proof2 = bounded_proof(vec![leaf1, leaf3]); - assert_ok!(MerkleAirdrop::claim(RuntimeOrigin::none(), 0, 3, 3000, proof2)); - assert_eq!(Balances::free_balance(3), 3000); - assert_eq!(Balances::balance_locked(VESTING_ID, &3), 3000); - - // User 3 claims - let proof3 = bounded_proof(vec![parent1]); - assert_ok!(MerkleAirdrop::claim(RuntimeOrigin::none(), 0, 4, 2000, proof3)); - assert_eq!(Balances::free_balance(4), 2000); - assert_eq!(Balances::balance_locked(VESTING_ID, &4), 2000); - - assert_eq!(MerkleAirdrop::airdrop_info(0).unwrap().balance, 1); - - assert_eq!(MerkleAirdrop::is_claimed(0, 2), ()); - assert_eq!(MerkleAirdrop::is_claimed(0, 3), ()); - assert_eq!(MerkleAirdrop::is_claimed(0, 4), ()); - }); -} - -#[test] -fn delete_airdrop_works() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - let merkle_root = [0u8; 32]; - let creator = 1; - - assert_ok!(MerkleAirdrop::create_airdrop( - RuntimeOrigin::signed(creator), - merkle_root, - Some(100), - Some(10) - )); - - let airdrop_info = MerkleAirdrop::airdrop_info(0).unwrap(); - - assert_eq!(airdrop_info.creator, creator); - - // Delete the airdrop (balance is zero) - assert_ok!(MerkleAirdrop::delete_airdrop(RuntimeOrigin::signed(creator), 0)); - - System::assert_last_event(Event::AirdropDeleted { airdrop_id: 0 }.into()); - - // Check that the airdrop no longer exists - assert!(MerkleAirdrop::airdrop_info(0).is_none()); - }); -} - -#[test] -fn delete_airdrop_with_balance_refunds_creator() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - let merkle_root = [0u8; 32]; - let creator = 1; - let initial_creator_balance = Balances::free_balance(creator); - let fund_amount = 100; - - assert_ok!(MerkleAirdrop::create_airdrop( - RuntimeOrigin::signed(creator), - merkle_root, - Some(100), - Some(10) - )); - - assert_ok!(MerkleAirdrop::fund_airdrop(RuntimeOrigin::signed(creator), 0, fund_amount)); - - // Creator's balance should be reduced by fund_amount - assert_eq!(Balances::free_balance(creator), initial_creator_balance - fund_amount); - - assert_ok!(MerkleAirdrop::delete_airdrop(RuntimeOrigin::signed(creator), 0)); - - // Check that the funds were returned to the creator - assert_eq!(Balances::free_balance(creator), initial_creator_balance); - - System::assert_last_event(Event::AirdropDeleted { airdrop_id: 0 }.into()); - }); -} - -#[test] -fn delete_airdrop_non_creator_fails() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - let merkle_root = [0u8; 32]; - let creator = 1; - let non_creator = 2; - - assert_ok!(MerkleAirdrop::create_airdrop( - RuntimeOrigin::signed(creator), - merkle_root, - Some(100), - Some(10) - )); - - assert_noop!( - MerkleAirdrop::delete_airdrop(RuntimeOrigin::signed(non_creator), 0), - Error::::NotAirdropCreator - ); - }); -} - -#[test] -fn delete_airdrop_nonexistent_fails() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - assert_noop!( - MerkleAirdrop::delete_airdrop(RuntimeOrigin::signed(1), 999), - Error::::AirdropNotFound - ); - }); -} - -#[test] -fn delete_airdrop_after_claims_works() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - let creator: u64 = 1; - let initial_creator_balance = Balances::free_balance(creator); - let account1: u64 = 2; - let amount1: u64 = 500; - let account2: u64 = 3; - let amount2: u64 = 300; - let total_fund = 1000; - - let leaf1 = calculate_leaf_hash(&account1, amount1); - let leaf2 = calculate_leaf_hash(&account2, amount2); - let merkle_root = calculate_parent_hash(&leaf1, &leaf2); - - assert_ok!(MerkleAirdrop::create_airdrop( - RuntimeOrigin::signed(creator), - merkle_root, - Some(100), - Some(10) - )); - assert_ok!(MerkleAirdrop::fund_airdrop(RuntimeOrigin::signed(creator), 0, total_fund)); - - // Let only one account claim (partial claiming) - let proof1 = bounded_proof(vec![leaf2]); - assert_ok!(MerkleAirdrop::claim(RuntimeOrigin::none(), 0, account1, amount1, proof1)); - - // Check that some balance remains - assert_eq!(MerkleAirdrop::airdrop_info(0).unwrap().balance, total_fund - amount1); - - // Now the creator deletes the airdrop with remaining balance - assert_ok!(MerkleAirdrop::delete_airdrop(RuntimeOrigin::signed(creator), 0)); - - // Check creator was refunded the unclaimed amount - assert_eq!( - Balances::free_balance(creator), - initial_creator_balance - total_fund + (total_fund - amount1) - ); - }); -} diff --git a/pallets/merkle-airdrop/src/weights.rs b/pallets/merkle-airdrop/src/weights.rs deleted file mode 100644 index c0213e38..00000000 --- a/pallets/merkle-airdrop/src/weights.rs +++ /dev/null @@ -1,193 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - - -//! Autogenerated weights for `pallet_merkle_airdrop` -//! -//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 47.2.0 -//! DATE: 2025-06-24, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` -//! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `MacBook-Pro-4.local`, CPU: `` -//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` - -// Executed Command: -// frame-omni-bencher -// v1 -// benchmark -// pallet -// --runtime -// ./target/release/wbuild/quantus-runtime/quantus_runtime.wasm -// --pallet -// pallet-merkle-airdrop -// --extrinsic -// * -// --template -// ./.maintain/frame-weight-template.hbs -// --output -// ./pallets/merkle-airdrop/src/weights.rs - -#![cfg_attr(rustfmt, rustfmt_skip)] -#![allow(unused_parens)] -#![allow(unused_imports)] -#![allow(missing_docs)] -#![allow(dead_code)] - -use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; -use core::marker::PhantomData; - -/// Weight functions needed for `pallet_merkle_airdrop`. -pub trait WeightInfo { - fn create_airdrop() -> Weight; - fn fund_airdrop() -> Weight; - fn claim(p: u32, ) -> Weight; - fn delete_airdrop() -> Weight; -} - -/// Weights for `pallet_merkle_airdrop` using the Substrate node and recommended hardware. -pub struct SubstrateWeight(PhantomData); -impl WeightInfo for SubstrateWeight { - /// Storage: `MerkleAirdrop::NextAirdropId` (r:1 w:1) - /// Proof: `MerkleAirdrop::NextAirdropId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) - /// Storage: `MerkleAirdrop::AirdropInfo` (r:0 w:1) - /// Proof: `MerkleAirdrop::AirdropInfo` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) - fn create_airdrop() -> Weight { - // Proof Size summary in bytes: - // Measured: `6` - // Estimated: `1489` - // Minimum execution time: 7_000_000 picoseconds. - Weight::from_parts(8_000_000, 1489) - .saturating_add(T::DbWeight::get().reads(1_u64)) - .saturating_add(T::DbWeight::get().writes(2_u64)) - } - /// Storage: `MerkleAirdrop::AirdropInfo` (r:1 w:1) - /// Proof: `MerkleAirdrop::AirdropInfo` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn fund_airdrop() -> Weight { - // Proof Size summary in bytes: - // Measured: `262` - // Estimated: `3593` - // Minimum execution time: 40_000_000 picoseconds. - Weight::from_parts(42_000_000, 3593) - .saturating_add(T::DbWeight::get().reads(2_u64)) - .saturating_add(T::DbWeight::get().writes(2_u64)) - } - /// Storage: `MerkleAirdrop::Claimed` (r:1 w:1) - /// Proof: `MerkleAirdrop::Claimed` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) - /// Storage: `MerkleAirdrop::AirdropInfo` (r:1 w:1) - /// Proof: `MerkleAirdrop::AirdropInfo` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) - /// Storage: `Vesting::Vesting` (r:1 w:1) - /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(1057), added: 3532, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:2 w:2) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `Balances::Locks` (r:1 w:1) - /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) - /// Storage: `Balances::Freezes` (r:1 w:0) - /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`) - /// The range of component `p` is `[0, 100]`. - fn claim(p: u32, ) -> Weight { - // Proof Size summary in bytes: - // Measured: `441` - // Estimated: `6196` - // Minimum execution time: 73_000_000 picoseconds. - Weight::from_parts(74_879_630, 6196) - // Standard Error: 1_851 - .saturating_add(Weight::from_parts(368_666, 0).saturating_mul(p.into())) - .saturating_add(T::DbWeight::get().reads(7_u64)) - .saturating_add(T::DbWeight::get().writes(6_u64)) - } - /// Storage: `MerkleAirdrop::AirdropInfo` (r:1 w:1) - /// Proof: `MerkleAirdrop::AirdropInfo` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn delete_airdrop() -> Weight { - // Proof Size summary in bytes: - // Measured: `262` - // Estimated: `3593` - // Minimum execution time: 39_000_000 picoseconds. - Weight::from_parts(39_000_000, 3593) - .saturating_add(T::DbWeight::get().reads(2_u64)) - .saturating_add(T::DbWeight::get().writes(2_u64)) - } -} - -// For backwards compatibility and tests. -impl WeightInfo for () { - /// Storage: `MerkleAirdrop::NextAirdropId` (r:1 w:1) - /// Proof: `MerkleAirdrop::NextAirdropId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) - /// Storage: `MerkleAirdrop::AirdropInfo` (r:0 w:1) - /// Proof: `MerkleAirdrop::AirdropInfo` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) - fn create_airdrop() -> Weight { - // Proof Size summary in bytes: - // Measured: `6` - // Estimated: `1489` - // Minimum execution time: 7_000_000 picoseconds. - Weight::from_parts(8_000_000, 1489) - .saturating_add(RocksDbWeight::get().reads(1_u64)) - .saturating_add(RocksDbWeight::get().writes(2_u64)) - } - /// Storage: `MerkleAirdrop::AirdropInfo` (r:1 w:1) - /// Proof: `MerkleAirdrop::AirdropInfo` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn fund_airdrop() -> Weight { - // Proof Size summary in bytes: - // Measured: `262` - // Estimated: `3593` - // Minimum execution time: 40_000_000 picoseconds. - Weight::from_parts(42_000_000, 3593) - .saturating_add(RocksDbWeight::get().reads(2_u64)) - .saturating_add(RocksDbWeight::get().writes(2_u64)) - } - /// Storage: `MerkleAirdrop::Claimed` (r:1 w:1) - /// Proof: `MerkleAirdrop::Claimed` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) - /// Storage: `MerkleAirdrop::AirdropInfo` (r:1 w:1) - /// Proof: `MerkleAirdrop::AirdropInfo` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) - /// Storage: `Vesting::Vesting` (r:1 w:1) - /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(1057), added: 3532, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:2 w:2) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `Balances::Locks` (r:1 w:1) - /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) - /// Storage: `Balances::Freezes` (r:1 w:0) - /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`) - /// The range of component `p` is `[0, 100]`. - fn claim(p: u32, ) -> Weight { - // Proof Size summary in bytes: - // Measured: `441` - // Estimated: `6196` - // Minimum execution time: 73_000_000 picoseconds. - Weight::from_parts(74_879_630, 6196) - // Standard Error: 1_851 - .saturating_add(Weight::from_parts(368_666, 0).saturating_mul(p.into())) - .saturating_add(RocksDbWeight::get().reads(7_u64)) - .saturating_add(RocksDbWeight::get().writes(6_u64)) - } - /// Storage: `MerkleAirdrop::AirdropInfo` (r:1 w:1) - /// Proof: `MerkleAirdrop::AirdropInfo` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn delete_airdrop() -> Weight { - // Proof Size summary in bytes: - // Measured: `262` - // Estimated: `3593` - // Minimum execution time: 39_000_000 picoseconds. - Weight::from_parts(39_000_000, 3593) - .saturating_add(RocksDbWeight::get().reads(2_u64)) - .saturating_add(RocksDbWeight::get().writes(2_u64)) - } -} diff --git a/primitives/header/src/lib.rs b/primitives/header/src/lib.rs index bbe897fe..00dac653 100644 --- a/primitives/header/src/lib.rs +++ b/primitives/header/src/lib.rs @@ -30,7 +30,10 @@ use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] #[cfg_attr(feature = "serde", serde(deny_unknown_fields))] -pub struct Header + TryFrom, Hash: HashT> { +pub struct Header +where + Number: Copy + Into + TryFrom, +{ pub parent_hash: Hash::Output, #[cfg_attr( feature = "serde", @@ -123,7 +126,7 @@ where // We override the default hashing function to use // a felt aligned pre-image for poseidon hashing. fn hash(&self) -> Self::Hash { - Header::hash(&self) + Header::hash(self) } } diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 51c7b67f..6f039fe1 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -31,7 +31,6 @@ pallet-assets.workspace = true pallet-assets-holder = { workspace = true, default-features = false } pallet-balances.workspace = true pallet-conviction-voting.workspace = true -pallet-merkle-airdrop.workspace = true pallet-mining-rewards.workspace = true pallet-preimage.workspace = true pallet-qpow.workspace = true @@ -46,7 +45,6 @@ pallet-transaction-payment.workspace = true pallet-transaction-payment-rpc-runtime-api.workspace = true pallet-treasury.workspace = true pallet-utility.workspace = true -pallet-vesting.workspace = true primitive-types.workspace = true qp-dilithium-crypto.workspace = true qp-header = { workspace = true, features = ["serde"] } @@ -96,7 +94,6 @@ std = [ "pallet-assets/std", "pallet-balances/std", "pallet-conviction-voting/std", - "pallet-merkle-airdrop/std", "pallet-mining-rewards/std", "pallet-preimage/std", "pallet-qpow/std", @@ -111,7 +108,6 @@ std = [ "pallet-transaction-payment/std", "pallet-treasury/std", "pallet-utility/std", - "pallet-vesting/std", "primitive-types/std", "qp-dilithium-crypto/full_crypto", "qp-dilithium-crypto/std", @@ -145,7 +141,6 @@ runtime-benchmarks = [ "pallet-assets/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-conviction-voting/runtime-benchmarks", - "pallet-merkle-airdrop/runtime-benchmarks", "pallet-mining-rewards/runtime-benchmarks", "pallet-preimage/runtime-benchmarks", "pallet-qpow/runtime-benchmarks", @@ -158,7 +153,6 @@ runtime-benchmarks = [ "pallet-timestamp/runtime-benchmarks", "pallet-transaction-payment/runtime-benchmarks", "pallet-treasury/runtime-benchmarks", - "pallet-vesting/runtime-benchmarks", "sp-runtime/runtime-benchmarks", ] @@ -177,7 +171,6 @@ try-runtime = [ "pallet-timestamp/try-runtime", "pallet-transaction-payment/try-runtime", "pallet-treasury/try-runtime", - "pallet-vesting/try-runtime", "sp-runtime/try-runtime", ] diff --git a/runtime/src/benchmarks.rs b/runtime/src/benchmarks.rs index c670981c..6efb6e0c 100644 --- a/runtime/src/benchmarks.rs +++ b/runtime/src/benchmarks.rs @@ -30,7 +30,6 @@ frame_benchmarking::define_benchmarks!( [pallet_timestamp, Timestamp] [pallet_sudo, Sudo] [pallet_reversible_transfers, ReversibleTransfers] - [pallet_merkle_airdrop, MerkleAirdrop] [pallet_mining_rewards, MiningRewards] [pallet_scheduler, Scheduler] [pallet_qpow, QPoW] diff --git a/runtime/src/configs/mod.rs b/runtime/src/configs/mod.rs index e6958e48..20ab50f4 100644 --- a/runtime/src/configs/mod.rs +++ b/runtime/src/configs/mod.rs @@ -39,7 +39,7 @@ use frame_support::{ derive_impl, parameter_types, traits::{ AsEnsureOriginWithArg, ConstU128, ConstU32, ConstU8, EitherOf, Get, NeverEnsureOrigin, - VariantCountOf, WithdrawReasons, + VariantCountOf, }, weights::{ constants::{RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND}, @@ -55,17 +55,14 @@ use pallet_ranked_collective::Linear; use pallet_transaction_payment::{ConstFeeMultiplier, FungibleAdapter, Multiplier}; use qp_poseidon::PoseidonHasher; use qp_scheduler::BlockNumberOrTimestamp; -use sp_runtime::{ - traits::{ConvertInto, One}, - FixedU128, Perbill, Permill, -}; +use sp_runtime::{traits::One, FixedU128, Perbill, Permill}; use sp_version::RuntimeVersion; // Local module imports use super::{ AccountId, Balance, Balances, Block, BlockNumber, Hash, Nonce, OriginCaller, PalletInfo, Preimage, Referenda, Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, - RuntimeHoldReason, RuntimeOrigin, RuntimeTask, Scheduler, System, Timestamp, Vesting, DAYS, + RuntimeHoldReason, RuntimeOrigin, RuntimeTask, Scheduler, System, Timestamp, DAYS, EXISTENTIAL_DEPOSIT, MICRO_UNIT, TARGET_BLOCK_TIME_MS, UNIT, VERSION, }; use sp_core::U512; @@ -85,8 +82,6 @@ parameter_types! { // To upload, 10Mbs link takes 4.1s and 100Mbs takes 500ms pub RuntimeBlockLength: BlockLength = BlockLength::max_with_normal_ratio(5 * 1024 * 1024, NORMAL_DISPATCH_RATIO); pub const SS58Prefix: u8 = 189; - pub const MerkleAirdropPalletId: PalletId = PalletId(*b"airdrop!"); - pub const UnsignedClaimPriority: u32 = 100; } /// The default types are being injected by [`derive_impl`](`frame_support::derive_impl`) from @@ -425,25 +420,6 @@ impl pallet_sudo::Config for Runtime { type WeightInfo = pallet_sudo::weights::SubstrateWeight; } -parameter_types! { - pub const MinVestedTransfer: Balance = UNIT; - /// Unvested funds can be transferred and reserved for any other means (reserves overlap) - pub UnvestedFundsAllowedWithdrawReasons: WithdrawReasons = - WithdrawReasons::except(WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE); -} - -impl pallet_vesting::Config for Runtime { - type Currency = Balances; - type RuntimeEvent = RuntimeEvent; - type WeightInfo = pallet_vesting::weights::SubstrateWeight; - type MinVestedTransfer = MinVestedTransfer; - type BlockNumberToBalance = ConvertInto; - type UnvestedFundsAllowedWithdrawReasons = UnvestedFundsAllowedWithdrawReasons; - type BlockNumberProvider = System; - - const MAX_VESTING_SCHEDULES: u32 = 28; -} - impl pallet_utility::Config for Runtime { type RuntimeCall = RuntimeCall; type RuntimeEvent = RuntimeEvent; @@ -502,20 +478,6 @@ impl pallet_reversible_transfers::Config for Runtime { type VolumeFee = HighSecurityVolumeFee; } -parameter_types! { - pub const MaxProofs: u32 = 4096; -} - -impl pallet_merkle_airdrop::Config for Runtime { - type Vesting = Vesting; - type MaxProofs = MaxProofs; - type PalletId = MerkleAirdropPalletId; - type WeightInfo = pallet_merkle_airdrop::weights::SubstrateWeight; - type UnsignedClaimPriority = UnsignedClaimPriority; - type BlockNumberProvider = System; - type BlockNumberToBalance = ConvertInto; -} - parameter_types! { pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); pub const ProposalBond: Permill = Permill::from_percent(5); diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 447ac474..01b17135 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -22,7 +22,6 @@ use sp_version::RuntimeVersion; pub use frame_system::Call as SystemCall; pub use pallet_balances::Call as BalancesCall; -pub use pallet_merkle_airdrop; pub use pallet_reversible_transfers as ReversibleTransfersCall; pub use pallet_timestamp::Call as TimestampCall; @@ -216,9 +215,6 @@ mod runtime { #[runtime::pallet_index(7)] pub type MiningRewards = pallet_mining_rewards; - #[runtime::pallet_index(8)] - pub type Vesting = pallet_vesting; - #[runtime::pallet_index(9)] pub type Preimage = pallet_preimage; @@ -243,9 +239,6 @@ mod runtime { #[runtime::pallet_index(16)] pub type TechReferenda = pallet_referenda::Pallet; - #[runtime::pallet_index(17)] - pub type MerkleAirdrop = pallet_merkle_airdrop; - #[runtime::pallet_index(18)] pub type TreasuryPallet = pallet_treasury; diff --git a/runtime/tests/governance/mod.rs b/runtime/tests/governance/mod.rs index 13fa1876..1261a867 100644 --- a/runtime/tests/governance/mod.rs +++ b/runtime/tests/governance/mod.rs @@ -2,4 +2,3 @@ pub mod engine; pub mod logic; pub mod tech_collective; pub mod treasury; -pub mod vesting; diff --git a/runtime/tests/governance/vesting.rs b/runtime/tests/governance/vesting.rs deleted file mode 100644 index 02f7700f..00000000 --- a/runtime/tests/governance/vesting.rs +++ /dev/null @@ -1,619 +0,0 @@ -#[cfg(test)] -mod tests { - use crate::common::TestCommons; - use codec::Encode; - use frame_support::{ - assert_ok, - traits::{Bounded, Currency, VestingSchedule}, - }; - use pallet_conviction_voting::{AccountVote, Vote}; - use pallet_vesting::VestingInfo; - use quantus_runtime::{ - Balances, ConvictionVoting, Preimage, Referenda, RuntimeCall, RuntimeOrigin, System, - Utility, Vesting, DAYS, UNIT, - }; - use sp_runtime::{ - traits::{BlakeTwo256, Hash}, - MultiAddress, - }; - - /// Test case: Grant application through referendum with vesting payment schedule - /// - /// Scenario: - /// 1. Grant proposal submitted for referendum voting (treasury track) - /// 2. After positive voting, treasury spend is approved and executed - /// 3. Separate vesting implementation follows (two-stage governance pattern) - #[test] - fn test_grant_application_with_vesting_schedule() { - TestCommons::new_fast_governance_test_ext().execute_with(|| { - // Setup accounts - let proposer = TestCommons::account_id(1); - let beneficiary = TestCommons::account_id(2); - let voter1 = TestCommons::account_id(3); - let voter2 = TestCommons::account_id(4); - - // Give voters some balance for voting - Balances::make_free_balance_be(&voter1, 1000 * UNIT); - Balances::make_free_balance_be(&voter2, 1000 * UNIT); - Balances::make_free_balance_be(&proposer, 10000 * UNIT); // Proposer needs more funds for vesting transfer - - // Step 1: Create a treasury proposal for referendum - let grant_amount = 1000 * UNIT; - let vesting_period = 30; // Fast test: 30 blocks instead of 30 days - let per_block = grant_amount / vesting_period as u128; - - // Create the vesting info for later implementation - let vesting_info = VestingInfo::new(grant_amount, per_block, 1); - - // Treasury call for referendum approval - let treasury_call = RuntimeCall::TreasuryPallet(pallet_treasury::Call::spend { - asset_kind: Box::new(()), - amount: grant_amount, - beneficiary: Box::new(MultiAddress::Id(beneficiary.clone())), - valid_from: None, - }); - - // Note: Two-stage process - referendum approves principle, implementation follows - let _vesting_call = RuntimeCall::Vesting(pallet_vesting::Call::vested_transfer { - target: MultiAddress::Id(beneficiary.clone()), - schedule: vesting_info, - }); - - // Two-stage governance flow: referendum approves treasury spend principle - // Implementation details (like vesting schedule) handled in separate execution phase - let referendum_call = treasury_call; - - // Step 2: Submit preimage for the referendum call - let encoded_proposal = referendum_call.encode(); - let preimage_hash = BlakeTwo256::hash(&encoded_proposal); - - assert_ok!(Preimage::note_preimage( - RuntimeOrigin::signed(proposer.clone()), - encoded_proposal.clone() - )); - - // Step 3: Submit referendum for treasury spending (using treasury track) - let bounded_call = - Bounded::Lookup { hash: preimage_hash, len: encoded_proposal.len() as u32 }; - assert_ok!(Referenda::submit( - RuntimeOrigin::signed(proposer.clone()), - Box::new( - quantus_runtime::governance::pallet_custom_origins::Origin::SmallSpender.into() - ), - bounded_call, - frame_support::traits::schedule::DispatchTime::After(1) - )); - - // Step 4: Vote on referendum - let referendum_index = 0; - - // Vote YES with conviction - assert_ok!(ConvictionVoting::vote( - RuntimeOrigin::signed(voter1.clone()), - referendum_index, - AccountVote::Standard { - vote: Vote { - aye: true, - conviction: pallet_conviction_voting::Conviction::Locked1x, - }, - balance: 500 * UNIT, - } - )); - - assert_ok!(ConvictionVoting::vote( - RuntimeOrigin::signed(voter2.clone()), - referendum_index, - AccountVote::Standard { - vote: Vote { - aye: true, - conviction: pallet_conviction_voting::Conviction::Locked2x, - }, - balance: 300 * UNIT, - } - )); - - // Step 5: Wait for referendum to pass and execute - // Fast forward blocks for voting period + confirmation period (using fast governance - // timing) - let blocks_to_advance = 2 + 2 + 2 + 2 + 1; // prepare + decision + confirm + enactment + 1 - TestCommons::run_to_block(System::block_number() + blocks_to_advance); - - // The referendum should now be approved and treasury spend executed - - // Step 6: Implementation phase - after referendum approval, implement with vesting - // This demonstrates a realistic two-stage governance pattern: - // 1. Community votes on grant approval (principle) - // 2. Treasury council/governance implements with appropriate safeguards (vesting) - // This separation allows for community input on allocation while maintaining - // implementation flexibility - - println!("Referendum approved treasury spend. Now implementing vesting..."); - - // Implementation of the approved grant with vesting schedule - // This would typically be done by treasury council or automated system - assert_ok!(Vesting::force_vested_transfer( - RuntimeOrigin::root(), - MultiAddress::Id(proposer.clone()), - MultiAddress::Id(beneficiary.clone()), - vesting_info, - )); - - let initial_balance = Balances::free_balance(&beneficiary); - let locked_balance = Vesting::vesting_balance(&beneficiary).unwrap_or(0); - - println!("Beneficiary balance: {:?}", initial_balance); - println!("Locked balance: {:?}", locked_balance); - - assert!(locked_balance > 0, "Vesting should have been created"); - - // Step 7: Test vesting unlock over time - let initial_block = System::block_number(); - let initial_locked_amount = locked_balance; // Save the initial locked amount - - // Check initial state - println!("Initial balance: {:?}", initial_balance); - println!("Initial locked: {:?}", locked_balance); - println!("Initial block: {:?}", initial_block); - - // Fast forward a few blocks and check unlocking - TestCommons::run_to_block(initial_block + 10); - - // Check after some blocks - let mid_balance = Balances::free_balance(&beneficiary); - let mid_locked = Vesting::vesting_balance(&beneficiary).unwrap_or(0); - - println!("Mid balance: {:?}", mid_balance); - println!("Mid locked: {:?}", mid_locked); - - // The test should pass if vesting is working correctly - // mid_locked should be less than the initial locked amount - assert!( - mid_locked < initial_locked_amount, - "Some funds should be unlocked over time: initial_locked={:?}, mid_locked={:?}", - initial_locked_amount, - mid_locked - ); - - // Fast-forward to end of vesting period - TestCommons::run_to_block(initial_block + vesting_period + 1); - - // All funds should be unlocked - let final_balance = Balances::free_balance(&beneficiary); - let final_locked = Vesting::vesting_balance(&beneficiary).unwrap_or(0); - - println!("Final balance: {:?}", final_balance); - println!("Final locked: {:?}", final_locked); - - assert_eq!(final_locked, 0, "All funds should be unlocked"); - // Note: In the vesting pallet, when funds are fully vested, they become available - // but the balance might not increase if the initial transfer was part of the vesting - // The main assertion is that the vesting worked correctly (final_locked == 0) - println!("Vesting test completed successfully - funds are fully unlocked"); - }); - } - - /// Test case: Multi-milestone grant with multiple vesting schedules - /// - /// Scenario: Grant paid out in multiple tranches (milestones) - /// after achieving specific goals - #[test] - fn test_milestone_based_grant_with_multiple_vesting() { - TestCommons::new_fast_governance_test_ext().execute_with(|| { - let grantee = TestCommons::account_id(1); - let grantor = TestCommons::account_id(2); - - Balances::make_free_balance_be(&grantor, 10000 * UNIT); - - // Atomic milestone funding: all operations succeed or fail together - let milestone1_amount = 300 * UNIT; - let milestone2_amount = 400 * UNIT; - let milestone3_amount = 300 * UNIT; - - let milestone1_vesting = VestingInfo::new(milestone1_amount, milestone1_amount / 30, 1); - let milestone2_vesting = - VestingInfo::new(milestone2_amount, milestone2_amount / 60, 31); - - // Create batch call for all milestone operations - let _milestone_batch = RuntimeCall::Utility(pallet_utility::Call::batch_all { - calls: vec![ - // Milestone 1: Initial funding with short vesting - RuntimeCall::Vesting(pallet_vesting::Call::vested_transfer { - target: MultiAddress::Id(grantee.clone()), - schedule: milestone1_vesting, - }), - // Milestone 2: Mid-term funding with longer vesting - RuntimeCall::Vesting(pallet_vesting::Call::vested_transfer { - target: MultiAddress::Id(grantee.clone()), - schedule: milestone2_vesting, - }), - // Milestone 3: Immediate payment - RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death { - dest: MultiAddress::Id(grantee.clone()), - value: milestone3_amount, - }), - ], - }); - - // Execute all milestones atomically - let calls = vec![ - RuntimeCall::Vesting(pallet_vesting::Call::vested_transfer { - target: MultiAddress::Id(grantee.clone()), - schedule: milestone1_vesting, - }), - RuntimeCall::Vesting(pallet_vesting::Call::vested_transfer { - target: MultiAddress::Id(grantee.clone()), - schedule: milestone2_vesting, - }), - RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death { - dest: MultiAddress::Id(grantee.clone()), - value: milestone3_amount, - }), - ]; - assert_ok!(Utility::batch_all(RuntimeOrigin::signed(grantor.clone()), calls)); - - // Check that multiple vesting schedules are active - let vesting_schedules = Vesting::vesting(grantee.clone()).unwrap(); - assert_eq!(vesting_schedules.len(), 2, "Should have 2 active vesting schedules"); - - // Fast forward and verify unlocking patterns - TestCommons::run_to_block(40); // Past first vesting period - - let balance_after_first = Balances::free_balance(&grantee); - assert!( - balance_after_first >= milestone1_amount + milestone3_amount, - "First milestone and immediate payment should be available" - ); - - // Fast forward past second vesting period - TestCommons::run_to_block(100); - - let final_balance = Balances::free_balance(&grantee); - let expected_total = milestone1_amount + milestone2_amount + milestone3_amount; - assert!(final_balance >= expected_total, "All grant funds should be available"); - }); - } - - /// Test case: Realistic grant process with Tech Collective milestone evaluation - /// - /// Scenario: - /// 1. Initial referendum approves entire grant plan - /// 2. For each milestone: grantee delivers proof → Tech Collective votes via referenda → - /// payment released - /// 3. Tech Collective determines vesting schedule based on milestone quality/risk assessment - #[test] - fn test_progressive_milestone_referenda() { - TestCommons::new_fast_governance_test_ext().execute_with(|| { - let grantee = TestCommons::account_id(1); - let proposer = TestCommons::account_id(2); - let voter1 = TestCommons::account_id(3); - let voter2 = TestCommons::account_id(4); - - // Tech Collective members - technical experts who evaluate milestones - let tech_member1 = TestCommons::account_id(5); - let tech_member2 = TestCommons::account_id(6); - let tech_member3 = TestCommons::account_id(7); - let treasury_account = TestCommons::account_id(8); - - // Setup balances for governance participation - Balances::make_free_balance_be(&voter1, 2000 * UNIT); - Balances::make_free_balance_be(&voter2, 2000 * UNIT); - Balances::make_free_balance_be(&proposer, 15000 * UNIT); - Balances::make_free_balance_be(&tech_member1, 3000 * UNIT); - Balances::make_free_balance_be(&tech_member2, 3000 * UNIT); - Balances::make_free_balance_be(&tech_member3, 3000 * UNIT); - Balances::make_free_balance_be(&treasury_account, 10000 * UNIT); - - // Add Tech Collective members - assert_ok!(quantus_runtime::TechCollective::add_member( - RuntimeOrigin::root(), - MultiAddress::Id(tech_member1.clone()) - )); - assert_ok!(quantus_runtime::TechCollective::add_member( - RuntimeOrigin::root(), - MultiAddress::Id(tech_member2.clone()) - )); - assert_ok!(quantus_runtime::TechCollective::add_member( - RuntimeOrigin::root(), - MultiAddress::Id(tech_member3.clone()) - )); - - let milestone1_amount = 400 * UNIT; - let milestone2_amount = 500 * UNIT; - let milestone3_amount = 600 * UNIT; - let total_grant = milestone1_amount + milestone2_amount + milestone3_amount; - - // === STEP 1: Initial referendum approves entire grant plan === - println!("=== REFERENDUM: Grant Plan Approval ==="); - - let grant_approval_call = RuntimeCall::TreasuryPallet(pallet_treasury::Call::spend { - asset_kind: Box::new(()), - amount: total_grant, - beneficiary: Box::new(MultiAddress::Id(treasury_account.clone())), - valid_from: None, - }); - - let encoded_proposal = grant_approval_call.encode(); - let preimage_hash = BlakeTwo256::hash(&encoded_proposal); - - assert_ok!(Preimage::note_preimage( - RuntimeOrigin::signed(proposer.clone()), - encoded_proposal.clone() - )); - - let bounded_call = - Bounded::Lookup { hash: preimage_hash, len: encoded_proposal.len() as u32 }; - assert_ok!(Referenda::submit( - RuntimeOrigin::signed(proposer.clone()), - Box::new( - quantus_runtime::governance::pallet_custom_origins::Origin::SmallSpender.into() - ), - bounded_call, - frame_support::traits::schedule::DispatchTime::After(1) - )); - - // Community votes on the grant plan - assert_ok!(ConvictionVoting::vote( - RuntimeOrigin::signed(voter1.clone()), - 0, - AccountVote::Standard { - vote: Vote { - aye: true, - conviction: pallet_conviction_voting::Conviction::Locked1x, - }, - balance: 800 * UNIT, - } - )); - - assert_ok!(ConvictionVoting::vote( - RuntimeOrigin::signed(voter2.clone()), - 0, - AccountVote::Standard { - vote: Vote { - aye: true, - conviction: pallet_conviction_voting::Conviction::Locked2x, - }, - balance: 600 * UNIT, - } - )); - - let blocks_to_advance = 2 + 2 + 2 + 2 + 1; // Fast governance timing: prepare + decision + confirm + enactment + 1 - TestCommons::run_to_block(System::block_number() + blocks_to_advance); - - println!("✅ Grant plan approved by referendum!"); - - // === STEP 2: Tech Collective milestone evaluations via referenda === - - // === MILESTONE 1: Tech Collective Decision === - println!("=== MILESTONE 1: Tech Collective Decision ==="); - - println!("📋 Grantee delivers milestone 1: Basic protocol implementation"); - TestCommons::run_to_block(System::block_number() + 10); - - // Tech Collective evaluates and decides on milestone 1 payment - let milestone1_vesting = VestingInfo::new( - milestone1_amount, - milestone1_amount / 60, // Fast test: 60 blocks instead of 60 days - System::block_number() + 1, - ); - - println!("🔍 Tech Collective evaluates milestone 1..."); - - // Tech Collective implements milestone payment directly (as technical body with - // authority) In practice this could be through their own governance or automated - // after technical review - assert_ok!(Vesting::force_vested_transfer( - RuntimeOrigin::root(), /* Tech Collective has root-level authority for technical - * decisions */ - MultiAddress::Id(treasury_account.clone()), - MultiAddress::Id(grantee.clone()), - milestone1_vesting, - )); - - println!("✅ Tech Collective approved milestone 1 with 60-day vesting"); - - let milestone1_locked = Vesting::vesting_balance(&grantee).unwrap_or(0); - println!("Grantee locked (vesting): {:?}", milestone1_locked); - assert!(milestone1_locked > 0, "Milestone 1 should be vesting"); - - // === MILESTONE 2: Tech Collective Decision === - println!("=== MILESTONE 2: Tech Collective Decision ==="); - - TestCommons::run_to_block(System::block_number() + 20); - println!("📋 Grantee delivers milestone 2: Advanced features + benchmarks"); - - // Reduced vesting due to high quality - let milestone2_vesting = VestingInfo::new( - milestone2_amount, - milestone2_amount / 30, // Fast test: 30 blocks instead of 30 days - System::block_number() + 1, - ); - - println!("🔍 Tech Collective evaluates milestone 2 (high quality work)..."); - - // Tech Collective approves with reduced vesting due to excellent work - assert_ok!(Vesting::force_vested_transfer( - RuntimeOrigin::root(), - MultiAddress::Id(treasury_account.clone()), - MultiAddress::Id(grantee.clone()), - milestone2_vesting, - )); - - println!("✅ Tech Collective approved milestone 2 with reduced 30-day vesting"); - - // === MILESTONE 3: Final Tech Collective Decision === - println!("=== MILESTONE 3: Final Tech Collective Decision ==="); - - TestCommons::run_to_block(System::block_number() + 20); - println!( - "📋 Grantee delivers final milestone: Production deployment + maintenance plan" - ); - - println!("🔍 Tech Collective evaluates final milestone (project completion)..."); - - // Immediate payment for completed project - no vesting needed - assert_ok!(Balances::transfer_allow_death( - RuntimeOrigin::signed(treasury_account.clone()), - MultiAddress::Id(grantee.clone()), - milestone3_amount, - )); - - println!("✅ Tech Collective approved final milestone with immediate payment"); - - // === Verify Tech Collective governance worked === - let final_balance = Balances::free_balance(&grantee); - let remaining_locked = Vesting::vesting_balance(&grantee).unwrap_or(0); - - println!("Final grantee balance: {:?}", final_balance); - println!("Remaining locked: {:?}", remaining_locked); - - let vesting_schedules = Vesting::vesting(grantee.clone()).unwrap_or_default(); - assert!( - !vesting_schedules.is_empty(), - "Should have active vesting schedules from Tech Collective decisions" - ); - - assert!( - final_balance >= milestone3_amount, - "Tech Collective milestone process should have provided controlled funding" - ); - - println!("🎉 Tech Collective governance process completed successfully!"); - println!(" - One community referendum approved the overall grant plan"); - println!(" - Tech Collective evaluated each milestone with technical expertise"); - println!(" - Vesting schedules determined by technical quality assessment:"); - println!(" * Milestone 1: 60-day vesting (conservative, early stage)"); - println!(" * Milestone 2: 30-day vesting (high confidence, quality work)"); - println!(" * Milestone 3: Immediate payment (project completed successfully)"); - }); - } - - /// Test case: Treasury proposal with automatic vesting integration - /// - /// Scenario: Treasury spend and vesting creation executed atomically - /// through batch calls for integrated fund management - #[test] - fn test_treasury_auto_vesting_integration() { - TestCommons::new_fast_governance_test_ext().execute_with(|| { - let beneficiary = TestCommons::account_id(1); - let amount = 1000 * UNIT; - - // Create atomic treasury spend + vesting creation through batch calls - let vesting_info = VestingInfo::new(amount, amount / (30 * DAYS) as u128, 1); - - let _treasury_vesting_batch = RuntimeCall::Utility(pallet_utility::Call::batch_all { - calls: vec![ - // Treasury spend - RuntimeCall::TreasuryPallet(pallet_treasury::Call::spend { - asset_kind: Box::new(()), - amount, - beneficiary: Box::new(MultiAddress::Id(beneficiary.clone())), - valid_from: None, - }), - // Vesting creation as part of same atomic transaction - RuntimeCall::Vesting(pallet_vesting::Call::force_vested_transfer { - source: MultiAddress::Id(beneficiary.clone()), /* Simplified - in - * practice treasury - * account */ - target: MultiAddress::Id(beneficiary.clone()), - schedule: vesting_info, - }), - ], - }); - - // Execute atomic treasury spend + vesting batch - let calls = vec![ - RuntimeCall::TreasuryPallet(pallet_treasury::Call::spend { - asset_kind: Box::new(()), - amount, - beneficiary: Box::new(MultiAddress::Id(beneficiary.clone())), - valid_from: None, - }), - RuntimeCall::Vesting(pallet_vesting::Call::force_vested_transfer { - source: MultiAddress::Id(beneficiary.clone()), - target: MultiAddress::Id(beneficiary.clone()), - schedule: vesting_info, - }), - ]; - assert_ok!(Utility::batch_all(RuntimeOrigin::root(), calls)); - - // Verify the integration worked - let locked_amount = Vesting::vesting_balance(&beneficiary).unwrap_or(0); - assert!(locked_amount > 0, "Vesting should be active"); - }); - } - - /// Test case: Emergency vesting operations with batch calls - /// - /// Scenario: Emergency handling of vesting schedules through - /// atomic batch operations for intervention scenarios - #[test] - fn test_emergency_vesting_cancellation() { - TestCommons::new_fast_governance_test_ext().execute_with(|| { - let grantee = TestCommons::account_id(1); - let grantor = TestCommons::account_id(2); - - Balances::make_free_balance_be(&grantor, 2000 * UNIT); - - // Create vesting schedule with atomic batch call setup - let total_amount = 1000 * UNIT; - let vesting_info = VestingInfo::new(total_amount, total_amount / 100, 1); - - // Example of comprehensive grant setup through batch operations - let _grant_batch = RuntimeCall::Utility(pallet_utility::Call::batch_all { - calls: vec![ - // Initial grant setup - RuntimeCall::Vesting(pallet_vesting::Call::vested_transfer { - target: MultiAddress::Id(grantee.clone()), - schedule: vesting_info, - }), - // Could include additional setup calls (metadata, tracking, etc.) - ], - }); - - let calls = vec![RuntimeCall::Vesting(pallet_vesting::Call::vested_transfer { - target: MultiAddress::Id(grantee.clone()), - schedule: vesting_info, - })]; - assert_ok!(Utility::batch_all(RuntimeOrigin::signed(grantor.clone()), calls)); - - // Let some time pass and some funds unlock - TestCommons::run_to_block(50); - - let balance_before_cancellation = Balances::free_balance(&grantee); - let locked_before = Vesting::vesting_balance(&grantee).unwrap_or(0); - - assert!(locked_before > 0, "Should still have locked funds"); - - // Emergency intervention through atomic batch operations - let _emergency_batch = RuntimeCall::Utility(pallet_utility::Call::batch_all { - calls: vec![ - // Emergency action: schedule management operations - RuntimeCall::Vesting(pallet_vesting::Call::merge_schedules { - schedule1_index: 0, - schedule2_index: 0, - }), - // Could include additional emergency measures like fund recovery or - // notifications - ], - }); - - // Execute emergency intervention if vesting exists - if !Vesting::vesting(grantee.clone()).unwrap().is_empty() { - let calls = vec![RuntimeCall::Vesting(pallet_vesting::Call::merge_schedules { - schedule1_index: 0, - schedule2_index: 0, - })]; - assert_ok!(Utility::batch_all(RuntimeOrigin::signed(grantee.clone()), calls)); - } - - let balance_after = Balances::free_balance(&grantee); - - // Verify that emergency operations maintained system integrity - // (In practice, this would involve more sophisticated intervention mechanisms) - assert!( - balance_after >= balance_before_cancellation, - "Emergency handling should maintain or improve user's position" - ); - }); - } -} From cee6888762ee552503e5a42fb5b5e22850d2abf7 Mon Sep 17 00:00:00 2001 From: illuzen Date: Fri, 23 Jan 2026 16:45:27 +0800 Subject: [PATCH 20/27] Enable wormhole addresses in genesis (#359) * generate transfer proofs for genesis endowment * fmt * fix balances tests * fmt --- pallets/balances/src/lib.rs | 10 ++ pallets/balances/src/tests/mod.rs | 2 + .../src/tests/transfer_counter_tests.rs | 104 +++++++++--------- pallets/mining-rewards/src/mock.rs | 1 + .../reversible-transfers/src/tests/mock.rs | 5 + pallets/wormhole/src/mock.rs | 1 + runtime/src/configs/mod.rs | 1 + 7 files changed, 74 insertions(+), 50 deletions(-) diff --git a/pallets/balances/src/lib.rs b/pallets/balances/src/lib.rs index a0ee17d3..164515a6 100644 --- a/pallets/balances/src/lib.rs +++ b/pallets/balances/src/lib.rs @@ -337,6 +337,13 @@ pub mod pallet { Self::AccountId, Self::Balance, >; + + /// Account ID used as the "from" account when creating transfer proofs for minted tokens + /// (e.g., genesis balances, mining rewards). This should be a well-known address that + /// represents "minted from nothing". + #[pallet::constant] + #[pallet::no_default] + type MintingAccount: Get; } /// The in-code storage version. @@ -572,10 +579,13 @@ pub mod pallet { "duplicate balances in genesis." ); + let mint_account = T::MintingAccount::get(); for &(ref who, free) in self.balances.iter() { frame_system::Pallet::::inc_providers(who); assert!(T::AccountStore::insert(who, AccountData { free, ..Default::default() }) .is_ok()); + // Create transfer proof for genesis balance (from minting account) + Pallet::::do_store_transfer_proof(&mint_account, who, free); } } } diff --git a/pallets/balances/src/tests/mod.rs b/pallets/balances/src/tests/mod.rs index cc469066..bb33433d 100644 --- a/pallets/balances/src/tests/mod.rs +++ b/pallets/balances/src/tests/mod.rs @@ -112,6 +112,7 @@ impl pallet_transaction_payment::Config for Test { parameter_types! { pub FooReason: TestId = TestId::Foo; + pub MintingAccount: AccountId = AccountId::new([1u8; 32]); } #[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] @@ -126,6 +127,7 @@ impl Config for Test { type RuntimeFreezeReason = TestId; type FreezeIdentifier = TestId; type MaxFreezes = VariantCountOf; + type MintingAccount = MintingAccount; } #[derive(Clone)] diff --git a/pallets/balances/src/tests/transfer_counter_tests.rs b/pallets/balances/src/tests/transfer_counter_tests.rs index 92cb360e..57fb1e7b 100644 --- a/pallets/balances/src/tests/transfer_counter_tests.rs +++ b/pallets/balances/src/tests/transfer_counter_tests.rs @@ -34,14 +34,19 @@ fn charlie() -> AccountId { account_id(3) } +/// When monied(true), genesis creates 5 accounts with balances. +/// However, account_id(1) == MintingAccount ([1u8; 32]), so that's a self-transfer +/// which doesn't create a proof. Thus we get 4 transfer proofs. +const GENESIS_TRANSFER_COUNT: u64 = 4; + #[test] -fn transfer_counter_starts_at_zero() { +fn transfer_counter_starts_at_genesis_count() { ExtBuilder::default() .existential_deposit(1) .monied(true) .build_and_execute_with(|| { - // Transfer counter should start at 0 - assert_eq!(Balances::transfer_count(), 0); + // Transfer counter should start at GENESIS_TRANSFER_COUNT (one per endowed account) + assert_eq!(Balances::transfer_count(), GENESIS_TRANSFER_COUNT); }); } @@ -51,20 +56,19 @@ fn transfer_allow_death_increments_counter() { .existential_deposit(1) .monied(true) .build_and_execute_with(|| { - // Initial counter should be 0 - assert_eq!(Balances::transfer_count(), 0); + let initial_count = Balances::transfer_count(); // Perform a transfer assert_ok!(Balances::transfer_allow_death(Some(alice()).into(), bob(), 5)); - // Counter should increment to 1 - assert_eq!(Balances::transfer_count(), 1); + // Counter should increment by 1 + assert_eq!(Balances::transfer_count(), initial_count + 1); // Perform another transfer assert_ok!(Balances::transfer_allow_death(Some(bob()).into(), charlie(), 3)); - // Counter should increment to 2 - assert_eq!(Balances::transfer_count(), 2); + // Counter should increment by 2 total + assert_eq!(Balances::transfer_count(), initial_count + 2); }); } @@ -74,14 +78,13 @@ fn transfer_keep_alive_increments_counter() { .existential_deposit(1) .monied(true) .build_and_execute_with(|| { - // Initial counter should be 0 - assert_eq!(Balances::transfer_count(), 0); + let initial_count = Balances::transfer_count(); // Perform a transfer_keep_alive assert_ok!(Balances::transfer_keep_alive(Some(alice()).into(), bob(), 5)); - // Counter should increment to 1 - assert_eq!(Balances::transfer_count(), 1); + // Counter should increment by 1 + assert_eq!(Balances::transfer_count(), initial_count + 1); }); } @@ -91,14 +94,13 @@ fn force_transfer_increments_counter() { .existential_deposit(1) .monied(true) .build_and_execute_with(|| { - // Initial counter should be 0 - assert_eq!(Balances::transfer_count(), 0); + let initial_count = Balances::transfer_count(); // Perform a force transfer assert_ok!(Balances::force_transfer(RuntimeOrigin::root(), alice(), bob(), 5)); - // Counter should increment to 1 - assert_eq!(Balances::transfer_count(), 1); + // Counter should increment by 1 + assert_eq!(Balances::transfer_count(), initial_count + 1); }); } @@ -108,14 +110,13 @@ fn transfer_all_increments_counter() { .existential_deposit(1) .monied(true) .build_and_execute_with(|| { - // Initial counter should be 0 - assert_eq!(Balances::transfer_count(), 0); + let initial_count = Balances::transfer_count(); // Perform a transfer_all assert_ok!(Balances::transfer_all(Some(alice()).into(), bob(), false)); - // Counter should increment to 1 - assert_eq!(Balances::transfer_count(), 1); + // Counter should increment by 1 + assert_eq!(Balances::transfer_count(), initial_count + 1); }); } @@ -125,14 +126,13 @@ fn self_transfer_does_not_increment_counter() { .existential_deposit(1) .monied(true) .build_and_execute_with(|| { - // Initial counter should be 0 - assert_eq!(Balances::transfer_count(), 0); + let initial_count = Balances::transfer_count(); // Self transfer should not increment counter assert_ok!(Balances::transfer_allow_death(Some(alice()).into(), alice(), 5)); - // Counter should remain 0 since it's a self-transfer - assert_eq!(Balances::transfer_count(), 0); + // Counter should remain unchanged since it's a self-transfer + assert_eq!(Balances::transfer_count(), initial_count); }); } @@ -142,11 +142,14 @@ fn transfer_proof_storage_is_created() { .existential_deposit(1) .monied(true) .build_and_execute_with(|| { + let current_count = Balances::transfer_count(); + // Perform a transfer assert_ok!(Balances::transfer_allow_death(Some(alice()).into(), bob(), 5)); - // Check that transfer proof was stored with correct key - let key = (0u64, alice(), bob(), 5); + // Check that transfer proof was stored with correct key (using current count as the + // index) + let key = (current_count, alice(), bob(), 5u128); assert!(TransferProof::::contains_key(&key)); }); } @@ -157,28 +160,30 @@ fn multiple_transfers_create_sequential_proofs() { .existential_deposit(1) .monied(true) .build_and_execute_with(|| { + let initial_count = Balances::transfer_count(); + // First transfer assert_ok!(Balances::transfer_allow_death(Some(alice()).into(), bob(), 5)); - assert_eq!(Balances::transfer_count(), 1); + assert_eq!(Balances::transfer_count(), initial_count + 1); // Check first proof exists - let key1 = (0u64, alice(), bob(), 5u128); + let key1 = (initial_count, alice(), bob(), 5u128); assert!(TransferProof::::contains_key(&key1)); // Second transfer assert_ok!(Balances::transfer_allow_death(Some(bob()).into(), charlie(), 3)); - assert_eq!(Balances::transfer_count(), 2); + assert_eq!(Balances::transfer_count(), initial_count + 2); // Check second proof exists - let key2 = (1u64, bob(), charlie(), 3u128); + let key2 = (initial_count + 1, bob(), charlie(), 3u128); assert!(TransferProof::::contains_key(&key2)); // Third transfer with different amount assert_ok!(Balances::transfer_allow_death(Some(alice()).into(), charlie(), 1)); - assert_eq!(Balances::transfer_count(), 3); + assert_eq!(Balances::transfer_count(), initial_count + 3); // Check third proof exists - let key3 = (2u64, alice(), charlie(), 1u128); + let key3 = (initial_count + 2, alice(), charlie(), 1u128); assert!(TransferProof::::contains_key(&key3)); }); } @@ -189,8 +194,7 @@ fn failed_transfers_do_not_increment_counter() { .existential_deposit(1) .monied(true) .build_and_execute_with(|| { - // Initial counter should be 0 - assert_eq!(Balances::transfer_count(), 0); + let initial_count = Balances::transfer_count(); // Attempt transfer with insufficient funds assert_noop!( @@ -198,8 +202,8 @@ fn failed_transfers_do_not_increment_counter() { Arithmetic(Underflow) ); - // Counter should remain 0 since transfer failed - assert_eq!(Balances::transfer_count(), 0); + // Counter should remain unchanged since transfer failed + assert_eq!(Balances::transfer_count(), initial_count); }); } @@ -268,19 +272,21 @@ fn transfer_counter_persists_across_blocks() { .existential_deposit(1) .monied(true) .build_and_execute_with(|| { + let initial_count = Balances::transfer_count(); + // Perform transfer in block 1 assert_ok!(Balances::transfer_allow_death(Some(alice()).into(), bob(), 5)); - assert_eq!(Balances::transfer_count(), 1); + assert_eq!(Balances::transfer_count(), initial_count + 1); // Move to block 2 System::set_block_number(2); // Counter should persist - assert_eq!(Balances::transfer_count(), 1); + assert_eq!(Balances::transfer_count(), initial_count + 1); // Perform another transfer in block 2 assert_ok!(Balances::transfer_allow_death(Some(bob()).into(), charlie(), 3)); - assert_eq!(Balances::transfer_count(), 2); + assert_eq!(Balances::transfer_count(), initial_count + 2); }); } @@ -290,17 +296,16 @@ fn zero_value_transfers_increment_counter() { .existential_deposit(1) .monied(true) .build_and_execute_with(|| { - // Initial counter should be 0 - assert_eq!(Balances::transfer_count(), 0); + let initial_count = Balances::transfer_count(); // Perform a zero-value transfer assert_ok!(Balances::transfer_allow_death(Some(alice()).into(), bob(), 0)); // Counter should increment even for zero-value transfers - assert_eq!(Balances::transfer_count(), 1); + assert_eq!(Balances::transfer_count(), initial_count + 1); // Transfer proof should be created - let key = (0u64, alice(), bob(), 0u128); + let key = (initial_count, alice(), bob(), 0u128); assert!(TransferProof::::contains_key(&key)); }); } @@ -311,26 +316,25 @@ fn different_transfer_types_all_increment_counter() { .existential_deposit(1) .monied(true) .build_and_execute_with(|| { - // Initial counter should be 0 - assert_eq!(Balances::transfer_count(), 0); + let initial_count = Balances::transfer_count(); // transfer_allow_death assert_ok!(Balances::transfer_allow_death(Some(alice()).into(), bob(), 1)); - assert_eq!(Balances::transfer_count(), 1); + assert_eq!(Balances::transfer_count(), initial_count + 1); // transfer_keep_alive assert_ok!(Balances::transfer_keep_alive(Some(alice()).into(), charlie(), 1)); - assert_eq!(Balances::transfer_count(), 2); + assert_eq!(Balances::transfer_count(), initial_count + 2); // force_transfer assert_ok!(Balances::force_transfer(RuntimeOrigin::root(), bob(), charlie(), 1)); - assert_eq!(Balances::transfer_count(), 3); + assert_eq!(Balances::transfer_count(), initial_count + 3); // transfer_all (transfer remaining balance) let remaining = Balances::free_balance(alice()); if remaining > 1 { assert_ok!(Balances::transfer_all(Some(alice()).into(), bob(), false)); - assert_eq!(Balances::transfer_count(), 4); + assert_eq!(Balances::transfer_count(), initial_count + 4); } }); } diff --git a/pallets/mining-rewards/src/mock.rs b/pallets/mining-rewards/src/mock.rs index 03ce2a4c..8592fc59 100644 --- a/pallets/mining-rewards/src/mock.rs +++ b/pallets/mining-rewards/src/mock.rs @@ -80,6 +80,7 @@ impl pallet_balances::Config for Test { type MaxReserves = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type MintingAccount = MintingAccount; } parameter_types! { diff --git a/pallets/reversible-transfers/src/tests/mock.rs b/pallets/reversible-transfers/src/tests/mock.rs index 031955ab..e1d829b7 100644 --- a/pallets/reversible-transfers/src/tests/mock.rs +++ b/pallets/reversible-transfers/src/tests/mock.rs @@ -136,6 +136,10 @@ impl frame_system::Config for Test { type AccountData = pallet_balances::AccountData; } +parameter_types! { + pub MintingAccount: AccountId = AccountId::new([1u8; 32]); +} + #[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] impl pallet_balances::Config for Test { type Balance = Balance; @@ -145,6 +149,7 @@ impl pallet_balances::Config for Test { type WeightInfo = (); type RuntimeHoldReason = RuntimeHoldReason; type MaxFreezes = MaxReversibleTransfers; + type MintingAccount = MintingAccount; } // In memory storage diff --git a/pallets/wormhole/src/mock.rs b/pallets/wormhole/src/mock.rs index 4c764f26..574ac336 100644 --- a/pallets/wormhole/src/mock.rs +++ b/pallets/wormhole/src/mock.rs @@ -89,6 +89,7 @@ impl pallet_balances::Config for Test { type MaxReserves = (); type MaxFreezes = (); type DoneSlashHandler = (); + type MintingAccount = MintingAccount; } // --- PALLET WORMHOLE --- diff --git a/runtime/src/configs/mod.rs b/runtime/src/configs/mod.rs index 20ab50f4..eac5366e 100644 --- a/runtime/src/configs/mod.rs +++ b/runtime/src/configs/mod.rs @@ -190,6 +190,7 @@ impl pallet_balances::Config for Runtime { type MaxReserves = (); type MaxFreezes = VariantCountOf; type DoneSlashHandler = (); + type MintingAccount = MintingAccount; } parameter_types! { From cbe0b9835dcb79f76c6753e1a1cbb03be185c0aa Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Mon, 26 Jan 2026 18:30:04 +0800 Subject: [PATCH 21/27] add recover funds call (#361) * add recover funds call * add unit tests * fix up remaining tests * cargo fmt * fix benchmarks, update weights * fix merge error --- .../reversible-transfers/src/benchmarking.rs | 39 ++- pallets/reversible-transfers/src/lib.rs | 52 ++-- .../src/tests/test_high_security_account.rs | 64 ++++- pallets/reversible-transfers/src/weights.rs | 239 +++++++++++------- .../transactions/reversible_integration.rs | 164 +++--------- 5 files changed, 303 insertions(+), 255 deletions(-) diff --git a/pallets/reversible-transfers/src/benchmarking.rs b/pallets/reversible-transfers/src/benchmarking.rs index 1d0b3a3c..ae69f5b0 100644 --- a/pallets/reversible-transfers/src/benchmarking.rs +++ b/pallets/reversible-transfers/src/benchmarking.rs @@ -172,14 +172,9 @@ mod benchmarks { #[benchmark] fn execute_transfer() -> Result<(), BenchmarkError> { let owner: T::AccountId = whitelisted_caller(); - fund_account::(&owner, BalanceOf::::from(10000u128)); // Fund owner + fund_account::(&owner, BalanceOf::::from(10000u128)); let recipient: T::AccountId = benchmark_account("recipient", 0, SEED); - // Fund recipient with minimum_balance * 100 to match assertion expectation - let initial_balance = as frame_support::traits::Currency< - T::AccountId, - >>::minimum_balance() * - 100_u128.into(); - fund_account::(&recipient, initial_balance); + fund_account::(&recipient, BalanceOf::::from(100u128)); let interceptor: T::AccountId = benchmark_account("interceptor", 1, SEED); let transfer_amount = 100u128; @@ -210,21 +205,25 @@ mod benchmarks { #[extrinsic_call] _(execute_origin, tx_id); - // Check state cleaned up assert_eq!(AccountPendingIndex::::get(&owner), 0); assert!(!PendingTransfers::::contains_key(tx_id)); - // Check side effect of inner call (balance transfer) - let initial_balance = as frame_support::traits::Currency< - T::AccountId, - >>::minimum_balance() * - 100_u128.into(); - let expected_balance = initial_balance.saturating_add(transfer_amount.into()); - assert_eq!( - as frame_support::traits::Currency>::free_balance( - &recipient - ), - expected_balance - ); + + Ok(()) + } + + #[benchmark] + fn recover_funds() -> Result<(), BenchmarkError> { + let account: T::AccountId = whitelisted_caller(); + let guardian: T::AccountId = benchmark_account("guardian", 0, SEED); + + fund_account::(&account, BalanceOf::::from(10000u128)); + fund_account::(&guardian, BalanceOf::::from(10000u128)); + + let delay = T::DefaultDelay::get(); + setup_high_security_account::(account.clone(), delay, guardian.clone()); + + #[extrinsic_call] + _(RawOrigin::Signed(guardian.clone()), account.clone()); Ok(()) } diff --git a/pallets/reversible-transfers/src/lib.rs b/pallets/reversible-transfers/src/lib.rs index 31d52084..a01dfb4c 100644 --- a/pallets/reversible-transfers/src/lib.rs +++ b/pallets/reversible-transfers/src/lib.rs @@ -41,10 +41,6 @@ use sp_runtime::traits::StaticLookup; pub type BlockNumberOrTimestampOf = BlockNumberOrTimestamp, ::Moment>; -/// Type alias for the Recovery pallet's expected block number type -pub type RecoveryBlockNumberOf = - <::BlockNumberProvider as sp_runtime::traits::BlockNumberProvider>::BlockNumber; - /// High security account details #[derive(Encode, Decode, MaxEncodedLen, Clone, Default, TypeInfo, Debug, PartialEq, Eq)] pub struct HighSecurityAccountData { @@ -123,7 +119,6 @@ pub mod pallet { > + pallet_balances::Config::RuntimeHoldReason> + pallet_assets::Config::Balance> + pallet_assets_holder::Config::RuntimeHoldReason> - + pallet_recovery::Config { /// Scheduler for the runtime. We use the Named scheduler for cancellability. type Scheduler: ScheduleNamed< @@ -284,11 +279,11 @@ pub mod pallet { execute_at: DispatchTime, T::Moment>, }, /// A scheduled transaction has been successfully cancelled by the owner. - /// [who, tx_id] TransactionCancelled { who: T::AccountId, tx_id: T::Hash }, /// A scheduled transaction was executed by the scheduler. - /// [tx_id, dispatch_result] TransactionExecuted { tx_id: T::Hash, result: DispatchResultWithPostInfo }, + /// Funds were recovered from a high security account by its guardian. + FundsRecovered { account: T::AccountId, guardian: T::AccountId }, } #[pallet::error] @@ -350,7 +345,7 @@ pub mod pallet { delay: BlockNumberOrTimestampOf, interceptor: T::AccountId, ) -> DispatchResult { - let who = ensure_signed(origin.clone())?; + let who = ensure_signed(origin)?; ensure!(interceptor != who.clone(), Error::::InterceptorCannotBeSelf); ensure!( @@ -360,17 +355,6 @@ pub mod pallet { Self::validate_delay(&delay)?; - // Set up zero delay recovery for interceptor - // The interceptor then simply needs to claim the recovery in order to be able - // to make calls on behalf of the high security account. - let recovery_delay_blocks: RecoveryBlockNumberOf = Zero::zero(); - pallet_recovery::Pallet::::create_recovery( - origin, - alloc::vec![interceptor.clone()], - One::one(), - recovery_delay_blocks, - )?; - let high_security_account_data = HighSecurityAccountData { interceptor: interceptor.clone(), delay }; @@ -497,6 +481,36 @@ pub mod pallet { Self::do_schedule_transfer_inner(who.clone(), dest, who, amount, delay, Some(asset_id)) } + + /// Allows the guardian (interceptor) to recover all funds from a high security + /// account by transferring the entire balance to themselves. + /// + /// This is an emergency function for when the high security account may be compromised. + #[pallet::call_index(7)] + #[pallet::weight(::WeightInfo::recover_funds())] + pub fn recover_funds( + origin: OriginFor, + account: T::AccountId, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + + let high_security_account_data = HighSecurityAccounts::::get(&account) + .ok_or(Error::::AccountNotHighSecurity)?; + + ensure!(who == high_security_account_data.interceptor, Error::::InvalidReverser); + + let call: RuntimeCallOf = pallet_balances::Call::::transfer_all { + dest: T::Lookup::unlookup(who.clone()), + keep_alive: false, + } + .into(); + + let result = call.dispatch(frame_system::RawOrigin::Signed(account.clone()).into()); + + Self::deposit_event(Event::FundsRecovered { account, guardian: who }); + + result + } } #[pallet::hooks] diff --git a/pallets/reversible-transfers/src/tests/test_high_security_account.rs b/pallets/reversible-transfers/src/tests/test_high_security_account.rs index 9ec8dec2..c9c8aa73 100644 --- a/pallets/reversible-transfers/src/tests/test_high_security_account.rs +++ b/pallets/reversible-transfers/src/tests/test_high_security_account.rs @@ -1,12 +1,66 @@ -use crate::tests::{ - mock::*, - test_reversible_transfers::{calculate_tx_id, transfer_call}, +use crate::{ + tests::{ + mock::*, + test_reversible_transfers::{calculate_tx_id, transfer_call}, + }, + Event, }; -use frame_support::assert_ok; +use frame_support::{assert_err, assert_ok}; use pallet_balances::TotalIssuance; // NOTE: Many of the high security / reversibility behaviors are enforced via SignedExtension or -// external pallets (Recovery/Proxy). They are covered by integration tests in runtime. +// external pallets (Proxy). They are covered by integration tests in runtime. + +#[test] +fn guardian_can_recover_all_funds_from_high_security_account() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let hs_user = alice(); + let guardian = bob(); + + let initial_hs_balance = Balances::free_balance(&hs_user); + let initial_guardian_balance = Balances::free_balance(&guardian); + + assert_ok!(ReversibleTransfers::recover_funds( + RuntimeOrigin::signed(guardian.clone()), + hs_user.clone() + )); + + assert_eq!(Balances::free_balance(&hs_user), 0); + assert_eq!( + Balances::free_balance(&guardian), + initial_guardian_balance + initial_hs_balance + ); + + System::assert_has_event(Event::FundsRecovered { account: hs_user, guardian }.into()); + }); +} + +#[test] +fn recover_funds_fails_if_caller_is_not_guardian() { + new_test_ext().execute_with(|| { + let hs_user = alice(); + let not_guardian = charlie(); + + assert_err!( + ReversibleTransfers::recover_funds(RuntimeOrigin::signed(not_guardian), hs_user), + crate::Error::::InvalidReverser + ); + }); +} + +#[test] +fn recover_funds_fails_for_non_high_security_account() { + new_test_ext().execute_with(|| { + let regular_user = charlie(); + let attacker = dave(); + + assert_err!( + ReversibleTransfers::recover_funds(RuntimeOrigin::signed(attacker), regular_user), + crate::Error::::AccountNotHighSecurity + ); + }); +} #[test] fn guardian_can_cancel_reversible_transactions_for_hs_account() { diff --git a/pallets/reversible-transfers/src/weights.rs b/pallets/reversible-transfers/src/weights.rs index 9fbe3a66..b5c0f5e2 100644 --- a/pallets/reversible-transfers/src/weights.rs +++ b/pallets/reversible-transfers/src/weights.rs @@ -18,27 +18,23 @@ //! Autogenerated weights for `pallet_reversible_transfers` //! -//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 47.2.0 -//! DATE: 2025-06-24, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 49.1.0 +//! DATE: 2026-01-26, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `MacBook-Pro-4.local`, CPU: `` -//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` +//! HOSTNAME: `arunachala.local`, CPU: `` +//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` // Executed Command: -// frame-omni-bencher -// v1 +// ./target/release/quantus-node // benchmark // pallet -// --runtime -// ./target/release/wbuild/quantus-runtime/quantus_runtime.wasm -// --pallet -// pallet-reversible-transfers -// --extrinsic -// * -// --template -// ./.maintain/frame-weight-template.hbs -// --output -// ./pallets/reversible-transfers/src/weights.rs +// --chain=dev +// --pallet=pallet_reversible_transfers +// --extrinsic=* +// --steps=50 +// --repeat=20 +// --output=pallets/reversible-transfers/src/weights.rs +// --template=.maintain/frame-weight-template.hbs #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] @@ -55,28 +51,35 @@ pub trait WeightInfo { fn schedule_transfer() -> Weight; fn cancel() -> Weight; fn execute_transfer() -> Weight; + fn recover_funds() -> Weight; } /// Weights for `pallet_reversible_transfers` using the Substrate node and recommended hardware. pub struct SubstrateWeight(PhantomData); impl WeightInfo for SubstrateWeight { - /// Storage: `ReversibleTransfers::ReversibleAccounts` (r:1 w:1) - /// Proof: `ReversibleTransfers::ReversibleAccounts` (`max_values`: None, `max_size`: Some(91), added: 2566, mode: `MaxEncodedLen`) + /// Storage: `ReversibleTransfers::HighSecurityAccounts` (r:1 w:1) + /// Proof: `ReversibleTransfers::HighSecurityAccounts` (`max_values`: None, `max_size`: Some(89), added: 2564, mode: `MaxEncodedLen`) + /// Storage: `ReversibleTransfers::InterceptorIndex` (r:1 w:1) + /// Proof: `ReversibleTransfers::InterceptorIndex` (`max_values`: None, `max_size`: Some(1073), added: 3548, mode: `MaxEncodedLen`) fn set_high_security() -> Weight { // Proof Size summary in bytes: - // Measured: `76` - // Estimated: `3556` - // Minimum execution time: 9_000_000 picoseconds. - Weight::from_parts(9_000_000, 3556) - .saturating_add(T::DbWeight::get().reads(1_u64)) - .saturating_add(T::DbWeight::get().writes(1_u64)) + // Measured: `192` + // Estimated: `4538` + // Minimum execution time: 78_000_000 picoseconds. + Weight::from_parts(80_000_000, 4538) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) } - /// Storage: `ReversibleTransfers::ReversibleAccounts` (r:1 w:0) - /// Proof: `ReversibleTransfers::ReversibleAccounts` (`max_values`: None, `max_size`: Some(91), added: 2566, mode: `MaxEncodedLen`) + /// Storage: `ReversibleTransfers::HighSecurityAccounts` (r:1 w:0) + /// Proof: `ReversibleTransfers::HighSecurityAccounts` (`max_values`: None, `max_size`: Some(89), added: 2564, mode: `MaxEncodedLen`) + /// Storage: `ReversibleTransfers::GlobalNonce` (r:1 w:1) + /// Proof: `ReversibleTransfers::GlobalNonce` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `ReversibleTransfers::AccountPendingIndex` (r:1 w:1) /// Proof: `ReversibleTransfers::AccountPendingIndex` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) - /// Storage: `ReversibleTransfers::PendingTransfers` (r:1 w:1) - /// Proof: `ReversibleTransfers::PendingTransfers` (`max_values`: None, `max_size`: Some(231), added: 2706, mode: `MaxEncodedLen`) + /// Storage: `ReversibleTransfers::PendingTransfersBySender` (r:1 w:1) + /// Proof: `ReversibleTransfers::PendingTransfersBySender` (`max_values`: None, `max_size`: Some(369), added: 2844, mode: `MaxEncodedLen`) + /// Storage: `ReversibleTransfers::PendingTransfersByRecipient` (r:1 w:1) + /// Proof: `ReversibleTransfers::PendingTransfersByRecipient` (`max_values`: None, `max_size`: Some(369), added: 2844, mode: `MaxEncodedLen`) /// Storage: `Scheduler::Lookup` (r:1 w:1) /// Proof: `Scheduler::Lookup` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) /// Storage: `Timestamp::Now` (r:1 w:0) @@ -85,78 +88,113 @@ impl WeightInfo for SubstrateWeight { /// Proof: `Scheduler::Agenda` (`max_values`: None, `max_size`: Some(10718), added: 13193, mode: `MaxEncodedLen`) /// Storage: `Balances::Holds` (r:1 w:1) /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) + /// Storage: `ReversibleTransfers::PendingTransfers` (r:0 w:1) + /// Proof: `ReversibleTransfers::PendingTransfers` (`max_values`: None, `max_size`: Some(291), added: 2766, mode: `MaxEncodedLen`) fn schedule_transfer() -> Weight { // Proof Size summary in bytes: - // Measured: `203` + // Measured: `637` // Estimated: `14183` - // Minimum execution time: 179_000_000 picoseconds. - Weight::from_parts(180_000_000, 14183) - .saturating_add(T::DbWeight::get().reads(7_u64)) - .saturating_add(T::DbWeight::get().writes(5_u64)) + // Minimum execution time: 536_000_000 picoseconds. + Weight::from_parts(550_000_000, 14183) + .saturating_add(T::DbWeight::get().reads(9_u64)) + .saturating_add(T::DbWeight::get().writes(8_u64)) } /// Storage: `ReversibleTransfers::PendingTransfers` (r:1 w:1) - /// Proof: `ReversibleTransfers::PendingTransfers` (`max_values`: None, `max_size`: Some(231), added: 2706, mode: `MaxEncodedLen`) - /// Storage: `ReversibleTransfers::ReversibleAccounts` (r:1 w:0) - /// Proof: `ReversibleTransfers::ReversibleAccounts` (`max_values`: None, `max_size`: Some(91), added: 2566, mode: `MaxEncodedLen`) + /// Proof: `ReversibleTransfers::PendingTransfers` (`max_values`: None, `max_size`: Some(291), added: 2766, mode: `MaxEncodedLen`) + /// Storage: `ReversibleTransfers::HighSecurityAccounts` (r:1 w:0) + /// Proof: `ReversibleTransfers::HighSecurityAccounts` (`max_values`: None, `max_size`: Some(89), added: 2564, mode: `MaxEncodedLen`) /// Storage: `ReversibleTransfers::AccountPendingIndex` (r:1 w:1) /// Proof: `ReversibleTransfers::AccountPendingIndex` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + /// Storage: `ReversibleTransfers::PendingTransfersBySender` (r:1 w:1) + /// Proof: `ReversibleTransfers::PendingTransfersBySender` (`max_values`: None, `max_size`: Some(369), added: 2844, mode: `MaxEncodedLen`) + /// Storage: `ReversibleTransfers::PendingTransfersByRecipient` (r:1 w:1) + /// Proof: `ReversibleTransfers::PendingTransfersByRecipient` (`max_values`: None, `max_size`: Some(369), added: 2844, mode: `MaxEncodedLen`) /// Storage: `Scheduler::Lookup` (r:1 w:1) /// Proof: `Scheduler::Lookup` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) /// Storage: `Scheduler::Agenda` (r:1 w:1) /// Proof: `Scheduler::Agenda` (`max_values`: None, `max_size`: Some(10718), added: 13193, mode: `MaxEncodedLen`) /// Storage: `Balances::Holds` (r:1 w:1) /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) + /// Storage: `System::Account` (r:2 w:2) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn cancel() -> Weight { // Proof Size summary in bytes: - // Measured: `809` + // Measured: `2224` // Estimated: `14183` - // Minimum execution time: 122_000_000 picoseconds. - Weight::from_parts(123_000_000, 14183) - .saturating_add(T::DbWeight::get().reads(7_u64)) - .saturating_add(T::DbWeight::get().writes(6_u64)) + // Minimum execution time: 342_000_000 picoseconds. + Weight::from_parts(349_000_000, 14183) + .saturating_add(T::DbWeight::get().reads(10_u64)) + .saturating_add(T::DbWeight::get().writes(9_u64)) } /// Storage: `ReversibleTransfers::PendingTransfers` (r:1 w:1) - /// Proof: `ReversibleTransfers::PendingTransfers` (`max_values`: None, `max_size`: Some(231), added: 2706, mode: `MaxEncodedLen`) + /// Proof: `ReversibleTransfers::PendingTransfers` (`max_values`: None, `max_size`: Some(291), added: 2766, mode: `MaxEncodedLen`) /// Storage: `Balances::Holds` (r:1 w:1) /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `ReversibleTransfers::AccountPendingIndex` (r:1 w:1) /// Proof: `ReversibleTransfers::AccountPendingIndex` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + /// Storage: `ReversibleTransfers::PendingTransfersBySender` (r:1 w:1) + /// Proof: `ReversibleTransfers::PendingTransfersBySender` (`max_values`: None, `max_size`: Some(369), added: 2844, mode: `MaxEncodedLen`) + /// Storage: `ReversibleTransfers::PendingTransfersByRecipient` (r:1 w:1) + /// Proof: `ReversibleTransfers::PendingTransfersByRecipient` (`max_values`: None, `max_size`: Some(369), added: 2844, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Balances::TransferCount` (r:1 w:1) + /// Proof: `Balances::TransferCount` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Balances::TransferProof` (r:0 w:1) /// Proof: `Balances::TransferProof` (`max_values`: None, `max_size`: Some(32), added: 2507, mode: `MaxEncodedLen`) fn execute_transfer() -> Weight { // Proof Size summary in bytes: - // Measured: `510` - // Estimated: `3696` - // Minimum execution time: 86_000_000 picoseconds. - Weight::from_parts(88_000_000, 3696) - .saturating_add(T::DbWeight::get().reads(4_u64)) - .saturating_add(T::DbWeight::get().writes(5_u64)) + // Measured: `1360` + // Estimated: `3834` + // Minimum execution time: 276_000_000 picoseconds. + Weight::from_parts(290_000_000, 3834) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(8_u64)) + } + /// Storage: `ReversibleTransfers::HighSecurityAccounts` (r:1 w:0) + /// Proof: `ReversibleTransfers::HighSecurityAccounts` (`max_values`: None, `max_size`: Some(89), added: 2564, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Balances::TransferCount` (r:1 w:1) + /// Proof: `Balances::TransferCount` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Balances::TransferProof` (r:0 w:1) + /// Proof: `Balances::TransferProof` (`max_values`: None, `max_size`: Some(32), added: 2507, mode: `MaxEncodedLen`) + fn recover_funds() -> Weight { + // Proof Size summary in bytes: + // Measured: `477` + // Estimated: `3593` + // Minimum execution time: 103_000_000 picoseconds. + Weight::from_parts(106_000_000, 3593) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) } } // For backwards compatibility and tests. impl WeightInfo for () { - /// Storage: `ReversibleTransfers::ReversibleAccounts` (r:1 w:1) - /// Proof: `ReversibleTransfers::ReversibleAccounts` (`max_values`: None, `max_size`: Some(91), added: 2566, mode: `MaxEncodedLen`) + /// Storage: `ReversibleTransfers::HighSecurityAccounts` (r:1 w:1) + /// Proof: `ReversibleTransfers::HighSecurityAccounts` (`max_values`: None, `max_size`: Some(89), added: 2564, mode: `MaxEncodedLen`) + /// Storage: `ReversibleTransfers::InterceptorIndex` (r:1 w:1) + /// Proof: `ReversibleTransfers::InterceptorIndex` (`max_values`: None, `max_size`: Some(1073), added: 3548, mode: `MaxEncodedLen`) fn set_high_security() -> Weight { // Proof Size summary in bytes: - // Measured: `76` - // Estimated: `3556` - // Minimum execution time: 9_000_000 picoseconds. - Weight::from_parts(9_000_000, 3556) - .saturating_add(RocksDbWeight::get().reads(1_u64)) - .saturating_add(RocksDbWeight::get().writes(1_u64)) + // Measured: `192` + // Estimated: `4538` + // Minimum execution time: 78_000_000 picoseconds. + Weight::from_parts(80_000_000, 4538) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) } - /// Storage: `ReversibleTransfers::ReversibleAccounts` (r:1 w:0) - /// Proof: `ReversibleTransfers::ReversibleAccounts` (`max_values`: None, `max_size`: Some(91), added: 2566, mode: `MaxEncodedLen`) + /// Storage: `ReversibleTransfers::HighSecurityAccounts` (r:1 w:0) + /// Proof: `ReversibleTransfers::HighSecurityAccounts` (`max_values`: None, `max_size`: Some(89), added: 2564, mode: `MaxEncodedLen`) + /// Storage: `ReversibleTransfers::GlobalNonce` (r:1 w:1) + /// Proof: `ReversibleTransfers::GlobalNonce` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `ReversibleTransfers::AccountPendingIndex` (r:1 w:1) /// Proof: `ReversibleTransfers::AccountPendingIndex` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) - /// Storage: `ReversibleTransfers::PendingTransfers` (r:1 w:1) - /// Proof: `ReversibleTransfers::PendingTransfers` (`max_values`: None, `max_size`: Some(231), added: 2706, mode: `MaxEncodedLen`) + /// Storage: `ReversibleTransfers::PendingTransfersBySender` (r:1 w:1) + /// Proof: `ReversibleTransfers::PendingTransfersBySender` (`max_values`: None, `max_size`: Some(369), added: 2844, mode: `MaxEncodedLen`) + /// Storage: `ReversibleTransfers::PendingTransfersByRecipient` (r:1 w:1) + /// Proof: `ReversibleTransfers::PendingTransfersByRecipient` (`max_values`: None, `max_size`: Some(369), added: 2844, mode: `MaxEncodedLen`) /// Storage: `Scheduler::Lookup` (r:1 w:1) /// Proof: `Scheduler::Lookup` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) /// Storage: `Timestamp::Now` (r:1 w:0) @@ -165,55 +203,84 @@ impl WeightInfo for () { /// Proof: `Scheduler::Agenda` (`max_values`: None, `max_size`: Some(10718), added: 13193, mode: `MaxEncodedLen`) /// Storage: `Balances::Holds` (r:1 w:1) /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) + /// Storage: `ReversibleTransfers::PendingTransfers` (r:0 w:1) + /// Proof: `ReversibleTransfers::PendingTransfers` (`max_values`: None, `max_size`: Some(291), added: 2766, mode: `MaxEncodedLen`) fn schedule_transfer() -> Weight { // Proof Size summary in bytes: - // Measured: `203` + // Measured: `637` // Estimated: `14183` - // Minimum execution time: 179_000_000 picoseconds. - Weight::from_parts(180_000_000, 14183) - .saturating_add(RocksDbWeight::get().reads(7_u64)) - .saturating_add(RocksDbWeight::get().writes(5_u64)) + // Minimum execution time: 536_000_000 picoseconds. + Weight::from_parts(550_000_000, 14183) + .saturating_add(RocksDbWeight::get().reads(9_u64)) + .saturating_add(RocksDbWeight::get().writes(8_u64)) } /// Storage: `ReversibleTransfers::PendingTransfers` (r:1 w:1) - /// Proof: `ReversibleTransfers::PendingTransfers` (`max_values`: None, `max_size`: Some(231), added: 2706, mode: `MaxEncodedLen`) - /// Storage: `ReversibleTransfers::ReversibleAccounts` (r:1 w:0) - /// Proof: `ReversibleTransfers::ReversibleAccounts` (`max_values`: None, `max_size`: Some(91), added: 2566, mode: `MaxEncodedLen`) + /// Proof: `ReversibleTransfers::PendingTransfers` (`max_values`: None, `max_size`: Some(291), added: 2766, mode: `MaxEncodedLen`) + /// Storage: `ReversibleTransfers::HighSecurityAccounts` (r:1 w:0) + /// Proof: `ReversibleTransfers::HighSecurityAccounts` (`max_values`: None, `max_size`: Some(89), added: 2564, mode: `MaxEncodedLen`) /// Storage: `ReversibleTransfers::AccountPendingIndex` (r:1 w:1) /// Proof: `ReversibleTransfers::AccountPendingIndex` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + /// Storage: `ReversibleTransfers::PendingTransfersBySender` (r:1 w:1) + /// Proof: `ReversibleTransfers::PendingTransfersBySender` (`max_values`: None, `max_size`: Some(369), added: 2844, mode: `MaxEncodedLen`) + /// Storage: `ReversibleTransfers::PendingTransfersByRecipient` (r:1 w:1) + /// Proof: `ReversibleTransfers::PendingTransfersByRecipient` (`max_values`: None, `max_size`: Some(369), added: 2844, mode: `MaxEncodedLen`) /// Storage: `Scheduler::Lookup` (r:1 w:1) /// Proof: `Scheduler::Lookup` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) /// Storage: `Scheduler::Agenda` (r:1 w:1) /// Proof: `Scheduler::Agenda` (`max_values`: None, `max_size`: Some(10718), added: 13193, mode: `MaxEncodedLen`) /// Storage: `Balances::Holds` (r:1 w:1) /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) + /// Storage: `System::Account` (r:2 w:2) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn cancel() -> Weight { // Proof Size summary in bytes: - // Measured: `809` + // Measured: `2224` // Estimated: `14183` - // Minimum execution time: 122_000_000 picoseconds. - Weight::from_parts(123_000_000, 14183) - .saturating_add(RocksDbWeight::get().reads(7_u64)) - .saturating_add(RocksDbWeight::get().writes(6_u64)) + // Minimum execution time: 342_000_000 picoseconds. + Weight::from_parts(349_000_000, 14183) + .saturating_add(RocksDbWeight::get().reads(10_u64)) + .saturating_add(RocksDbWeight::get().writes(9_u64)) } /// Storage: `ReversibleTransfers::PendingTransfers` (r:1 w:1) - /// Proof: `ReversibleTransfers::PendingTransfers` (`max_values`: None, `max_size`: Some(231), added: 2706, mode: `MaxEncodedLen`) + /// Proof: `ReversibleTransfers::PendingTransfers` (`max_values`: None, `max_size`: Some(291), added: 2766, mode: `MaxEncodedLen`) /// Storage: `Balances::Holds` (r:1 w:1) /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `ReversibleTransfers::AccountPendingIndex` (r:1 w:1) /// Proof: `ReversibleTransfers::AccountPendingIndex` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + /// Storage: `ReversibleTransfers::PendingTransfersBySender` (r:1 w:1) + /// Proof: `ReversibleTransfers::PendingTransfersBySender` (`max_values`: None, `max_size`: Some(369), added: 2844, mode: `MaxEncodedLen`) + /// Storage: `ReversibleTransfers::PendingTransfersByRecipient` (r:1 w:1) + /// Proof: `ReversibleTransfers::PendingTransfersByRecipient` (`max_values`: None, `max_size`: Some(369), added: 2844, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Balances::TransferCount` (r:1 w:1) + /// Proof: `Balances::TransferCount` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Balances::TransferProof` (r:0 w:1) /// Proof: `Balances::TransferProof` (`max_values`: None, `max_size`: Some(32), added: 2507, mode: `MaxEncodedLen`) fn execute_transfer() -> Weight { // Proof Size summary in bytes: - // Measured: `510` - // Estimated: `3696` - // Minimum execution time: 86_000_000 picoseconds. - Weight::from_parts(88_000_000, 3696) - .saturating_add(RocksDbWeight::get().reads(4_u64)) - .saturating_add(RocksDbWeight::get().writes(5_u64)) + // Measured: `1360` + // Estimated: `3834` + // Minimum execution time: 276_000_000 picoseconds. + Weight::from_parts(290_000_000, 3834) + .saturating_add(RocksDbWeight::get().reads(7_u64)) + .saturating_add(RocksDbWeight::get().writes(8_u64)) + } + /// Storage: `ReversibleTransfers::HighSecurityAccounts` (r:1 w:0) + /// Proof: `ReversibleTransfers::HighSecurityAccounts` (`max_values`: None, `max_size`: Some(89), added: 2564, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Balances::TransferCount` (r:1 w:1) + /// Proof: `Balances::TransferCount` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Balances::TransferProof` (r:0 w:1) + /// Proof: `Balances::TransferProof` (`max_values`: None, `max_size`: Some(32), added: 2507, mode: `MaxEncodedLen`) + fn recover_funds() -> Weight { + // Proof Size summary in bytes: + // Measured: `477` + // Estimated: `3593` + // Minimum execution time: 103_000_000 picoseconds. + Weight::from_parts(106_000_000, 3593) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) } } diff --git a/runtime/tests/transactions/reversible_integration.rs b/runtime/tests/transactions/reversible_integration.rs index b2a49c9a..28f2d0d0 100644 --- a/runtime/tests/transactions/reversible_integration.rs +++ b/runtime/tests/transactions/reversible_integration.rs @@ -1,9 +1,7 @@ use crate::common::TestCommons; use frame_support::{assert_err, assert_ok}; use qp_scheduler::BlockNumberOrTimestamp; -use quantus_runtime::{ - Balances, Recovery, ReversibleTransfers, RuntimeCall, RuntimeOrigin, EXISTENTIAL_DEPOSIT, UNIT, -}; +use quantus_runtime::{Balances, ReversibleTransfers, RuntimeOrigin, EXISTENTIAL_DEPOSIT}; use sp_runtime::MultiAddress; fn acc(n: u8) -> sp_core::crypto::AccountId32 { @@ -16,9 +14,6 @@ fn high_security_account() -> sp_core::crypto::AccountId32 { fn interceptor() -> sp_core::crypto::AccountId32 { TestCommons::account_id(2) } -fn recoverer() -> sp_core::crypto::AccountId32 { - TestCommons::account_id(3) -} #[test] fn high_security_end_to_end_flow() { @@ -97,64 +92,38 @@ fn high_security_end_to_end_flow() { pallet_reversible_transfers::Error::::AccountAlreadyHighSecurity ); - // 6) Interceptor recovers all funds from high sec account via Recovery pallet - - // 6.1 Interceptor initiates recovery - assert_ok!(Recovery::initiate_recovery( - RuntimeOrigin::signed(interceptor()), - MultiAddress::Id(high_security_account()), - )); - - // 6.2 Interceptor vouches on recovery - assert_ok!(Recovery::vouch_recovery( - RuntimeOrigin::signed(interceptor()), - MultiAddress::Id(high_security_account()), - MultiAddress::Id(interceptor()), - )); - - // 6.3 Interceptor claims recovery - assert_ok!(Recovery::claim_recovery( - RuntimeOrigin::signed(interceptor()), - MultiAddress::Id(high_security_account()), - )); - + // 6) Interceptor recovers all funds from high sec account via recover_funds let interceptor_before_recovery = Balances::free_balance(interceptor()); - // 6.4 Interceptor recovers all funds - let call = RuntimeCall::Balances(pallet_balances::Call::transfer_all { - dest: MultiAddress::Id(interceptor()), - keep_alive: false, - }); - assert_ok!(Recovery::as_recovered( + assert_ok!(ReversibleTransfers::recover_funds( RuntimeOrigin::signed(interceptor()), - MultiAddress::Id(high_security_account()), - Box::new(call), + high_security_account(), )); let hs_after_recovery = Balances::free_balance(high_security_account()); let interceptor_after_recovery = Balances::free_balance(interceptor()); - // HS should be drained to existential deposit; account 2 increased accordingly - assert_eq!(hs_after_recovery, EXISTENTIAL_DEPOSIT); - - // Fees - Interceptor spends 11 units in total for all the calls they are making. + // HS account should be drained completely (keep_alive: false) + assert_eq!(hs_after_recovery, 0); - // Interceptor has hs account's balance now - let estimated_fees = UNIT/100 * 101; // The final recover call costs 1.01 units. + // Interceptor should have received all the HS account's remaining funds assert!( - interceptor_after_recovery >= (hs_after_cancel + interceptor_before_recovery - estimated_fees), - "recoverer {interceptor_after_recovery} should be at least {hs_after_cancel} + {interceptor_start} - {estimated_fees}" + interceptor_after_recovery > interceptor_before_recovery, + "interceptor should have received funds from HS account" + ); + assert_eq!( + interceptor_after_recovery, + interceptor_before_recovery + hs_after_cancel, + "interceptor should have received the HS account's remaining balance" ); }); } #[test] -fn test_recovery_allows_multiple_recovery_configs() { - // Test that Account 3 can recover both Account 1 (HS) and Account 2 (interceptor) - // This proves our inheritance + high security use case will work +fn test_recover_funds_only_works_for_guardian() { + // Test that only the guardian (interceptor) can call recover_funds let mut ext = TestCommons::new_test_ext(); ext.execute_with(|| { - // Set up Account 1 as high security with Account 2 as interceptor let delay = BlockNumberOrTimestamp::BlockNumber(5); assert_ok!(ReversibleTransfers::set_high_security( RuntimeOrigin::signed(high_security_account()), @@ -162,88 +131,33 @@ fn test_recovery_allows_multiple_recovery_configs() { interceptor(), )); - // Account 2 initiates recovery of Account 1 - assert_ok!(Recovery::initiate_recovery( - RuntimeOrigin::signed(interceptor()), - MultiAddress::Id(high_security_account()), - )); - assert_ok!(Recovery::vouch_recovery( - RuntimeOrigin::signed(interceptor()), - MultiAddress::Id(high_security_account()), - MultiAddress::Id(interceptor()), - )); - assert_ok!(Recovery::claim_recovery( - RuntimeOrigin::signed(interceptor()), - MultiAddress::Id(high_security_account()), - )); - - // Set up recovery for Account 2 with Account 3 as friend - assert_ok!(Recovery::create_recovery( - RuntimeOrigin::signed(interceptor()), - vec![recoverer()], - 1, - 0, - )); - - // Now Account 3 can recover Account 2 - assert_ok!(Recovery::initiate_recovery( - RuntimeOrigin::signed(recoverer()), - MultiAddress::Id(interceptor()), - )); - assert_ok!(Recovery::vouch_recovery( - RuntimeOrigin::signed(recoverer()), - MultiAddress::Id(interceptor()), - MultiAddress::Id(recoverer()), - )); - - // This should succeed - Account 3 can recover Account 2 - assert_ok!(Recovery::claim_recovery( - RuntimeOrigin::signed(recoverer()), - MultiAddress::Id(interceptor()), - )); - - // Verify both proxies exist - // Account 2 proxies Account 1 - assert_eq!(Recovery::proxy(interceptor()), Some(high_security_account())); - // Account 3 proxies Account 2 - assert_eq!(Recovery::proxy(recoverer()), Some(interceptor())); - - // Give Account 1 some funds to test transfer - let transfer_amount = 100 * UNIT; - assert_ok!(Balances::force_set_balance( - RuntimeOrigin::root(), - MultiAddress::Id(high_security_account()), - transfer_amount, - )); + // Non-guardian (account 3) tries to recover funds - should fail + assert_err!( + ReversibleTransfers::recover_funds( + RuntimeOrigin::signed(acc(3)), + high_security_account(), + ), + pallet_reversible_transfers::Error::::InvalidReverser + ); - // Capture balances before nested transfer + // Guardian (account 2) can recover funds let hs_balance_before = Balances::free_balance(high_security_account()); - let recoverer_balance_before = Balances::free_balance(recoverer()); - - // Now test nested as_recovered: Account 3 -> Account 2 -> Account 1 - let inner_call = RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive { - dest: MultiAddress::Id(recoverer()), - value: transfer_amount / 2, // Transfer half the amount - }); - let outer_call = RuntimeCall::Recovery(pallet_recovery::Call::as_recovered { - account: MultiAddress::Id(high_security_account()), - call: Box::new(inner_call), - }); - - // Account 3 calls as_recovered on Account 2, which contains as_recovered on Account 1 - // This should succeed and transfer funds: Account 1 -> Account 3 - assert_ok!(Recovery::as_recovered( - RuntimeOrigin::signed(recoverer()), - MultiAddress::Id(interceptor()), - Box::new(outer_call), + let interceptor_balance_before = Balances::free_balance(interceptor()); + + assert_ok!(ReversibleTransfers::recover_funds( + RuntimeOrigin::signed(interceptor()), + high_security_account(), )); - // Verify the transfer happened + // Verify funds were transferred let hs_balance_after = Balances::free_balance(high_security_account()); - let recoverer_balance_after = Balances::free_balance(recoverer()); - - assert_eq!(hs_balance_before, transfer_amount); - assert!(hs_balance_after < hs_balance_before); // Account 1 lost funds - assert!(recoverer_balance_after > recoverer_balance_before); // Account 3 gained funds + let interceptor_balance_after = Balances::free_balance(interceptor()); + + assert_eq!(hs_balance_after, 0); + assert_eq!( + interceptor_balance_after, + interceptor_balance_before + hs_balance_before, + "guardian should have received all HS account funds" + ); }); } From 1c2b9784d50e065597f0edd8b462e37dde74f198 Mon Sep 17 00:00:00 2001 From: Cezary Olborski Date: Tue, 27 Jan 2026 22:02:21 +0800 Subject: [PATCH 22/27] feat: Custom Mutisig Pallet (#352) * feat: Merkle Airdrop - removed * feat: Vesting pallet - removed * poc: First multisig version * fix: Taplo * fix: Execution for expired & address simplified fallback * draft: Historical proposals - paginaged endpoint * draft: Historical proposals - from events only * ref: Events renamed + Deposits logic simplified * feat: GracePeriod param removed * fix: Reentrancy * feat: History cleaning redesigned * fix: Expiry - additional validation * feat: Proposal nonce * feat: Dynamic weights * feat: Multisig deposit fee * feat: MaxExpiry param * feat: Fees to Treasury * feat: History removable only by signers * fix: Weights * feat: Fees burned * feat: Filibuster protection * feat: Proposals auto cleaning * feat: Proposal id - nonce instead of hash * feat: Calls - production whitelist * feat: Remove call whitelisting * fix: Test fix after balances pallet update * fix: Review cleaning --- Cargo.lock | 19 + Cargo.toml | 2 + pallets/multisig/Cargo.toml | 61 + pallets/multisig/README.md | 483 ++++++++ pallets/multisig/src/benchmarking.rs | 521 +++++++++ pallets/multisig/src/lib.rs | 1110 ++++++++++++++++++ pallets/multisig/src/mock.rs | 143 +++ pallets/multisig/src/tests.rs | 1247 +++++++++++++++++++++ pallets/multisig/src/weights.rs | 323 ++++++ runtime/Cargo.toml | 3 + runtime/src/benchmarks.rs | 1 + runtime/src/configs/mod.rs | 31 + runtime/src/lib.rs | 3 + runtime/tests/common.rs | 4 +- runtime/tests/transactions/integration.rs | 4 +- 15 files changed, 3951 insertions(+), 4 deletions(-) create mode 100644 pallets/multisig/Cargo.toml create mode 100644 pallets/multisig/README.md create mode 100644 pallets/multisig/src/benchmarking.rs create mode 100644 pallets/multisig/src/lib.rs create mode 100644 pallets/multisig/src/mock.rs create mode 100644 pallets/multisig/src/tests.rs create mode 100644 pallets/multisig/src/weights.rs diff --git a/Cargo.lock b/Cargo.lock index a5147e1b..9f186480 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7170,6 +7170,24 @@ dependencies = [ "sp-mmr-primitives", ] +[[package]] +name = "pallet-multisig" +version = "1.0.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-balances 40.0.1", + "pallet-timestamp", + "parity-scale-codec", + "scale-info", + "sp-arithmetic", + "sp-core", + "sp-io", + "sp-runtime", +] + [[package]] name = "pallet-preimage" version = "41.0.0" @@ -9158,6 +9176,7 @@ dependencies = [ "pallet-balances 40.0.1", "pallet-conviction-voting", "pallet-mining-rewards", + "pallet-multisig", "pallet-preimage", "pallet-qpow", "pallet-ranked-collective", diff --git a/Cargo.toml b/Cargo.toml index bd8bcfd9..a4c9f0a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "node", "pallets/balances", "pallets/mining-rewards", + "pallets/multisig", "pallets/qpow", "pallets/reversible-transfers", "pallets/scheduler", @@ -131,6 +132,7 @@ zeroize = { version = "1.7.0", default-features = false } # Own dependencies pallet-balances = { path = "./pallets/balances", default-features = false } pallet-mining-rewards = { path = "./pallets/mining-rewards", default-features = false } +pallet-multisig = { path = "./pallets/multisig", default-features = false } pallet-qpow = { path = "./pallets/qpow", default-features = false } pallet-reversible-transfers = { path = "./pallets/reversible-transfers", default-features = false } pallet-scheduler = { path = "./pallets/scheduler", default-features = false } diff --git a/pallets/multisig/Cargo.toml b/pallets/multisig/Cargo.toml new file mode 100644 index 00000000..0d768485 --- /dev/null +++ b/pallets/multisig/Cargo.toml @@ -0,0 +1,61 @@ +[package] +authors.workspace = true +description = "Multisig pallet for Quantus" +edition.workspace = true +homepage.workspace = true +license = "MIT-0" +name = "pallet-multisig" +repository.workspace = true +version = "1.0.0" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { features = ["derive", "max-encoded-len"], workspace = true } +frame-benchmarking = { optional = true, workspace = true } +frame-support.workspace = true +frame-system.workspace = true +log.workspace = true +pallet-balances.workspace = true +scale-info = { features = ["derive"], workspace = true } +sp-arithmetic.workspace = true +sp-core.workspace = true +sp-io.workspace = true +sp-runtime.workspace = true + +[dev-dependencies] +frame-support = { workspace = true, features = ["experimental"], default-features = true } +pallet-balances = { workspace = true, features = ["std"] } +pallet-timestamp.workspace = true +sp-core.workspace = true +sp-io.workspace = true + +[features] +default = ["std"] +runtime-benchmarks = [ + "frame-benchmarking", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "log/std", + "pallet-balances/std", + "pallet-timestamp/std", + "scale-info/std", + "sp-arithmetic/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/pallets/multisig/README.md b/pallets/multisig/README.md new file mode 100644 index 00000000..8d259e3a --- /dev/null +++ b/pallets/multisig/README.md @@ -0,0 +1,483 @@ +# Multisig Pallet + +A multisignature wallet pallet for the Quantus blockchain with an economic security model. + +## Overview + +This pallet provides functionality for creating and managing multisig accounts that require multiple approvals before executing transactions. It implements a dual fee+deposit system for spam prevention and storage cleanup mechanisms with grace periods. + +## Quick Start + +Basic workflow for using a multisig: + +```rust +// 1. Create a 2-of-3 multisig (Alice creates, Bob/Charlie/Dave are signers) +Multisig::create_multisig(Origin::signed(alice), vec![bob, charlie, dave], 2); +let multisig_addr = Multisig::derive_multisig_address(&[bob, charlie, dave], 0); + +// 2. Bob proposes a transaction +let call = RuntimeCall::Balances(pallet_balances::Call::transfer { dest: eve, value: 100 }); +Multisig::propose(Origin::signed(bob), multisig_addr, call.encode(), expiry_block); + +// 3. Charlie approves - transaction executes automatically (2/2 threshold reached) +Multisig::approve(Origin::signed(charlie), multisig_addr, proposal_id); +// ✅ Transaction executed! No separate call needed. +``` + +**Key Point:** Once the threshold is reached, the transaction is **automatically executed**. +There is no separate `execute()` call exposed to users. + +## Core Functionality + +### 1. Create Multisig +Creates a new multisig account with deterministic address generation. + +**Required Parameters:** +- `signers: Vec` - List of authorized signers (REQUIRED, 1 to MaxSigners) +- `threshold: u32` - Number of approvals needed (REQUIRED, 1 ≤ threshold ≤ signers.len()) + +**Validation:** +- No duplicate signers +- Threshold must be > 0 +- Threshold cannot exceed number of signers +- Signers count must be ≤ MaxSigners + +**Important:** Signers are automatically sorted before storing and address generation. Order doesn't matter: +- `[alice, bob, charlie]` → sorted to `[alice, bob, charlie]` → `address_1` +- `[charlie, bob, alice]` → sorted to `[alice, bob, charlie]` → `address_1` (same!) +- To create multiple multisigs with same signers, the nonce provides uniqueness + +**Economic Costs:** +- **MultisigFee**: Non-refundable fee (spam prevention) → burned +- **MultisigDeposit**: Refundable deposit (storage rent) → returned when multisig dissolved + +### 2. Propose Transaction +Creates a new proposal for multisig execution. + +**Required Parameters:** +- `multisig_address: AccountId` - Target multisig account (REQUIRED) +- `call: Vec` - Encoded RuntimeCall to execute (REQUIRED, max MaxCallSize bytes) +- `expiry: BlockNumber` - Deadline for collecting approvals (REQUIRED) + +**Validation:** +- Caller must be a signer +- Call size must be ≤ MaxCallSize +- Multisig cannot have MaxTotalProposalsInStorage or more total proposals in storage +- Caller cannot exceed their per-signer proposal limit (`MaxTotalProposalsInStorage / signers_count`) +- Expiry must be in the future (expiry > current_block) +- Expiry must not exceed MaxExpiryDuration blocks from now (expiry ≤ current_block + MaxExpiryDuration) + +**Auto-Cleanup Before Creation:** +Before creating a new proposal, the system **automatically removes all expired Active proposals** for this multisig: +- Expired proposals are identified (current_block > expiry) +- Deposits are returned to original proposers +- Storage is cleaned up +- Counters are decremented +- Events are emitted for each removed proposal + +This ensures storage is kept clean and users get their deposits back without manual intervention. + +**Economic Costs:** +- **ProposalFee**: Non-refundable fee (spam prevention, scaled by signer count) → burned +- **ProposalDeposit**: Refundable deposit (storage rent) → returned when proposal removed + +**Important:** Fee is ALWAYS paid, even if proposal expires or is cancelled. Only deposit is refundable. + +### 3. Approve Transaction +Adds caller's approval to an existing proposal. **If this approval brings the total approvals +to or above the threshold, the transaction will be automatically executed and immediately removed from storage.** + +**Required Parameters:** +- `multisig_address: AccountId` - Target multisig (REQUIRED) +- `proposal_id: u32` - ID (nonce) of the proposal to approve (REQUIRED) + +**Validation:** +- Caller must be a signer +- Proposal must exist +- Proposal must not be expired (current_block ≤ expiry) +- Caller must not have already approved + +**Auto-Execution:** +When approval count reaches the threshold: +- Encoded call is executed as multisig_address origin +- Proposal **immediately removed** from storage +- ProposalDeposit **immediately returned** to proposer +- TransactionExecuted event emitted with execution result + +**Economic Costs:** None (deposit immediately returned on execution) + +### 4. Cancel Transaction +Cancels a proposal and immediately removes it from storage (proposer only). + +**Required Parameters:** +- `multisig_address: AccountId` - Target multisig (REQUIRED) +- `proposal_id: u32` - ID (nonce) of the proposal to cancel (REQUIRED) + +**Validation:** +- Caller must be the proposer +- Proposal must exist and be Active + +**Economic Effects:** +- Proposal **immediately removed** from storage +- ProposalDeposit **immediately returned** to proposer +- Counters decremented + +**Economic Costs:** None (deposit immediately returned) + +**Note:** ProposalFee is NOT refunded - it was burned at proposal creation. + +### 5. Remove Expired +Manually removes expired proposals from storage. Only signers can call this. + +**Important:** This is rarely needed because expired proposals are automatically cleaned up when anyone creates a new proposal in the same multisig. + +**Required Parameters:** +- `multisig_address: AccountId` - Target multisig (REQUIRED) +- `proposal_id: u32` - ID (nonce) of the expired proposal (REQUIRED) + +**Validation:** +- Caller must be a signer of the multisig +- Proposal must exist and be Active +- Must be expired (current_block > expiry) + +**Note:** Executed/Cancelled proposals are automatically removed immediately, so this only applies to Active+Expired proposals. + +**Economic Effects:** +- ProposalDeposit returned to **original proposer** (not caller) +- Proposal removed from storage +- Counters decremented + +**Economic Costs:** None (deposit always returned to proposer) + +**Auto-Cleanup:** When anyone calls `propose()`, all expired proposals are automatically removed first, making this function often unnecessary. + +### 6. Claim Deposits +Batch cleanup operation to recover all expired proposal deposits. + +**Important:** This is rarely needed because expired proposals are automatically cleaned up when anyone creates a new proposal in the same multisig. + +**Required Parameters:** +- `multisig_address: AccountId` - Target multisig (REQUIRED) + +**Validation:** +- Only cleans proposals where caller is proposer +- Only removes Active+Expired proposals (Executed/Cancelled already auto-removed) +- Must be expired (current_block > expiry) + +**Economic Effects:** +- Returns all eligible proposal deposits to caller +- Removes all expired proposals from storage +- Counters decremented + +**Economic Costs:** None (only returns deposits) + +**Auto-Cleanup:** When anyone calls `propose()`, all expired proposals are automatically removed first, making this function often unnecessary. + +## Use Cases + +**Payroll Multisig (transfers only):** +```rust +// Only allow keep_alive transfers to prevent account deletion +matches!(call, RuntimeCall::Balances(Call::transfer_keep_alive { .. })) +``` + +**Treasury Multisig (governance + transfers):** +```rust +matches!(call, + RuntimeCall::Balances(Call::transfer_keep_alive { .. }) | + RuntimeCall::Scheduler(Call::schedule { .. }) | // Time-locked ops + RuntimeCall::Democracy(Call::veto { .. }) // Emergency stops +) +``` + +## Economic Model + +### Fees (Non-refundable, burned) +**Purpose:** Spam prevention and deflationary pressure + +- **MultisigFee**: + - Charged on multisig creation + - Burned immediately (reduces total supply) + - **Never returned** (even if multisig dissolved) + - Creates economic barrier to prevent spam multisig creation + +- **ProposalFee**: + - Charged on proposal creation + - **Dynamically scaled** by signer count: `BaseFee × (1 + SignerCount × StepFactor)` + - Burned immediately (reduces total supply) + - **Never returned** (even if proposal expires or is cancelled) + - Makes spam expensive, scales cost with multisig complexity + +**Why burned (not sent to treasury)?** +- Creates deflationary pressure on token supply +- Simpler implementation (no treasury dependency) +- Spam attacks reduce circulating supply +- Lower transaction costs (withdraw vs transfer) + +### Deposits (Refundable, locked as storage rent) +**Purpose:** Compensate for on-chain storage, incentivize cleanup + +- **MultisigDeposit**: + - Reserved on multisig creation + - Returned when multisig dissolved (via `dissolve_multisig`) + - Locked until no proposals exist and balance is zero + - Opportunity cost incentivizes cleanup + +- **ProposalDeposit**: + - Reserved on proposal creation + - **Auto-Returned Immediately:** + - When proposal executed (threshold reached) + - When proposal cancelled (proposer cancels) + - **Manual Cleanup Required:** + - Expired proposals: Must be manually removed OR auto-cleaned on next `propose()` + - **Auto-Cleanup:** When anyone creates new proposal, all expired proposals cleaned automatically + - No grace period needed - executed/cancelled proposals auto-removed + +### Storage Limits & Configuration +**Purpose:** Prevent unbounded storage growth and resource exhaustion + +- **MaxSigners**: Maximum signers per multisig + - Trade-off: Higher → more flexible governance, more computation per approval + +- **MaxTotalProposalsInStorage**: Maximum total proposals (Active + Executed + Cancelled) + - Trade-off: Higher → more flexible, more storage risk + - Forces periodic cleanup to continue operating + - **Auto-cleanup**: Expired proposals are automatically removed when new proposals are created + - **Per-Signer Limit**: Each signer gets `MaxTotalProposalsInStorage / signers_count` quota + - Prevents single signer from monopolizing storage (filibuster protection) + - Fair allocation ensures all signers can participate + - Example: 20 total, 5 signers → 4 proposals max per signer + +- **MaxCallSize**: Maximum encoded call size in bytes + - Trade-off: Larger → more flexibility, more storage per proposal + - Should accommodate common operations (transfers, staking, governance) + +- **MaxExpiryDuration**: Maximum blocks in the future for proposal expiry + - Trade-off: Shorter → faster turnover, may not suit slow decision-making + - Prevents infinite-duration deposit locks + - Should exceed typical multisig decision timeframes + +**Configuration values are runtime-specific.** See runtime config for production values. + +## Storage + +### Multisigs: Map +Stores multisig account data: +```rust +MultisigData { + signers: BoundedVec, // List of authorized signers + threshold: u32, // Required approvals + nonce: u64, // Unique identifier used in address generation + deposit: Balance, // Reserved deposit (refundable) + creator: AccountId, // Who created it (receives deposit back) + last_activity: BlockNumber, // Last action timestamp (for grace period) + active_proposals: u32, // Count of open proposals (monitoring/analytics) + proposals_per_signer: BoundedBTreeMap, // Per-signer proposal count (filibuster protection) +} +``` + +### Proposals: DoubleMap +Stores proposal data indexed by (multisig_address, proposal_id): +```rust +ProposalData { + proposer: AccountId, // Who proposed (receives deposit back) + call: BoundedVec, // Encoded RuntimeCall to execute + expiry: BlockNumber, // Deadline for approvals + approvals: BoundedVec, // List of signers who approved + deposit: Balance, // Reserved deposit (refundable) + status: ProposalStatus, // Active only (Executed/Cancelled are removed immediately) +} +``` + +**Important:** Only **Active** proposals are stored. Executed and Cancelled proposals are **immediately removed** from storage and their deposits are returned. Historical data is available through events (see Historical Data section below). + +### GlobalNonce: u64 +Internal counter for generating unique multisig addresses. Not exposed via API. + +## Events + +- `MultisigCreated { creator, multisig_address, signers, threshold, nonce }` +- `ProposalCreated { multisig_address, proposer, proposal_id }` +- `ProposalApproved { multisig_address, approver, proposal_id, approvals_count }` +- `ProposalExecuted { multisig_address, proposal_id, proposer, call, approvers, result }` +- `ProposalCancelled { multisig_address, proposer, proposal_id }` +- `ProposalRemoved { multisig_address, proposal_id, proposer, removed_by }` +- `DepositsClaimed { multisig_address, claimer, total_returned, proposals_removed, multisig_removed }` +- `MultisigDissolved { multisig_address, caller, deposit_returned }` + +## Errors + +- `NotEnoughSigners` - Less than 1 signer provided +- `ThresholdZero` - Threshold cannot be 0 +- `ThresholdTooHigh` - Threshold exceeds number of signers +- `TooManySigners` - Exceeds MaxSigners limit +- `DuplicateSigner` - Duplicate address in signers list +- `MultisigAlreadyExists` - Multisig with this address already exists +- `MultisigNotFound` - Multisig does not exist +- `NotASigner` - Caller is not authorized signer +- `ProposalNotFound` - Proposal does not exist +- `NotProposer` - Caller is not the proposer (for cancel) +- `AlreadyApproved` - Signer already approved this proposal +- `NotEnoughApprovals` - Threshold not met (internal error, should not occur) +- `ExpiryInPast` - Proposal expiry is not in the future (for propose) +- `ExpiryTooFar` - Proposal expiry exceeds MaxExpiryDuration (for propose) +- `ProposalExpired` - Proposal deadline passed (for approve) +- `CallTooLarge` - Encoded call exceeds MaxCallSize +- `InvalidCall` - Call decoding failed during execution +- `InsufficientBalance` - Not enough funds for fee/deposit +- `TooManyProposalsInStorage` - Multisig has MaxTotalProposalsInStorage total proposals (cleanup required to create new) +- `TooManyProposalsPerSigner` - Caller has reached their per-signer proposal limit (`MaxTotalProposalsInStorage / signers_count`) +- `ProposalNotExpired` - Proposal not yet expired (for remove_expired) +- `ProposalNotActive` - Proposal is not active (already executed or cancelled) + +## Important Behavior + +### Simple Proposal IDs (Not Hashes) +Proposals are identified by a simple **nonce (u32)** instead of a hash: +- **More efficient:** 4 bytes instead of 32 bytes (Blake2_256 hash) +- **Simpler:** No need to hash `(call, nonce)`, just use nonce directly +- **Better UX:** Sequential IDs (0, 1, 2...) easier to read than random hashes +- **Easier queries:** Can iterate proposals by ID without needing call data + +**Example:** +```rust +propose(...) // → proposal_id: 0 +propose(...) // → proposal_id: 1 +propose(...) // → proposal_id: 2 + +// Approve by ID (not hash) +approve(multisig, 1) // Approve proposal #1 +``` + +### Signer Order Doesn't Matter +Signers are **automatically sorted** before address generation and storage: +- Input order is irrelevant - signers are always sorted deterministically +- Address is derived from `Hash(PalletId + sorted_signers + nonce)` +- Same signers in any order = same multisig address (with same nonce) +- To create multiple multisigs with same participants, use different creation transactions (nonce auto-increments) + +**Example:** +```rust +// These create the SAME multisig address (same signers, same nonce): +create_multisig([alice, bob, charlie], 2) // → multisig_addr_1 (nonce=0) +create_multisig([charlie, bob, alice], 2) // → multisig_addr_1 (SAME! nonce would be 1 but already exists) + +// To create another multisig with same signers: +create_multisig([alice, bob, charlie], 2) // → multisig_addr_2 (nonce=1, different address) +``` + +## Historical Data and Event Indexing + +The pallet does **not** maintain on-chain storage of executed proposal history. Instead, all historical data is available through **blockchain events**, which are designed to be efficiently indexed by off-chain indexers like **SubSquid**. + +### ProposalExecuted Event + +When a proposal is successfully executed, the pallet emits a comprehensive `ProposalExecuted` event containing all relevant data: + +```rust +Event::ProposalExecuted { + multisig_address: T::AccountId, // The multisig that executed + proposal_id: u32, // ID (nonce) of the proposal + proposer: T::AccountId, // Who originally proposed it + call: Vec, // The encoded call that was executed + approvers: Vec, // All accounts that approved + result: DispatchResult, // Whether execution succeeded or failed +} +``` + +### Indexing with SubSquid + +This event structure is optimized for indexing by SubSquid and similar indexers: +- **Complete data**: All information needed to reconstruct the full proposal history +- **Queryable**: Indexers can efficiently query by multisig address, proposer, approvers, etc. +- **Execution result**: Both successful and failed executions are recorded +- **No storage bloat**: Events don't consume on-chain storage long-term + +**All events** for complete history: +- `MultisigCreated` - When a multisig is created +- `ProposalCreated` - When a proposal is submitted +- `ProposalApproved` - Each time someone approves (includes current approval count) +- `ProposalExecuted` - When a proposal is executed (includes full execution details) +- `ProposalCancelled` - When a proposal is cancelled by proposer +- `ProposalRemoved` - When a proposal is removed from storage (deposits returned) +- `DepositsClaimed` - Batch removal of multiple proposals + +### Benefits of Event-Based History + +- ✅ **No storage costs**: Events don't occupy chain storage after archival +- ✅ **Complete history**: All actions are recorded permanently in events +- ✅ **Efficient querying**: Off-chain indexers provide fast, flexible queries +- ✅ **No DoS risk**: No on-chain iteration over unbounded storage +- ✅ **Standard practice**: Follows Substrate best practices for historical data + +## Security Considerations + +### Spam Prevention +- Fees (non-refundable, burned) prevent proposal spam +- Deposits (refundable) prevent storage bloat +- MaxTotalProposalsInStorage caps total storage per multisig +- Per-signer limits prevent single signer from monopolizing storage (filibuster protection) +- Auto-cleanup of expired proposals reduces storage pressure + +### Storage Cleanup +- Grace period allows proposers priority cleanup +- After grace: public cleanup incentivized +- Batch cleanup via claim_deposits for efficiency + +### Economic Attacks +- **Multisig Spam:** Costs MultisigFee (burned, reduces supply) + - No refund even if never used + - Economic barrier to creation spam +- **Proposal Spam:** Costs ProposalFee (burned, reduces supply) + ProposalDeposit (locked) + - Fee never returned (even if expired/cancelled) + - Deposit locked until cleanup + - Cost scales with multisig size (dynamic pricing) +- **Filibuster Attack (Single Signer Monopolization):** + - **Attack:** One signer tries to fill entire proposal queue + - **Defense:** Per-signer limit caps each at `MaxTotalProposalsInStorage / signers_count` + - **Effect:** Other signers retain their fair quota + - **Cost:** Attacker still pays fees for their proposals (burned) +- **Result:** Spam attempts reduce circulating supply +- **No global limits:** Only per-multisig limits (decentralized resistance) + +### Call Execution +- Calls execute with multisig_address as origin +- Multisig can call ANY pallet (including recursive multisig calls) +- Call validation happens at execution time +- Failed calls emit event with error but don't revert proposal removal + +## Configuration Example + + +```rust +impl pallet_multisig::Config for Runtime { + type RuntimeCall = RuntimeCall; + type Currency = Balances; + + // Storage limits (prevent unbounded growth) + type MaxSigners = ConstU32<100>; // Max complexity + type MaxTotalProposalsInStorage = ConstU32<200>; // Total storage cap (auto-cleanup on propose) + type MaxCallSize = ConstU32<10240>; // Per-proposal storage limit + type MaxExpiryDuration = ConstU32<100_800>; // Max proposal lifetime (~2 weeks @ 12s) + + // Economic parameters (example values - adjust per runtime) + type MultisigFee = ConstU128<{ 100 * MILLI_UNIT }>; // Creation barrier + type MultisigDeposit = ConstU128<{ 500 * MILLI_UNIT }>; // Storage rent + type ProposalFee = ConstU128<{ 1000 * MILLI_UNIT }>; // Base proposal cost + type ProposalDeposit = ConstU128<{ 1000 * MILLI_UNIT }>; // Cleanup incentive + type SignerStepFactor = Permill::from_percent(1); // Dynamic pricing (1% per signer) + + type PalletId = ConstPalletId(*b"py/mltsg"); + type WeightInfo = pallet_multisig::weights::SubstrateWeight; +} +``` + +**Parameter Selection Considerations:** +- **High-value chains:** Lower fees, higher deposits, tighter limits +- **Low-value chains:** Higher fees (maintain spam protection), lower deposits +- **Enterprise use:** Higher MaxSigners, longer MaxExpiryDuration +- **Public use:** Moderate limits, shorter expiry for faster turnover + +## License + +MIT-0 diff --git a/pallets/multisig/src/benchmarking.rs b/pallets/multisig/src/benchmarking.rs new file mode 100644 index 00000000..40c28a2e --- /dev/null +++ b/pallets/multisig/src/benchmarking.rs @@ -0,0 +1,521 @@ +//! Benchmarking setup for pallet-multisig + +use super::*; +use crate::Pallet as Multisig; +use alloc::vec; +use frame_benchmarking::{account as benchmark_account, v2::*, BenchmarkError}; +use frame_support::traits::{fungible::Mutate, ReservableCurrency}; +use frame_system::RawOrigin; + +const SEED: u32 = 0; + +// Helper to fund an account +type BalanceOf2 = ::Balance; + +fn fund_account(account: &T::AccountId, amount: BalanceOf2) +where + T: Config + pallet_balances::Config, +{ + let _ = as Mutate>::mint_into( + account, + amount * as frame_support::traits::Currency>::minimum_balance(), + ); +} + +#[benchmarks( + where + T: Config + pallet_balances::Config, + BalanceOf2: From, +)] +mod benchmarks { + use super::*; + use codec::Encode; + + #[benchmark] + fn create_multisig() -> Result<(), BenchmarkError> { + let caller: T::AccountId = whitelisted_caller(); + + // Fund the caller with enough balance for deposit + fund_account::(&caller, BalanceOf2::::from(10000u128)); + + // Create signers (including caller) + let signer1: T::AccountId = benchmark_account("signer1", 0, SEED); + let signer2: T::AccountId = benchmark_account("signer2", 1, SEED); + let signers = vec![caller.clone(), signer1, signer2]; + let threshold = 2u32; + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), signers.clone(), threshold); + + // Verify the multisig was created + // Note: signers are sorted internally, so we must sort for address derivation + let mut sorted_signers = signers.clone(); + sorted_signers.sort(); + let multisig_address = Multisig::::derive_multisig_address(&sorted_signers, 0); + assert!(Multisigs::::contains_key(multisig_address)); + + Ok(()) + } + + #[benchmark] + fn propose( + c: Linear<0, { T::MaxCallSize::get().saturating_sub(100) }>, + e: Linear<0, { T::MaxTotalProposalsInStorage::get() }>, // expired proposals to cleanup + ) -> Result<(), BenchmarkError> { + // Setup: Create a multisig first + let caller: T::AccountId = whitelisted_caller(); + fund_account::(&caller, BalanceOf2::::from(100000u128)); + + let signer1: T::AccountId = benchmark_account("signer1", 0, SEED); + let signer2: T::AccountId = benchmark_account("signer2", 1, SEED); + fund_account::(&signer1, BalanceOf2::::from(100000u128)); + fund_account::(&signer2, BalanceOf2::::from(100000u128)); + + let mut signers = vec![caller.clone(), signer1.clone(), signer2.clone()]; + let threshold = 2u32; + signers.sort(); + + // Create multisig directly in storage + let multisig_address = Multisig::::derive_multisig_address(&signers, 0); + let bounded_signers: BoundedSignersOf = signers.clone().try_into().unwrap(); + let multisig_data = MultisigDataOf:: { + signers: bounded_signers, + threshold, + nonce: 0, + proposal_nonce: e, // We'll insert e expired proposals + creator: caller.clone(), + deposit: T::MultisigDeposit::get(), + last_activity: frame_system::Pallet::::block_number(), + active_proposals: e, + proposals_per_signer: BoundedBTreeMap::new(), + }; + Multisigs::::insert(&multisig_address, multisig_data); + + // Insert e expired proposals (worst case for auto-cleanup) + let expired_block = 10u32.into(); + for i in 0..e { + let system_call = frame_system::Call::::remark { remark: vec![i as u8; 10] }; + let call = ::RuntimeCall::from(system_call); + let encoded_call = call.encode(); + let bounded_call: BoundedCallOf = encoded_call.try_into().unwrap(); + let bounded_approvals: BoundedApprovalsOf = vec![caller.clone()].try_into().unwrap(); + + let proposal_data = ProposalDataOf:: { + proposer: caller.clone(), + call: bounded_call, + expiry: expired_block, + approvals: bounded_approvals, + deposit: 10u32.into(), + status: ProposalStatus::Active, + }; + Proposals::::insert(&multisig_address, i, proposal_data); + } + + // Move past expiry so proposals are expired + frame_system::Pallet::::set_block_number(100u32.into()); + + // Create a new proposal (will auto-cleanup all e expired proposals) + let system_call = frame_system::Call::::remark { remark: vec![99u8; c as usize] }; + let call = ::RuntimeCall::from(system_call); + let encoded_call = call.encode(); + let expiry = frame_system::Pallet::::block_number() + 1000u32.into(); + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), multisig_address.clone(), encoded_call, expiry); + + // Verify new proposal was created and expired ones were cleaned + let multisig = Multisigs::::get(&multisig_address).unwrap(); + assert_eq!(multisig.active_proposals, 1); // Only new proposal remains + + Ok(()) + } + + #[benchmark] + fn approve( + c: Linear<0, { T::MaxCallSize::get().saturating_sub(100) }>, + ) -> Result<(), BenchmarkError> { + // Setup: Create multisig and proposal directly in storage + // Threshold is 3, so adding one more approval won't trigger execution + let caller: T::AccountId = whitelisted_caller(); + fund_account::(&caller, BalanceOf2::::from(10000u128)); + + let signer1: T::AccountId = benchmark_account("signer1", 0, SEED); + let signer2: T::AccountId = benchmark_account("signer2", 1, SEED); + let signer3: T::AccountId = benchmark_account("signer3", 2, SEED); + fund_account::(&signer1, BalanceOf2::::from(10000u128)); + fund_account::(&signer2, BalanceOf2::::from(10000u128)); + fund_account::(&signer3, BalanceOf2::::from(10000u128)); + + let mut signers = vec![caller.clone(), signer1.clone(), signer2.clone(), signer3.clone()]; + let threshold = 3u32; // Need 3 approvals + + // Sort signers to match create_multisig behavior + signers.sort(); + + // Directly insert multisig into storage + let multisig_address = Multisig::::derive_multisig_address(&signers, 0); + let bounded_signers: BoundedSignersOf = signers.clone().try_into().unwrap(); + let multisig_data = MultisigDataOf:: { + signers: bounded_signers, + threshold, + nonce: 0, + proposal_nonce: 1, // We'll insert proposal with id 0 + creator: caller.clone(), + deposit: T::MultisigDeposit::get(), + last_activity: frame_system::Pallet::::block_number(), + active_proposals: 1, + proposals_per_signer: BoundedBTreeMap::new(), + }; + Multisigs::::insert(&multisig_address, multisig_data); + + // Directly insert proposal into storage with 1 approval + // Create a remark call where the remark itself is c bytes + let system_call = frame_system::Call::::remark { remark: vec![1u8; c as usize] }; + let call = ::RuntimeCall::from(system_call); + let encoded_call = call.encode(); + let expiry = frame_system::Pallet::::block_number() + 1000u32.into(); + let bounded_call: BoundedCallOf = encoded_call.clone().try_into().unwrap(); + let bounded_approvals: BoundedApprovalsOf = vec![caller.clone()].try_into().unwrap(); + + let proposal_data = ProposalDataOf:: { + proposer: caller.clone(), + call: bounded_call, + expiry, + approvals: bounded_approvals, + deposit: 10u32.into(), + status: ProposalStatus::Active, + }; + + let proposal_id = 0u32; + Proposals::::insert(&multisig_address, proposal_id, proposal_data); + + #[extrinsic_call] + _(RawOrigin::Signed(signer1.clone()), multisig_address.clone(), proposal_id); + + // Verify approval was added (now 2/3, not executed yet) + let proposal = Proposals::::get(&multisig_address, proposal_id).unwrap(); + assert!(proposal.approvals.contains(&signer1)); + assert_eq!(proposal.approvals.len(), 2); + + Ok(()) + } + + #[benchmark] + fn approve_and_execute( + c: Linear<0, { T::MaxCallSize::get().saturating_sub(100) }>, + ) -> Result<(), BenchmarkError> { + // Benchmarks approve() when it triggers auto-execution (threshold reached) + let caller: T::AccountId = whitelisted_caller(); + fund_account::(&caller, BalanceOf2::::from(10000u128)); + + let signer1: T::AccountId = benchmark_account("signer1", 0, SEED); + let signer2: T::AccountId = benchmark_account("signer2", 1, SEED); + fund_account::(&signer1, BalanceOf2::::from(10000u128)); + fund_account::(&signer2, BalanceOf2::::from(10000u128)); + + let mut signers = vec![caller.clone(), signer1.clone(), signer2.clone()]; + let threshold = 2u32; + + // Sort signers to match create_multisig behavior + signers.sort(); + + // Directly insert multisig into storage + let multisig_address = Multisig::::derive_multisig_address(&signers, 0); + let bounded_signers: BoundedSignersOf = signers.clone().try_into().unwrap(); + let multisig_data = MultisigDataOf:: { + signers: bounded_signers, + threshold, + nonce: 0, + proposal_nonce: 1, // We'll insert proposal with id 0 + creator: caller.clone(), + deposit: T::MultisigDeposit::get(), + last_activity: frame_system::Pallet::::block_number(), + active_proposals: 1, + proposals_per_signer: BoundedBTreeMap::new(), + }; + Multisigs::::insert(&multisig_address, multisig_data); + + // Directly insert proposal with 1 approval (caller already approved) + // signer2 will approve and trigger execution + // Create a remark call where the remark itself is c bytes + let system_call = frame_system::Call::::remark { remark: vec![1u8; c as usize] }; + let call = ::RuntimeCall::from(system_call); + let encoded_call = call.encode(); + let expiry = frame_system::Pallet::::block_number() + 1000u32.into(); + let bounded_call: BoundedCallOf = encoded_call.clone().try_into().unwrap(); + // Only 1 approval so far + let bounded_approvals: BoundedApprovalsOf = vec![caller.clone()].try_into().unwrap(); + + let proposal_data = ProposalDataOf:: { + proposer: caller.clone(), + call: bounded_call, + expiry, + approvals: bounded_approvals, + deposit: 10u32.into(), + status: ProposalStatus::Active, + }; + + let proposal_id = 0u32; + Proposals::::insert(&multisig_address, proposal_id, proposal_data); + + // signer2 approves, reaching threshold (2/2), triggering auto-execution + #[extrinsic_call] + approve(RawOrigin::Signed(signer2.clone()), multisig_address.clone(), proposal_id); + + // Verify proposal was removed from storage (auto-deleted after execution) + assert!(!Proposals::::contains_key(&multisig_address, proposal_id)); + + Ok(()) + } + + #[benchmark] + fn cancel( + c: Linear<0, { T::MaxCallSize::get().saturating_sub(100) }>, + ) -> Result<(), BenchmarkError> { + // Setup: Create multisig and proposal directly in storage + let caller: T::AccountId = whitelisted_caller(); + fund_account::(&caller, BalanceOf2::::from(10000u128)); + + let signer1: T::AccountId = benchmark_account("signer1", 0, SEED); + let signer2: T::AccountId = benchmark_account("signer2", 1, SEED); + fund_account::(&signer1, BalanceOf2::::from(10000u128)); + fund_account::(&signer2, BalanceOf2::::from(10000u128)); + + let mut signers = vec![caller.clone(), signer1.clone(), signer2.clone()]; + let threshold = 2u32; + + // Sort signers to match create_multisig behavior + signers.sort(); + + // Directly insert multisig into storage + let multisig_address = Multisig::::derive_multisig_address(&signers, 0); + let bounded_signers: BoundedSignersOf = signers.clone().try_into().unwrap(); + let multisig_data = MultisigDataOf:: { + signers: bounded_signers, + threshold, + nonce: 0, + proposal_nonce: 1, // We'll insert proposal with id 0 + creator: caller.clone(), + deposit: T::MultisigDeposit::get(), + last_activity: frame_system::Pallet::::block_number(), + active_proposals: 1, + proposals_per_signer: BoundedBTreeMap::new(), + }; + Multisigs::::insert(&multisig_address, multisig_data); + + // Directly insert proposal into storage + // Create a remark call where the remark itself is c bytes + let system_call = frame_system::Call::::remark { remark: vec![1u8; c as usize] }; + let call = ::RuntimeCall::from(system_call); + let encoded_call = call.encode(); + let expiry = frame_system::Pallet::::block_number() + 1000u32.into(); + let bounded_call: BoundedCallOf = encoded_call.clone().try_into().unwrap(); + let bounded_approvals: BoundedApprovalsOf = vec![caller.clone()].try_into().unwrap(); + + let proposal_data = ProposalDataOf:: { + proposer: caller.clone(), + call: bounded_call, + expiry, + approvals: bounded_approvals, + deposit: 10u32.into(), + status: ProposalStatus::Active, + }; + + let proposal_id = 0u32; + Proposals::::insert(&multisig_address, proposal_id, proposal_data); + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), multisig_address.clone(), proposal_id); + + // Verify proposal was removed from storage (auto-deleted after cancellation) + assert!(!Proposals::::contains_key(&multisig_address, proposal_id)); + + Ok(()) + } + + #[benchmark] + fn remove_expired() -> Result<(), BenchmarkError> { + // Setup: Create multisig and expired proposal directly in storage + let caller: T::AccountId = whitelisted_caller(); + fund_account::(&caller, BalanceOf2::::from(10000u128)); + + let signer1: T::AccountId = benchmark_account("signer1", 0, SEED); + let signer2: T::AccountId = benchmark_account("signer2", 1, SEED); + fund_account::(&signer1, BalanceOf2::::from(10000u128)); + fund_account::(&signer2, BalanceOf2::::from(10000u128)); + + let mut signers = vec![caller.clone(), signer1.clone(), signer2.clone()]; + let threshold = 2u32; + + // Sort signers to match create_multisig behavior + signers.sort(); + + // Directly insert multisig into storage + let multisig_address = Multisig::::derive_multisig_address(&signers, 0); + let bounded_signers: BoundedSignersOf = signers.clone().try_into().unwrap(); + let multisig_data = MultisigDataOf:: { + signers: bounded_signers, + threshold, + nonce: 0, + proposal_nonce: 1, // We'll insert proposal with id 0 + creator: caller.clone(), + deposit: T::MultisigDeposit::get(), + last_activity: 1u32.into(), + active_proposals: 1, + proposals_per_signer: BoundedBTreeMap::new(), + }; + Multisigs::::insert(&multisig_address, multisig_data); + + // Create proposal with expired timestamp + let system_call = frame_system::Call::::remark { remark: vec![1u8; 32] }; + let call = ::RuntimeCall::from(system_call); + let encoded_call = call.encode(); + let expiry = 10u32.into(); // Already expired + let bounded_call: BoundedCallOf = encoded_call.clone().try_into().unwrap(); + let bounded_approvals: BoundedApprovalsOf = vec![caller.clone()].try_into().unwrap(); + + let proposal_data = ProposalDataOf:: { + proposer: caller.clone(), + call: bounded_call, + expiry, + approvals: bounded_approvals, + deposit: 10u32.into(), + status: ProposalStatus::Active, + }; + + let proposal_id = 0u32; + Proposals::::insert(&multisig_address, proposal_id, proposal_data); + + // Move past expiry + frame_system::Pallet::::set_block_number(100u32.into()); + + // Call as signer (caller is one of signers) + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), multisig_address.clone(), proposal_id); + + // Verify proposal was removed + assert!(!Proposals::::contains_key(&multisig_address, proposal_id)); + + Ok(()) + } + + #[benchmark] + fn claim_deposits( + p: Linear<1, { T::MaxTotalProposalsInStorage::get() }>, /* number of expired proposals + * to cleanup */ + ) -> Result<(), BenchmarkError> { + // Setup: Create multisig with multiple expired proposals directly in storage + let caller: T::AccountId = whitelisted_caller(); + fund_account::(&caller, BalanceOf2::::from(100000u128)); + + let signer1: T::AccountId = benchmark_account("signer1", 0, SEED); + let signer2: T::AccountId = benchmark_account("signer2", 1, SEED); + fund_account::(&signer1, BalanceOf2::::from(10000u128)); + fund_account::(&signer2, BalanceOf2::::from(10000u128)); + + let mut signers = vec![caller.clone(), signer1.clone(), signer2.clone()]; + let threshold = 2u32; + + // Sort signers to match create_multisig behavior + signers.sort(); + + // Directly insert multisig into storage + let multisig_address = Multisig::::derive_multisig_address(&signers, 0); + let bounded_signers: BoundedSignersOf = signers.clone().try_into().unwrap(); + let multisig_data = MultisigDataOf:: { + signers: bounded_signers, + threshold, + nonce: 0, + proposal_nonce: p, // We'll insert p proposals with ids 0..p-1 + creator: caller.clone(), + deposit: T::MultisigDeposit::get(), + last_activity: 1u32.into(), + active_proposals: p, + proposals_per_signer: BoundedBTreeMap::new(), + }; + Multisigs::::insert(&multisig_address, multisig_data); + + // Create multiple expired proposals directly in storage + let expiry = 10u32.into(); // Already expired + + for i in 0..p { + let system_call = frame_system::Call::::remark { remark: vec![i as u8; 32] }; + let call = ::RuntimeCall::from(system_call); + let encoded_call = call.encode(); + let bounded_call: BoundedCallOf = encoded_call.clone().try_into().unwrap(); + let bounded_approvals: BoundedApprovalsOf = vec![caller.clone()].try_into().unwrap(); + + let proposal_data = ProposalDataOf:: { + proposer: caller.clone(), + call: bounded_call, + expiry, + approvals: bounded_approvals, + deposit: 10u32.into(), + status: ProposalStatus::Active, + }; + + Proposals::::insert(&multisig_address, i, proposal_data); + } + + // Move past expiry + frame_system::Pallet::::set_block_number(100u32.into()); + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), multisig_address.clone()); + + // Verify all expired proposals were cleaned up + assert_eq!(Proposals::::iter_key_prefix(&multisig_address).count(), 0); + + Ok(()) + } + + #[benchmark] + fn dissolve_multisig() -> Result<(), BenchmarkError> { + // Setup: Create a clean multisig (no proposals, zero balance) + let caller: T::AccountId = whitelisted_caller(); + fund_account::(&caller, BalanceOf2::::from(10000u128)); + + let signer1: T::AccountId = benchmark_account("signer1", 0, SEED); + let signer2: T::AccountId = benchmark_account("signer2", 1, SEED); + + let mut signers = vec![caller.clone(), signer1.clone(), signer2.clone()]; + let threshold = 2u32; + + // Sort signers to match create_multisig behavior + signers.sort(); + + // Directly insert multisig into storage + let multisig_address = Multisig::::derive_multisig_address(&signers, 0); + let bounded_signers: BoundedSignersOf = signers.clone().try_into().unwrap(); + let deposit = T::MultisigDeposit::get(); + + // Reserve deposit from caller + T::Currency::reserve(&caller, deposit)?; + + let multisig_data = MultisigDataOf:: { + signers: bounded_signers, + threshold, + nonce: 0, + proposal_nonce: 0, + creator: caller.clone(), + deposit, + last_activity: frame_system::Pallet::::block_number(), + active_proposals: 0, // No proposals + proposals_per_signer: BoundedBTreeMap::new(), + }; + Multisigs::::insert(&multisig_address, multisig_data); + + // Ensure multisig address has zero balance (required for dissolution) + // Don't fund it at all + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), multisig_address.clone()); + + // Verify multisig was removed + assert!(!Multisigs::::contains_key(&multisig_address)); + + Ok(()) + } + + impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test); +} diff --git a/pallets/multisig/src/lib.rs b/pallets/multisig/src/lib.rs new file mode 100644 index 00000000..2ac3fda1 --- /dev/null +++ b/pallets/multisig/src/lib.rs @@ -0,0 +1,1110 @@ +//! # Quantus Multisig Pallet +//! +//! This pallet provides multisignature functionality for managing shared accounts +//! that require multiple approvals before executing transactions. +//! +//! ## Features +//! +//! - Create multisig addresses with configurable thresholds +//! - Propose transactions for multisig approval +//! - Approve proposed transactions +//! - Execute transactions once threshold is reached +//! +//! ## Data Structures +//! +//! - **Multisig**: Contains signers, threshold, and global nonce +//! - **Proposal**: Contains transaction data, proposer, expiry, and approvals + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; +use alloc::vec::Vec; +pub use pallet::*; +pub use weights::*; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +pub mod weights; + +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::{traits::Get, BoundedBTreeMap, BoundedVec}; +use scale_info::TypeInfo; +use sp_runtime::RuntimeDebug; + +/// Multisig account data +#[derive(Encode, Decode, MaxEncodedLen, Clone, TypeInfo, RuntimeDebug, PartialEq, Eq)] +pub struct MultisigData +{ + /// List of signers who can approve transactions + pub signers: BoundedSigners, + /// Number of approvals required to execute a transaction + pub threshold: u32, + /// Global unique identifier for this multisig (for address derivation) + pub nonce: u64, + /// Proposal counter for unique proposal hashes + pub proposal_nonce: u32, + /// Account that created this multisig + pub creator: AccountId, + /// Deposit reserved by the creator + pub deposit: Balance, + /// Last block when this multisig was used + pub last_activity: BlockNumber, + /// Number of currently active (non-executed/non-cancelled) proposals + pub active_proposals: u32, + /// Counter of proposals in storage per signer (for filibuster protection) + pub proposals_per_signer: BoundedProposalsPerSigner, +} + +impl< + BlockNumber: Default, + AccountId: Default, + BoundedSigners: Default, + Balance: Default, + BoundedProposalsPerSigner: Default, + > Default + for MultisigData +{ + fn default() -> Self { + Self { + signers: Default::default(), + threshold: 1, + nonce: 0, + proposal_nonce: 0, + creator: Default::default(), + deposit: Default::default(), + last_activity: Default::default(), + active_proposals: 0, + proposals_per_signer: Default::default(), + } + } +} + +/// Proposal status +#[derive(Encode, Decode, MaxEncodedLen, Clone, TypeInfo, RuntimeDebug, PartialEq, Eq)] +pub enum ProposalStatus { + /// Proposal is active and awaiting approvals + Active, + /// Proposal was executed successfully + Executed, + /// Proposal was cancelled by proposer + Cancelled, +} + +/// Proposal data +#[derive(Encode, Decode, MaxEncodedLen, Clone, TypeInfo, RuntimeDebug, PartialEq, Eq)] +pub struct ProposalData { + /// Account that proposed this transaction + pub proposer: AccountId, + /// The encoded call to be executed + pub call: BoundedCall, + /// Expiry block number + pub expiry: BlockNumber, + /// List of accounts that have approved this proposal + pub approvals: BoundedApprovals, + /// Deposit held for this proposal (returned only when proposal is removed) + pub deposit: Balance, + /// Current status of the proposal + pub status: ProposalStatus, +} + +/// Balance type +type BalanceOf = <::Currency as frame_support::traits::Currency< + ::AccountId, +>>::Balance; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use codec::Encode; + use frame_support::{ + dispatch::{ + DispatchResult, DispatchResultWithPostInfo, GetDispatchInfo, Pays, PostDispatchInfo, + }, + pallet_prelude::*, + traits::{Currency, ReservableCurrency}, + PalletId, + }; + use frame_system::pallet_prelude::*; + use sp_arithmetic::traits::Saturating; + use sp_runtime::{ + traits::{Dispatchable, Hash, TrailingZeroInput}, + Permill, + }; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config>> { + /// The overarching call type + type RuntimeCall: Parameter + + Dispatchable + + GetDispatchInfo + + From> + + codec::Decode; + + /// Currency type for handling deposits + type Currency: Currency + ReservableCurrency; + + /// Maximum number of signers allowed in a multisig + #[pallet::constant] + type MaxSigners: Get; + + /// Maximum total number of proposals in storage per multisig (Active + Executed + + /// Cancelled) This prevents unbounded storage growth and incentivizes cleanup + #[pallet::constant] + type MaxTotalProposalsInStorage: Get; + + /// Maximum size of an encoded call + #[pallet::constant] + type MaxCallSize: Get; + + /// Fee charged for creating a multisig (non-refundable, burned) + #[pallet::constant] + type MultisigFee: Get>; + + /// Deposit reserved for creating a multisig (returned when dissolved). + /// Keeps the state clean by incentivizing removal of unused multisigs. + #[pallet::constant] + type MultisigDeposit: Get>; + + /// Deposit required per proposal (returned on execute or cancel) + #[pallet::constant] + type ProposalDeposit: Get>; + + /// Fee charged for creating a proposal (non-refundable, paid always) + #[pallet::constant] + type ProposalFee: Get>; + + /// Percentage increase in ProposalFee for each signer in the multisig. + /// + /// Formula: `FinalFee = ProposalFee + (ProposalFee * SignerCount * SignerStepFactor)` + /// Example: If Fee=100, Signers=5, Factor=1%, then Extra = 100 * 5 * 0.01 = 5. Total = 105. + #[pallet::constant] + type SignerStepFactor: Get; + + /// Pallet ID for generating multisig addresses + #[pallet::constant] + type PalletId: Get; + + /// Maximum duration (in blocks) that a proposal can be set to expire in the future. + /// This prevents proposals from being created with extremely far expiry dates + /// that would lock deposits and bloat storage for extended periods. + /// + /// Example: If set to 100_000 blocks (~2 weeks at 12s blocks), + /// a proposal created at block 1000 cannot have expiry > 101_000. + #[pallet::constant] + type MaxExpiryDuration: Get>; + + /// Weight information for extrinsics + type WeightInfo: WeightInfo; + } + + /// Type alias for bounded signers vector + pub type BoundedSignersOf = + BoundedVec<::AccountId, ::MaxSigners>; + + /// Type alias for bounded approvals vector + pub type BoundedApprovalsOf = + BoundedVec<::AccountId, ::MaxSigners>; + + /// Type alias for bounded call data + pub type BoundedCallOf = BoundedVec::MaxCallSize>; + + /// Type alias for bounded proposals per signer map + pub type BoundedProposalsPerSignerOf = + BoundedBTreeMap<::AccountId, u32, ::MaxSigners>; + + /// Type alias for MultisigData with proper bounds + pub type MultisigDataOf = MultisigData< + BlockNumberFor, + ::AccountId, + BoundedSignersOf, + BalanceOf, + BoundedProposalsPerSignerOf, + >; + + /// Type alias for ProposalData with proper bounds + pub type ProposalDataOf = ProposalData< + ::AccountId, + BalanceOf, + BlockNumberFor, + BoundedCallOf, + BoundedApprovalsOf, + >; + + /// Global nonce for generating unique multisig addresses + #[pallet::storage] + pub type GlobalNonce = StorageValue<_, u64, ValueQuery>; + + /// Multisigs stored by their generated address + #[pallet::storage] + #[pallet::getter(fn multisigs)] + pub type Multisigs = + StorageMap<_, Blake2_128Concat, T::AccountId, MultisigDataOf, OptionQuery>; + + /// Proposals indexed by (multisig_address, proposal_nonce) + #[pallet::storage] + #[pallet::getter(fn proposals)] + pub type Proposals = StorageDoubleMap< + _, + Blake2_128Concat, + T::AccountId, + Twox64Concat, + u32, + ProposalDataOf, + OptionQuery, + >; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A new multisig account was created + /// [creator, multisig_address, signers, threshold, nonce] + MultisigCreated { + creator: T::AccountId, + multisig_address: T::AccountId, + signers: Vec, + threshold: u32, + nonce: u64, + }, + /// A proposal has been created + ProposalCreated { multisig_address: T::AccountId, proposer: T::AccountId, proposal_id: u32 }, + /// A proposal has been approved by a signer + ProposalApproved { + multisig_address: T::AccountId, + approver: T::AccountId, + proposal_id: u32, + approvals_count: u32, + }, + /// A proposal has been executed + /// Contains all data needed for indexing by SubSquid + ProposalExecuted { + multisig_address: T::AccountId, + proposal_id: u32, + proposer: T::AccountId, + call: Vec, + approvers: Vec, + result: DispatchResult, + }, + /// A proposal has been cancelled by the proposer + ProposalCancelled { + multisig_address: T::AccountId, + proposer: T::AccountId, + proposal_id: u32, + }, + /// Expired proposal was removed from storage + ProposalRemoved { + multisig_address: T::AccountId, + proposal_id: u32, + proposer: T::AccountId, + removed_by: T::AccountId, + }, + /// Batch deposits claimed + DepositsClaimed { + multisig_address: T::AccountId, + claimer: T::AccountId, + total_returned: BalanceOf, + proposals_removed: u32, + multisig_removed: bool, + }, + /// A multisig account was dissolved and deposit returned + MultisigDissolved { + multisig_address: T::AccountId, + caller: T::AccountId, + deposit_returned: BalanceOf, + }, + } + + #[pallet::error] + pub enum Error { + /// Not enough signers provided + NotEnoughSigners, + /// Threshold must be greater than zero + ThresholdZero, + /// Threshold exceeds number of signers + ThresholdTooHigh, + /// Too many signers + TooManySigners, + /// Duplicate signer in list + DuplicateSigner, + /// Multisig already exists + MultisigAlreadyExists, + /// Multisig not found + MultisigNotFound, + /// Caller is not a signer of this multisig + NotASigner, + /// Proposal not found + ProposalNotFound, + /// Caller is not the proposer + NotProposer, + /// Already approved by this signer + AlreadyApproved, + /// Not enough approvals to execute + NotEnoughApprovals, + /// Proposal expiry is in the past + ExpiryInPast, + /// Proposal expiry is too far in the future (exceeds MaxExpiryDuration) + ExpiryTooFar, + /// Proposal has expired + ProposalExpired, + /// Call data too large + CallTooLarge, + /// Failed to decode call data + InvalidCall, + /// Too many total proposals in storage for this multisig (cleanup required) + TooManyProposalsInStorage, + /// This signer has too many proposals in storage (filibuster protection) + TooManyProposalsPerSigner, + /// Insufficient balance for deposit + InsufficientBalance, + /// Proposal has active deposit + ProposalHasDeposit, + /// Proposal has not expired yet + ProposalNotExpired, + /// Proposal is not active (already executed or cancelled) + ProposalNotActive, + /// Cannot dissolve multisig with existing proposals (clear them first) + ProposalsExist, + /// Multisig account must have zero balance before dissolution + MultisigAccountNotZero, + } + + #[pallet::call] + impl Pallet { + /// Create a new multisig account + /// + /// Parameters: + /// - `signers`: List of accounts that can sign for this multisig + /// - `threshold`: Number of approvals required to execute transactions + /// + /// The multisig address is derived from a hash of all signers + global nonce. + /// The creator must pay a non-refundable fee (burned). + #[pallet::call_index(0)] + #[pallet::weight(::WeightInfo::create_multisig())] + pub fn create_multisig( + origin: OriginFor, + signers: Vec, + threshold: u32, + ) -> DispatchResult { + let creator = ensure_signed(origin)?; + + // Validate inputs + ensure!(threshold > 0, Error::::ThresholdZero); + ensure!(!signers.is_empty(), Error::::NotEnoughSigners); + ensure!(threshold <= signers.len() as u32, Error::::ThresholdTooHigh); + ensure!(signers.len() <= T::MaxSigners::get() as usize, Error::::TooManySigners); + + // Sort signers for deterministic address generation + // (order shouldn't matter - nonce provides uniqueness) + let mut sorted_signers = signers.clone(); + sorted_signers.sort(); + + // Check for duplicate signers + for i in 1..sorted_signers.len() { + ensure!(sorted_signers[i] != sorted_signers[i - 1], Error::::DuplicateSigner); + } + + // Get and increment global nonce + let nonce = GlobalNonce::::get(); + GlobalNonce::::put(nonce.saturating_add(1)); + + // Generate multisig address from hash of (sorted_signers, nonce) + let multisig_address = Self::derive_multisig_address(&sorted_signers, nonce); + + // Ensure multisig doesn't already exist + ensure!( + !Multisigs::::contains_key(&multisig_address), + Error::::MultisigAlreadyExists + ); + + // Charge non-refundable fee (burned) + let fee = T::MultisigFee::get(); + let _ = T::Currency::withdraw( + &creator, + fee, + frame_support::traits::WithdrawReasons::FEE, + frame_support::traits::ExistenceRequirement::KeepAlive, + ) + .map_err(|_| Error::::InsufficientBalance)?; + + // Reserve deposit from creator (will be returned on dissolve) + let deposit = T::MultisigDeposit::get(); + T::Currency::reserve(&creator, deposit).map_err(|_| Error::::InsufficientBalance)?; + + // Convert sorted signers to bounded vec + let bounded_signers: BoundedSignersOf = + sorted_signers.try_into().map_err(|_| Error::::TooManySigners)?; + + // Get current block for last_activity + let current_block = frame_system::Pallet::::block_number(); + + // Store multisig data + Multisigs::::insert( + &multisig_address, + MultisigDataOf:: { + signers: bounded_signers.clone(), + threshold, + nonce, + proposal_nonce: 0, + creator: creator.clone(), + deposit, + last_activity: current_block, + active_proposals: 0, + proposals_per_signer: Default::default(), + }, + ); + + // Emit event with sorted signers + Self::deposit_event(Event::MultisigCreated { + creator, + multisig_address, + signers: bounded_signers.to_vec(), + threshold, + nonce, + }); + + Ok(()) + } + + /// Propose a transaction to be executed by the multisig + /// + /// Parameters: + /// - `multisig_address`: The multisig account that will execute the call + /// - `call`: The encoded call to execute + /// - `expiry`: Block number when this proposal expires + /// + /// The proposer must be a signer and must pay: + /// - A deposit (locked until proposal is removed after grace period) + /// - A fee (non-refundable, burned immediately) + /// + /// The proposal remains in storage even after execution/cancellation. + /// Use `remove_expired()` or `claim_deposits()` after grace period to recover the deposit. + #[pallet::call_index(1)] + #[pallet::weight(::WeightInfo::propose( + call.len() as u32, + T::MaxTotalProposalsInStorage::get() + ))] + pub fn propose( + origin: OriginFor, + multisig_address: T::AccountId, + call: Vec, + expiry: BlockNumberFor, + ) -> DispatchResult { + let proposer = ensure_signed(origin)?; + + // Check if proposer is a signer and active proposals limit + let multisig_data = + Multisigs::::get(&multisig_address).ok_or(Error::::MultisigNotFound)?; + ensure!(multisig_data.signers.contains(&proposer), Error::::NotASigner); + + // Auto-cleanup expired proposals before creating new one + // This ensures storage is managed proactively during normal operation + let current_block = frame_system::Pallet::::block_number(); + let expired_proposals: Vec<(u32, T::AccountId, BalanceOf)> = + Proposals::::iter_prefix(&multisig_address) + .filter_map(|(id, proposal)| { + if proposal.status == ProposalStatus::Active && + current_block > proposal.expiry + { + Some((id, proposal.proposer, proposal.deposit)) + } else { + None + } + }) + .collect(); + + // Remove expired proposals and return deposits + for (id, expired_proposer, deposit) in expired_proposals.iter() { + Self::remove_proposal_and_return_deposit( + &multisig_address, + *id, + expired_proposer, + *deposit, + ); + + // Emit event for each removed proposal + Self::deposit_event(Event::ProposalRemoved { + multisig_address: multisig_address.clone(), + proposal_id: *id, + proposer: expired_proposer.clone(), + removed_by: proposer.clone(), + }); + } + + // Reload multisig data after potential cleanup + let multisig_data = + Multisigs::::get(&multisig_address).ok_or(Error::::MultisigNotFound)?; + + // Get signers count (used for multiple checks below) + let signers_count = multisig_data.signers.len() as u32; + + // Check total proposals in storage limit (Active + Executed + Cancelled) + // This incentivizes cleanup and prevents unbounded storage growth + let total_proposals_in_storage = + Proposals::::iter_prefix(&multisig_address).count() as u32; + ensure!( + total_proposals_in_storage < T::MaxTotalProposalsInStorage::get(), + Error::::TooManyProposalsInStorage + ); + + // Check per-signer proposal limit (filibuster protection) + // Each signer can have at most (MaxTotal / NumSigners) proposals in storage + // This prevents a single signer from monopolizing the proposal queue + // Use saturating_div to handle edge cases (division by 0, etc.) and ensure at least 1 + let max_per_signer = T::MaxTotalProposalsInStorage::get() + .checked_div(signers_count) + .unwrap_or(1) // If division fails (shouldn't happen), allow at least 1 + .max(1); // Ensure minimum of 1 proposal per signer + let proposer_count = + multisig_data.proposals_per_signer.get(&proposer).copied().unwrap_or(0); + ensure!(proposer_count < max_per_signer, Error::::TooManyProposalsPerSigner); + + // Check call size + ensure!(call.len() as u32 <= T::MaxCallSize::get(), Error::::CallTooLarge); + + // Validate expiry is in the future + ensure!(expiry > current_block, Error::::ExpiryInPast); + + // Validate expiry is not too far in the future + let max_expiry = current_block.saturating_add(T::MaxExpiryDuration::get()); + ensure!(expiry <= max_expiry, Error::::ExpiryTooFar); + + // Calculate dynamic fee based on number of signers + // Fee = Base + (Base * SignerCount * StepFactor) + let base_fee = T::ProposalFee::get(); + let step_factor = T::SignerStepFactor::get(); + + // Calculate extra fee: (Base * Factor) * Count + // mul_floor returns the part of the fee corresponding to the percentage + let fee_increase_per_signer = step_factor.mul_floor(base_fee); + let total_increase = fee_increase_per_signer.saturating_mul(signers_count.into()); + let fee = base_fee.saturating_add(total_increase); + + // Charge non-refundable fee (burned) + let _ = T::Currency::withdraw( + &proposer, + fee, + frame_support::traits::WithdrawReasons::FEE, + frame_support::traits::ExistenceRequirement::KeepAlive, + ) + .map_err(|_| Error::::InsufficientBalance)?; + + // Reserve deposit from proposer (will be returned) + let deposit = T::ProposalDeposit::get(); + T::Currency::reserve(&proposer, deposit) + .map_err(|_| Error::::InsufficientBalance)?; + + // Update multisig last_activity + Multisigs::::mutate(&multisig_address, |maybe_multisig| { + if let Some(multisig) = maybe_multisig { + multisig.last_activity = current_block; + } + }); + + // Convert to bounded vec + let bounded_call: BoundedCallOf = + call.try_into().map_err(|_| Error::::CallTooLarge)?; + + // Get and increment proposal nonce for unique ID + let proposal_id = Multisigs::::mutate(&multisig_address, |maybe_multisig| { + if let Some(multisig) = maybe_multisig { + let nonce = multisig.proposal_nonce; + multisig.proposal_nonce = multisig.proposal_nonce.saturating_add(1); + nonce + } else { + 0 // Should never happen due to earlier check + } + }); + + // Create proposal with proposer as first approval + let mut approvals = BoundedApprovalsOf::::default(); + let _ = approvals.try_push(proposer.clone()); + + let proposal = ProposalData { + proposer: proposer.clone(), + call: bounded_call, + expiry, + approvals, + deposit, + status: ProposalStatus::Active, + }; + + // Store proposal with nonce as key (simple and efficient) + Proposals::::insert(&multisig_address, proposal_id, proposal); + + // Increment active proposals counter and per-signer counter + Multisigs::::mutate(&multisig_address, |maybe_multisig| { + if let Some(multisig) = maybe_multisig { + multisig.active_proposals = multisig.active_proposals.saturating_add(1); + + // Update per-signer counter for filibuster protection + let current_count = + multisig.proposals_per_signer.get(&proposer).copied().unwrap_or(0); + let _ = multisig + .proposals_per_signer + .try_insert(proposer.clone(), current_count.saturating_add(1)); + } + }); + + // Emit event + Self::deposit_event(Event::ProposalCreated { + multisig_address: multisig_address.clone(), + proposer, + proposal_id, + }); + + // Check if threshold is reached immediately (threshold=1 case) + // Proposer is already counted as first approval + if 1 >= multisig_data.threshold { + // Threshold reached - execute immediately + // Need to get proposal again since we inserted it + let proposal = Proposals::::get(&multisig_address, proposal_id) + .ok_or(Error::::ProposalNotFound)?; + Self::do_execute(multisig_address, proposal_id, proposal)?; + } + + Ok(()) + } + + /// Approve a proposed transaction + /// + /// If this approval brings the total approvals to or above the threshold, + /// the transaction will be automatically executed. + /// + /// Parameters: + /// - `multisig_address`: The multisig account + /// - `proposal_id`: ID (nonce) of the proposal to approve + /// + /// Weight: Charges for MAX call size, but refunds based on actual call size + #[pallet::call_index(2)] + #[pallet::weight(::WeightInfo::approve(T::MaxCallSize::get()))] + #[allow(clippy::useless_conversion)] + pub fn approve( + origin: OriginFor, + multisig_address: T::AccountId, + proposal_id: u32, + ) -> DispatchResultWithPostInfo { + let approver = ensure_signed(origin)?; + + // Check if approver is a signer + let multisig_data = Self::ensure_is_signer(&multisig_address, &approver)?; + + // Get proposal + let mut proposal = Proposals::::get(&multisig_address, proposal_id) + .ok_or(Error::::ProposalNotFound)?; + + // Calculate actual weight based on real call size (for refund) + let actual_call_size = proposal.call.len() as u32; + let actual_weight = ::WeightInfo::approve(actual_call_size); + + // Check if not expired + let current_block = frame_system::Pallet::::block_number(); + ensure!(current_block <= proposal.expiry, Error::::ProposalExpired); + + // Check if already approved + ensure!(!proposal.approvals.contains(&approver), Error::::AlreadyApproved); + + // Add approval + proposal + .approvals + .try_push(approver.clone()) + .map_err(|_| Error::::TooManySigners)?; + + let approvals_count = proposal.approvals.len() as u32; + + // Emit approval event + Self::deposit_event(Event::ProposalApproved { + multisig_address: multisig_address.clone(), + approver, + proposal_id, + approvals_count, + }); + + // Check if threshold is reached - if so, execute immediately + if approvals_count >= multisig_data.threshold { + // Execute the transaction + Self::do_execute(multisig_address, proposal_id, proposal)?; + } else { + // Not ready yet, just save the proposal + Proposals::::insert(&multisig_address, proposal_id, proposal); + + // Update multisig last_activity + Multisigs::::mutate(&multisig_address, |maybe_multisig| { + if let Some(multisig) = maybe_multisig { + multisig.last_activity = frame_system::Pallet::::block_number(); + } + }); + } + + // Return actual weight (refund overpayment) + Ok(PostDispatchInfo { actual_weight: Some(actual_weight), pays_fee: Pays::Yes }) + } + + /// Cancel a proposed transaction (only by proposer) + /// + /// Parameters: + /// - `multisig_address`: The multisig account + /// - `proposal_id`: ID (nonce) of the proposal to cancel + /// + /// Weight: Charges for MAX call size, but refunds based on actual call size + #[pallet::call_index(3)] + #[pallet::weight(::WeightInfo::cancel(T::MaxCallSize::get()))] + #[allow(clippy::useless_conversion)] + pub fn cancel( + origin: OriginFor, + multisig_address: T::AccountId, + proposal_id: u32, + ) -> DispatchResultWithPostInfo { + let canceller = ensure_signed(origin)?; + + // Get proposal + let proposal = Proposals::::get(&multisig_address, proposal_id) + .ok_or(Error::::ProposalNotFound)?; + + // Calculate actual weight based on real call size (for refund) + let actual_call_size = proposal.call.len() as u32; + let actual_weight = ::WeightInfo::cancel(actual_call_size); + + // Check if caller is the proposer + ensure!(canceller == proposal.proposer, Error::::NotProposer); + + // Check if proposal is still active + ensure!(proposal.status == ProposalStatus::Active, Error::::ProposalNotActive); + + // Remove proposal from storage and return deposit immediately + Self::remove_proposal_and_return_deposit( + &multisig_address, + proposal_id, + &proposal.proposer, + proposal.deposit, + ); + + // Emit event + Self::deposit_event(Event::ProposalCancelled { + multisig_address, + proposer: canceller, + proposal_id, + }); + + // Return actual weight (refund overpayment) + Ok(PostDispatchInfo { actual_weight: Some(actual_weight), pays_fee: Pays::Yes }) + } + + /// Remove expired proposals and return deposits to proposers + /// + /// Can only be called by signers of the multisig. + /// Only removes Active proposals that have expired (past expiry block). + /// Executed and Cancelled proposals are automatically cleaned up immediately. + /// + /// The deposit is always returned to the original proposer, not the caller. + /// This allows any signer to help clean up storage even if proposer is inactive. + #[pallet::call_index(4)] + #[pallet::weight(::WeightInfo::remove_expired())] + pub fn remove_expired( + origin: OriginFor, + multisig_address: T::AccountId, + proposal_id: u32, + ) -> DispatchResult { + let caller = ensure_signed(origin)?; + + // Verify caller is a signer + let _multisig_data = Self::ensure_is_signer(&multisig_address, &caller)?; + + // Get proposal + let proposal = Proposals::::get(&multisig_address, proposal_id) + .ok_or(Error::::ProposalNotFound)?; + + // Only Active proposals can be manually removed (Executed/Cancelled already + // auto-removed) + ensure!(proposal.status == ProposalStatus::Active, Error::::ProposalNotActive); + + // Check if expired + let current_block = frame_system::Pallet::::block_number(); + ensure!(current_block > proposal.expiry, Error::::ProposalNotExpired); + + // Remove proposal from storage and return deposit + Self::remove_proposal_and_return_deposit( + &multisig_address, + proposal_id, + &proposal.proposer, + proposal.deposit, + ); + + // Emit event + Self::deposit_event(Event::ProposalRemoved { + multisig_address, + proposal_id, + proposer: proposal.proposer.clone(), + removed_by: caller, + }); + + Ok(()) + } + + /// Claim all deposits from expired proposals + /// + /// This is a batch operation that removes all expired proposals where: + /// - Caller is the proposer + /// - Proposal is Active and past expiry block + /// + /// Note: Executed and Cancelled proposals are automatically cleaned up immediately, + /// so only Active+Expired proposals need manual cleanup. + /// + /// Returns all proposal deposits to the proposer in a single transaction. + #[pallet::call_index(5)] + #[pallet::weight(::WeightInfo::claim_deposits( + T::MaxTotalProposalsInStorage::get() + ))] + pub fn claim_deposits( + origin: OriginFor, + multisig_address: T::AccountId, + ) -> DispatchResult { + let caller = ensure_signed(origin)?; + + let current_block = frame_system::Pallet::::block_number(); + + let mut total_returned = BalanceOf::::zero(); + let mut removed_count = 0u32; + + // Iterate through all proposals for this multisig + // Only Active+Expired proposals exist (Executed/Cancelled are auto-removed) + let proposals_to_remove: Vec<(u32, ProposalDataOf)> = + Proposals::::iter_prefix(&multisig_address) + .filter(|(_, proposal)| { + // Only proposals where caller is proposer + if proposal.proposer != caller { + return false; + } + + // Only Active proposals can exist (Executed/Cancelled auto-removed) + // Must be expired to remove + proposal.status == ProposalStatus::Active && current_block > proposal.expiry + }) + .collect(); + + // Remove proposals and return deposits + for (id, proposal) in proposals_to_remove { + total_returned = total_returned.saturating_add(proposal.deposit); + removed_count = removed_count.saturating_add(1); + + // Remove from storage and return deposit + Self::remove_proposal_and_return_deposit( + &multisig_address, + id, + &proposal.proposer, + proposal.deposit, + ); + + // Emit event for each removed proposal + Self::deposit_event(Event::ProposalRemoved { + multisig_address: multisig_address.clone(), + proposal_id: id, + proposer: caller.clone(), + removed_by: caller.clone(), + }); + } + + // Emit summary event + Self::deposit_event(Event::DepositsClaimed { + multisig_address: multisig_address.clone(), + claimer: caller, + total_returned, + proposals_removed: removed_count, + multisig_removed: false, // Multisig is never auto-removed now + }); + + Ok(()) + } + + /// Dissolve (remove) a multisig and recover the creation deposit. + /// + /// Requirements: + /// - No proposals exist (active, executed, or cancelled) - must be fully cleaned up. + /// - Multisig account balance must be zero. + /// - Can be called by the creator OR any signer. + /// + /// The deposit is ALWAYS returned to the original `creator` stored in `MultisigData`. + #[pallet::call_index(6)] + #[pallet::weight(::WeightInfo::dissolve_multisig())] + pub fn dissolve_multisig( + origin: OriginFor, + multisig_address: T::AccountId, + ) -> DispatchResult { + let caller = ensure_signed(origin)?; + + // 1. Get multisig data + let multisig_data = + Multisigs::::get(&multisig_address).ok_or(Error::::MultisigNotFound)?; + + // 2. Check permissions: Creator OR Any Signer + let is_signer = multisig_data.signers.contains(&caller); + let is_creator = multisig_data.creator == caller; + ensure!(is_signer || is_creator, Error::::NotASigner); + + // 3. Check if account is clean (no proposals at all) + // iter_prefix is efficient enough here as we just need to check if ANY exist + if Proposals::::iter_prefix(&multisig_address).next().is_some() { + return Err(Error::::ProposalsExist.into()); + } + + // 4. Check if account balance is zero + let balance = T::Currency::total_balance(&multisig_address); + ensure!(balance.is_zero(), Error::::MultisigAccountNotZero); + + // 5. Return deposit to creator + T::Currency::unreserve(&multisig_data.creator, multisig_data.deposit); + + // 6. Remove multisig from storage + Multisigs::::remove(&multisig_address); + + // 7. Emit event + Self::deposit_event(Event::MultisigDissolved { + multisig_address, + caller, + deposit_returned: multisig_data.deposit, + }); + + Ok(()) + } + } + + impl Pallet { + /// Derive a multisig address from signers and nonce + pub fn derive_multisig_address(signers: &[T::AccountId], nonce: u64) -> T::AccountId { + // Create a unique identifier from pallet id + signers + nonce. + // + // IMPORTANT: + // - Do NOT `Decode` directly from a finite byte-slice and then "fallback" to a constant + // address on error: that can cause address collisions / DoS. + // - Using `TrailingZeroInput` makes decoding deterministic and infallible by providing + // an infinite stream (hash bytes padded with zeros). + let pallet_id = T::PalletId::get(); + let mut data = Vec::new(); + data.extend_from_slice(&pallet_id.0); + data.extend_from_slice(&signers.encode()); + data.extend_from_slice(&nonce.encode()); + + // Hash the data and map it deterministically into an AccountId. + let hash = T::Hashing::hash(&data); + T::AccountId::decode(&mut TrailingZeroInput::new(hash.as_ref())) + .expect("TrailingZeroInput provides sufficient bytes; qed") + } + + /// Check if an account is a signer for a given multisig + pub fn is_signer(multisig_address: &T::AccountId, account: &T::AccountId) -> bool { + if let Some(multisig_data) = Multisigs::::get(multisig_address) { + multisig_data.signers.contains(account) + } else { + false + } + } + + /// Ensure account is a signer, otherwise return error + /// Returns multisig data if successful + fn ensure_is_signer( + multisig_address: &T::AccountId, + account: &T::AccountId, + ) -> Result, DispatchError> { + let multisig_data = + Multisigs::::get(multisig_address).ok_or(Error::::MultisigNotFound)?; + ensure!(multisig_data.signers.contains(account), Error::::NotASigner); + Ok(multisig_data) + } + + /// Decrement proposal counters (active_proposals and per-signer counter) + /// Used when removing proposals from storage + fn decrement_proposal_counters(multisig_address: &T::AccountId, proposer: &T::AccountId) { + Multisigs::::mutate(multisig_address, |maybe_multisig| { + if let Some(multisig) = maybe_multisig { + multisig.active_proposals = multisig.active_proposals.saturating_sub(1); + + // Decrement per-signer counter + if let Some(count) = multisig.proposals_per_signer.get_mut(proposer) { + *count = count.saturating_sub(1); + if *count == 0 { + multisig.proposals_per_signer.remove(proposer); + } + } + } + }); + } + + /// Remove a proposal from storage and return deposit to proposer + /// Used for cleanup operations + fn remove_proposal_and_return_deposit( + multisig_address: &T::AccountId, + proposal_id: u32, + proposer: &T::AccountId, + deposit: BalanceOf, + ) { + // Remove from storage + Proposals::::remove(multisig_address, proposal_id); + + // Return deposit to proposer + T::Currency::unreserve(proposer, deposit); + + // Decrement counters + Self::decrement_proposal_counters(multisig_address, proposer); + } + + /// Internal function to execute a proposal + /// Called automatically from `approve()` when threshold is reached + /// + /// Removes the proposal immediately and returns deposit. + /// + /// This function is private and cannot be called from outside the pallet + /// + /// SECURITY: Uses Checks-Effects-Interactions pattern to prevent reentrancy attacks. + /// Storage is updated BEFORE dispatching the call. + fn do_execute( + multisig_address: T::AccountId, + proposal_id: u32, + proposal: ProposalDataOf, + ) -> DispatchResult { + // CHECKS: Decode the call (validation) + let call = ::RuntimeCall::decode(&mut &proposal.call[..]) + .map_err(|_| Error::::InvalidCall)?; + + // EFFECTS: Remove proposal from storage and return deposit BEFORE external interaction + // (reentrancy protection) + Self::remove_proposal_and_return_deposit( + &multisig_address, + proposal_id, + &proposal.proposer, + proposal.deposit, + ); + + // EFFECTS: Update multisig last_activity BEFORE external interaction + Multisigs::::mutate(&multisig_address, |maybe_multisig| { + if let Some(multisig) = maybe_multisig { + multisig.last_activity = frame_system::Pallet::::block_number(); + } + }); + + // INTERACTIONS: NOW execute the call as the multisig account + // Proposal already removed, so reentrancy cannot affect storage + let result = + call.dispatch(frame_system::RawOrigin::Signed(multisig_address.clone()).into()); + + // Emit event with all execution details for SubSquid indexing + Self::deposit_event(Event::ProposalExecuted { + multisig_address, + proposal_id, + proposer: proposal.proposer, + call: proposal.call.to_vec(), + approvers: proposal.approvals.to_vec(), + result: result.map(|_| ()).map_err(|e| e.error), + }); + + Ok(()) + } + } +} diff --git a/pallets/multisig/src/mock.rs b/pallets/multisig/src/mock.rs new file mode 100644 index 00000000..38f241d6 --- /dev/null +++ b/pallets/multisig/src/mock.rs @@ -0,0 +1,143 @@ +//! Mock runtime for testing pallet-multisig + +use crate as pallet_multisig; +use frame_support::{ + parameter_types, + traits::{ConstU32, Everything}, + PalletId, +}; +use sp_core::{crypto::AccountId32, H256}; +use sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, Permill, +}; + +type Block = frame_system::mocking::MockBlock; +type Balance = u128; + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system, + Balances: pallet_balances, + Multisig: pallet_multisig, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; +} + +impl frame_system::Config for Test { + type RuntimeEvent = RuntimeEvent; + type BaseCallFilter = Everything; + type Block = Block; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Nonce = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId32; + type Lookup = IdentityLookup; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; + type RuntimeTask = (); + type SingleBlockMigrations = (); + type MultiBlockMigrator = (); + type PreInherents = (); + type PostInherents = (); + type PostTransactions = (); + type ExtensionsWeightInfo = (); +} + +parameter_types! { + pub const ExistentialDeposit: Balance = 1; + pub const MaxLocks: u32 = 50; + pub const MaxReserves: u32 = 50; + pub const MaxFreezes: u32 = 50; + pub const MintingAccount: AccountId32 = AccountId32::new([99u8; 32]); +} + +impl pallet_balances::Config for Test { + type WeightInfo = (); + type Balance = Balance; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type MaxLocks = MaxLocks; + type MaxReserves = MaxReserves; + type ReserveIdentifier = [u8; 8]; + type RuntimeHoldReason = RuntimeHoldReason; + type RuntimeFreezeReason = RuntimeFreezeReason; + type FreezeIdentifier = (); + type MaxFreezes = MaxFreezes; + type DoneSlashHandler = (); + type MintingAccount = MintingAccount; +} + +parameter_types! { + pub const MultisigPalletId: PalletId = PalletId(*b"py/mltsg"); + pub const MaxSignersParam: u32 = 10; + pub const MaxTotalProposalsInStorageParam: u32 = 20; + pub const MaxCallSizeParam: u32 = 1024; + pub const MultisigFeeParam: Balance = 1000; // Non-refundable fee + pub const MultisigDepositParam: Balance = 500; // Refundable deposit + pub const ProposalDepositParam: Balance = 100; + pub const ProposalFeeParam: Balance = 1000; // Non-refundable fee + pub const SignerStepFactorParam: Permill = Permill::from_parts(10_000); // 1% + pub const MaxExpiryDurationParam: u64 = 10000; // 10000 blocks for testing (enough for all test scenarios) +} + +impl pallet_multisig::Config for Test { + type RuntimeCall = RuntimeCall; + type Currency = Balances; + type MaxSigners = MaxSignersParam; + type MaxTotalProposalsInStorage = MaxTotalProposalsInStorageParam; + type MaxCallSize = MaxCallSizeParam; + type MultisigFee = MultisigFeeParam; + type MultisigDeposit = MultisigDepositParam; + type ProposalDeposit = ProposalDepositParam; + type ProposalFee = ProposalFeeParam; + type SignerStepFactor = SignerStepFactorParam; + type MaxExpiryDuration = MaxExpiryDurationParam; + type PalletId = MultisigPalletId; + type WeightInfo = (); +} + +// Helper to create AccountId32 from u64 +pub fn account_id(id: u64) -> AccountId32 { + let mut data = [0u8; 32]; + data[0..8].copy_from_slice(&id.to_le_bytes()); + AccountId32::new(data) +} + +// Build genesis storage according to the mock runtime. +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + pallet_balances::GenesisConfig:: { + balances: vec![ + (account_id(1), 100000), // Alice + (account_id(2), 200000), // Bob + (account_id(3), 300000), // Charlie + (account_id(4), 400000), // Dave + (account_id(5), 500000), // Eve + ], + } + .assimilate_storage(&mut t) + .unwrap(); + + t.into() +} diff --git a/pallets/multisig/src/tests.rs b/pallets/multisig/src/tests.rs new file mode 100644 index 00000000..d0d4aa5b --- /dev/null +++ b/pallets/multisig/src/tests.rs @@ -0,0 +1,1247 @@ +//! Unit tests for pallet-multisig + +use crate::{mock::*, Error, Event, GlobalNonce, Multisigs, ProposalStatus, Proposals}; +use codec::Encode; +use frame_support::{assert_noop, assert_ok, traits::fungible::Mutate}; +use sp_core::crypto::AccountId32; + +/// Helper function to get Alice's account ID +fn alice() -> AccountId32 { + account_id(1) +} + +/// Helper function to get Bob's account ID +fn bob() -> AccountId32 { + account_id(2) +} + +/// Helper function to get Charlie's account ID +fn charlie() -> AccountId32 { + account_id(3) +} + +/// Helper function to get Dave's account ID +fn dave() -> AccountId32 { + account_id(4) +} + +/// Helper function to create a simple encoded call +fn make_call(remark: Vec) -> Vec { + let call = RuntimeCall::System(frame_system::Call::remark { remark }); + call.encode() +} + +/// Helper function to get the ID of the last proposal created +/// Returns the current proposal_nonce - 1 (last used ID) +fn get_last_proposal_id(multisig_address: &AccountId32) -> u32 { + let multisig = Multisigs::::get(multisig_address).expect("Multisig should exist"); + multisig.proposal_nonce.saturating_sub(1) +} + +// ==================== MULTISIG CREATION TESTS ==================== + +#[test] +fn create_multisig_works() { + new_test_ext().execute_with(|| { + // Initialize block number for events + System::set_block_number(1); + + // Setup + let creator = alice(); + let signers = vec![bob(), charlie(), dave()]; + let threshold = 2; + + // Get initial balance + let initial_balance = Balances::free_balance(creator.clone()); + let fee = 1000; // MultisigFeeParam + let deposit = 500; // MultisigDepositParam + + // Create multisig + assert_ok!(Multisig::create_multisig( + RuntimeOrigin::signed(creator.clone()), + signers.clone(), + threshold, + )); + + // Check balances + // Deposit is reserved, fee is burned + assert_eq!(Balances::reserved_balance(creator.clone()), deposit); + assert_eq!(Balances::free_balance(creator.clone()), initial_balance - fee - deposit); + + // Check that multisig was created + let global_nonce = GlobalNonce::::get(); + assert_eq!(global_nonce, 1); + + // Get multisig address + let multisig_address = Multisig::derive_multisig_address(&signers, 0); + + // Check storage + let multisig_data = Multisigs::::get(&multisig_address).unwrap(); + assert_eq!(multisig_data.threshold, threshold); + assert_eq!(multisig_data.nonce, 0); + assert_eq!(multisig_data.signers.to_vec(), signers); + assert_eq!(multisig_data.active_proposals, 0); + assert_eq!(multisig_data.creator, creator.clone()); + assert_eq!(multisig_data.deposit, deposit); + + // Check that event was emitted + System::assert_last_event( + Event::MultisigCreated { creator, multisig_address, signers, threshold, nonce: 0 } + .into(), + ); + }); +} + +#[test] +fn create_multisig_fails_with_threshold_zero() { + new_test_ext().execute_with(|| { + let creator = alice(); + let signers = vec![bob(), charlie()]; + let threshold = 0; + + assert_noop!( + Multisig::create_multisig(RuntimeOrigin::signed(creator.clone()), signers, threshold,), + Error::::ThresholdZero + ); + }); +} + +#[test] +fn create_multisig_fails_with_empty_signers() { + new_test_ext().execute_with(|| { + let creator = alice(); + let signers = vec![]; + let threshold = 1; + + assert_noop!( + Multisig::create_multisig(RuntimeOrigin::signed(creator.clone()), signers, threshold,), + Error::::NotEnoughSigners + ); + }); +} + +#[test] +fn create_multisig_fails_with_threshold_too_high() { + new_test_ext().execute_with(|| { + let creator = alice(); + let signers = vec![bob(), charlie()]; + let threshold = 3; // More than number of signers + + assert_noop!( + Multisig::create_multisig(RuntimeOrigin::signed(creator.clone()), signers, threshold,), + Error::::ThresholdTooHigh + ); + }); +} + +#[test] +fn create_multisig_fails_with_duplicate_signers() { + new_test_ext().execute_with(|| { + let creator = alice(); + let signers = vec![bob(), bob(), charlie()]; // Bob twice + let threshold = 2; + + assert_noop!( + Multisig::create_multisig(RuntimeOrigin::signed(creator.clone()), signers, threshold,), + Error::::DuplicateSigner + ); + }); +} + +#[test] +fn create_multiple_multisigs_increments_nonce() { + new_test_ext().execute_with(|| { + let creator = alice(); + let signers1 = vec![bob(), charlie()]; + let signers2 = vec![bob(), dave()]; + + assert_ok!(Multisig::create_multisig( + RuntimeOrigin::signed(creator.clone()), + signers1.clone(), + 2 + )); + assert_ok!(Multisig::create_multisig( + RuntimeOrigin::signed(creator.clone()), + signers2.clone(), + 2 + )); + + // Check both multisigs exist + let multisig1 = Multisig::derive_multisig_address(&signers1, 0); + let multisig2 = Multisig::derive_multisig_address(&signers2, 1); + + assert!(Multisigs::::contains_key(multisig1)); + assert!(Multisigs::::contains_key(multisig2)); + }); +} + +// ==================== PROPOSAL CREATION TESTS ==================== + +#[test] +fn propose_works() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + let creator = alice(); + let signers = vec![bob(), charlie()]; + assert_ok!(Multisig::create_multisig( + RuntimeOrigin::signed(creator.clone()), + signers.clone(), + 2 + )); + + let multisig_address = Multisig::derive_multisig_address(&signers, 0); + + // Propose a transaction + let proposer = bob(); + let call = make_call(vec![1, 2, 3]); + let expiry = 1000; + + let initial_balance = Balances::free_balance(proposer.clone()); + let proposal_deposit = 100; // ProposalDepositParam (Changed in mock) + // Fee calculation: Base(1000) + (Base(1000) * 1% * 2 signers) = 1000 + 20 = 1020 + let proposal_fee = 1020; + + assert_ok!(Multisig::propose( + RuntimeOrigin::signed(proposer.clone()), + multisig_address.clone(), + call.clone(), + expiry + )); + + // Check balances - deposit reserved, fee sent to treasury + assert_eq!(Balances::reserved_balance(proposer.clone()), proposal_deposit); + assert_eq!( + Balances::free_balance(proposer.clone()), + initial_balance - proposal_deposit - proposal_fee + ); + // Fee is burned (reduces total issuance) + + // Check event + let proposal_id = get_last_proposal_id(&multisig_address); + System::assert_last_event( + Event::ProposalCreated { multisig_address, proposer, proposal_id }.into(), + ); + }); +} + +#[test] +fn propose_fails_if_not_signer() { + new_test_ext().execute_with(|| { + let creator = alice(); + let signers = vec![bob(), charlie()]; + assert_ok!(Multisig::create_multisig( + RuntimeOrigin::signed(creator.clone()), + signers.clone(), + 2 + )); + + let multisig_address = Multisig::derive_multisig_address(&signers, 0); + + // Try to propose as non-signer + let call = make_call(vec![1, 2, 3]); + assert_noop!( + Multisig::propose(RuntimeOrigin::signed(dave()), multisig_address.clone(), call, 1000), + Error::::NotASigner + ); + }); +} + +// ==================== APPROVAL TESTS ==================== + +#[test] +fn approve_works() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + let creator = alice(); + let signers = vec![bob(), charlie(), dave()]; + assert_ok!(Multisig::create_multisig( + RuntimeOrigin::signed(creator.clone()), + signers.clone(), + 3 + )); // Need 3 approvals + + let multisig_address = Multisig::derive_multisig_address(&signers, 0); + + let call = make_call(vec![1, 2, 3]); + assert_ok!(Multisig::propose( + RuntimeOrigin::signed(bob()), + multisig_address.clone(), + call.clone(), + 1000 + )); + + let proposal_id = get_last_proposal_id(&multisig_address); + + // Charlie approves (now 2/3) + assert_ok!(Multisig::approve( + RuntimeOrigin::signed(charlie()), + multisig_address.clone(), + proposal_id + )); + + // Check event + System::assert_last_event( + Event::ProposalApproved { + multisig_address: multisig_address.clone(), + approver: charlie(), + proposal_id, + approvals_count: 2, + } + .into(), + ); + + // Proposal should still exist (not executed yet) + assert!(crate::Proposals::::contains_key(&multisig_address, proposal_id)); + }); +} + +#[test] +fn approve_auto_executes_when_threshold_reached() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + let creator = alice(); + let signers = vec![bob(), charlie()]; + assert_ok!(Multisig::create_multisig( + RuntimeOrigin::signed(creator.clone()), + signers.clone(), + 2 + )); + + let multisig_address = Multisig::derive_multisig_address(&signers, 0); + + let call = make_call(vec![1, 2, 3]); + assert_ok!(Multisig::propose( + RuntimeOrigin::signed(bob()), + multisig_address.clone(), + call.clone(), + 1000 + )); + + let proposal_id = get_last_proposal_id(&multisig_address); + + // Charlie approves - threshold reached (2/2), auto-executes and removes + assert_ok!(Multisig::approve( + RuntimeOrigin::signed(charlie()), + multisig_address.clone(), + proposal_id + )); + + // Check that proposal was executed and immediately removed from storage + assert!(crate::Proposals::::get(&multisig_address, proposal_id).is_none()); + + // Deposit should be returned immediately + assert_eq!(Balances::reserved_balance(bob()), 0); // No longer reserved + + // Check event was emitted + System::assert_has_event( + Event::ProposalExecuted { + multisig_address, + proposal_id, + proposer: bob(), + call: call.clone(), + approvers: vec![bob(), charlie()], + result: Ok(()), + } + .into(), + ); + }); +} + +// ==================== CANCELLATION TESTS ==================== + +#[test] +fn cancel_works() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + let creator = alice(); + let signers = vec![bob(), charlie()]; + assert_ok!(Multisig::create_multisig( + RuntimeOrigin::signed(creator.clone()), + signers.clone(), + 2 + )); + + let multisig_address = Multisig::derive_multisig_address(&signers, 0); + + let proposer = bob(); + let call = make_call(vec![1, 2, 3]); + assert_ok!(Multisig::propose( + RuntimeOrigin::signed(proposer.clone()), + multisig_address.clone(), + call.clone(), + 1000 + )); + + let proposal_id = get_last_proposal_id(&multisig_address); + + // Cancel the proposal - immediately removes and returns deposit + assert_ok!(Multisig::cancel( + RuntimeOrigin::signed(proposer.clone()), + multisig_address.clone(), + proposal_id + )); + + // Proposal should be immediately removed from storage + assert!(crate::Proposals::::get(&multisig_address, proposal_id).is_none()); + + // Deposit should be returned immediately + assert_eq!(Balances::reserved_balance(proposer.clone()), 0); + + // Check event + System::assert_last_event( + Event::ProposalCancelled { multisig_address, proposer, proposal_id }.into(), + ); + }); +} + +#[test] +fn cancel_fails_if_already_executed() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + let creator = alice(); + let signers = vec![bob(), charlie()]; + assert_ok!(Multisig::create_multisig( + RuntimeOrigin::signed(creator.clone()), + signers.clone(), + 2 + )); + + let multisig_address = Multisig::derive_multisig_address(&signers, 0); + + let call = make_call(vec![1, 2, 3]); + assert_ok!(Multisig::propose( + RuntimeOrigin::signed(bob()), + multisig_address.clone(), + call.clone(), + 1000 + )); + + let proposal_id = get_last_proposal_id(&multisig_address); + + // Approve to execute (auto-executes and removes proposal) + assert_ok!(Multisig::approve( + RuntimeOrigin::signed(charlie()), + multisig_address.clone(), + proposal_id + )); + + // Try to cancel executed proposal (already removed, so ProposalNotFound) + assert_noop!( + Multisig::cancel(RuntimeOrigin::signed(bob()), multisig_address.clone(), proposal_id), + Error::::ProposalNotFound + ); + }); +} + +// ==================== DEPOSIT RECOVERY TESTS ==================== + +#[test] +fn remove_expired_works_after_grace_period() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + let creator = alice(); + let signers = vec![bob(), charlie()]; + assert_ok!(Multisig::create_multisig( + RuntimeOrigin::signed(creator.clone()), + signers.clone(), + 2 + )); + + let multisig_address = Multisig::derive_multisig_address(&signers, 0); + + let call = make_call(vec![1, 2, 3]); + let expiry = 100; + assert_ok!(Multisig::propose( + RuntimeOrigin::signed(bob()), + multisig_address.clone(), + call.clone(), + expiry + )); + + let proposal_id = get_last_proposal_id(&multisig_address); + + // Move past expiry + grace period (100 blocks) + System::set_block_number(expiry + 101); + + // Any signer can remove after grace period (charlie is a signer) + assert_ok!(Multisig::remove_expired( + RuntimeOrigin::signed(charlie()), + multisig_address.clone(), + proposal_id + )); + + // Proposal should be gone + assert!(!crate::Proposals::::contains_key(&multisig_address, proposal_id)); + + // Deposit should be returned to proposer + assert_eq!(Balances::reserved_balance(bob()), 0); + }); +} + +#[test] +fn executed_proposals_auto_removed() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + let creator = alice(); + let signers = vec![bob(), charlie()]; + assert_ok!(Multisig::create_multisig( + RuntimeOrigin::signed(creator.clone()), + signers.clone(), + 2 + )); + + let multisig_address = Multisig::derive_multisig_address(&signers, 0); + + let call = make_call(vec![1, 2, 3]); + assert_ok!(Multisig::propose( + RuntimeOrigin::signed(bob()), + multisig_address.clone(), + call.clone(), + 1000 + )); + + let proposal_id = get_last_proposal_id(&multisig_address); + + // Execute - should auto-remove proposal and return deposit + assert_ok!(Multisig::approve( + RuntimeOrigin::signed(charlie()), + multisig_address.clone(), + proposal_id + )); + + // Proposal should be immediately removed + assert!(crate::Proposals::::get(&multisig_address, proposal_id).is_none()); + + // Deposit should be immediately returned + assert_eq!(Balances::reserved_balance(bob()), 0); + + // Trying to remove again should fail (already removed) + assert_noop!( + Multisig::remove_expired( + RuntimeOrigin::signed(charlie()), + multisig_address.clone(), + proposal_id + ), + Error::::ProposalNotFound + ); + }); +} + +#[test] +fn remove_expired_fails_for_non_signer() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + let creator = alice(); + let signers = vec![bob(), charlie()]; + assert_ok!(Multisig::create_multisig( + RuntimeOrigin::signed(creator.clone()), + signers.clone(), + 2 + )); + + let multisig_address = Multisig::derive_multisig_address(&signers, 0); + + let call = make_call(vec![1, 2, 3]); + let expiry = 1000; + assert_ok!(Multisig::propose( + RuntimeOrigin::signed(bob()), + multisig_address.clone(), + call.clone(), + expiry + )); + + let proposal_id = get_last_proposal_id(&multisig_address); + + // Move past expiry + System::set_block_number(expiry + 1); + + // Dave is not a signer, should fail + assert_noop!( + Multisig::remove_expired( + RuntimeOrigin::signed(dave()), + multisig_address.clone(), + proposal_id + ), + Error::::NotASigner + ); + + // But charlie (who is a signer) can do it + assert_ok!(Multisig::remove_expired( + RuntimeOrigin::signed(charlie()), + multisig_address.clone(), + proposal_id + )); + }); +} + +#[test] +fn claim_deposits_works() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + let creator = alice(); + let signers = vec![bob(), charlie()]; + assert_ok!(Multisig::create_multisig( + RuntimeOrigin::signed(creator.clone()), + signers.clone(), + 2 + )); + + let multisig_address = Multisig::derive_multisig_address(&signers, 0); + + // Bob creates 3 proposals + for i in 0..3 { + let call = make_call(vec![i as u8; 32]); + assert_ok!(Multisig::propose( + RuntimeOrigin::signed(bob()), + multisig_address.clone(), + call, + 100 + )); + } + + // All reserved + assert_eq!(Balances::reserved_balance(bob()), 300); // 3 * 100 + + // Move past expiry + grace period + System::set_block_number(201); + + // Bob claims all deposits at once + assert_ok!(Multisig::claim_deposits( + RuntimeOrigin::signed(bob()), + multisig_address.clone() + )); + + // All deposits returned + assert_eq!(Balances::reserved_balance(bob()), 0); + + // Check event + System::assert_has_event( + Event::DepositsClaimed { + multisig_address, + claimer: bob(), + total_returned: 300, + proposals_removed: 3, + multisig_removed: false, + } + .into(), + ); + }); +} + +// ==================== HELPER FUNCTION TESTS ==================== + +#[test] +fn derive_multisig_address_is_deterministic() { + new_test_ext().execute_with(|| { + let signers = vec![bob(), charlie(), dave()]; + let nonce = 42; + + let address1 = Multisig::derive_multisig_address(&signers, nonce); + let address2 = Multisig::derive_multisig_address(&signers, nonce); + + assert_eq!(address1, address2); + }); +} + +#[test] +fn derive_multisig_address_different_for_different_nonce() { + new_test_ext().execute_with(|| { + let signers = vec![bob(), charlie(), dave()]; + + let address1 = Multisig::derive_multisig_address(&signers, 0); + let address2 = Multisig::derive_multisig_address(&signers, 1); + + assert_ne!(address1, address2); + }); +} + +#[test] +fn is_signer_works() { + new_test_ext().execute_with(|| { + let signers = vec![bob(), charlie()]; + assert_ok!(Multisig::create_multisig(RuntimeOrigin::signed(alice()), signers.clone(), 2)); + + let multisig_address = Multisig::derive_multisig_address(&signers, 0); + + assert!(Multisig::is_signer(&multisig_address, &bob())); + assert!(Multisig::is_signer(&multisig_address, &charlie())); + assert!(!Multisig::is_signer(&multisig_address, &dave())); + }); +} + +#[test] +fn too_many_proposals_in_storage_fails() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + let creator = alice(); + let signers = vec![bob(), charlie()]; + assert_ok!(Multisig::create_multisig( + RuntimeOrigin::signed(creator.clone()), + signers.clone(), + 2 + )); + let multisig_address = Multisig::derive_multisig_address(&signers, 0); + + // MaxTotal = 20, 2 signers = 10 each + // Executed/Cancelled proposals are auto-removed, so only Active count toward storage + // Create 10 active proposals from Bob + for i in 0..10 { + let call = make_call(vec![i as u8]); + assert_ok!(Multisig::propose( + RuntimeOrigin::signed(bob()), + multisig_address.clone(), + call.clone(), + 1000 + )); + } + // Bob has 10 active = 10 total (at per-signer limit) + + // Create 10 active proposals from Charlie + for i in 10..20 { + let call = make_call(vec![i as u8]); + assert_ok!(Multisig::propose( + RuntimeOrigin::signed(charlie()), + multisig_address.clone(), + call.clone(), + 1000 + )); + } + // Charlie has 10 active = 10 total (at per-signer limit) + // Total: 20 active (AT LIMIT) + + // Try to add 21st - should fail on total limit + let call = make_call(vec![99]); + assert_noop!( + Multisig::propose(RuntimeOrigin::signed(bob()), multisig_address.clone(), call, 2000), + Error::::TooManyProposalsInStorage + ); + }); +} + +#[test] +fn only_active_proposals_remain_in_storage() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + let creator = alice(); + let signers = vec![bob(), charlie()]; + assert_ok!(Multisig::create_multisig( + RuntimeOrigin::signed(creator.clone()), + signers.clone(), + 2 + )); + let multisig_address = Multisig::derive_multisig_address(&signers, 0); + + // Test that only Active proposals remain in storage (Executed/Cancelled auto-removed) + + // Bob creates 10, executes 5, cancels 1 - only 4 active remain + for i in 0..10 { + let call = make_call(vec![i as u8]); + assert_ok!(Multisig::propose( + RuntimeOrigin::signed(bob()), + multisig_address.clone(), + call.clone(), + 1000 + )); + + if i < 5 { + let id = get_last_proposal_id(&multisig_address); + assert_ok!(Multisig::approve( + RuntimeOrigin::signed(charlie()), + multisig_address.clone(), + id + )); + } else if i == 5 { + let id = get_last_proposal_id(&multisig_address); + assert_ok!(Multisig::cancel( + RuntimeOrigin::signed(bob()), + multisig_address.clone(), + id + )); + } + } + // Bob now has 4 Active in storage (i=6,7,8,9), 5 executed + 1 cancelled were removed + + // Bob can create 6 more to reach his per-signer limit (10) + for i in 10..16 { + assert_ok!(Multisig::propose( + RuntimeOrigin::signed(bob()), + multisig_address.clone(), + make_call(vec![i]), + 2000 + )); + } + // Bob: 10 Active (at per-signer limit) + + // Bob cannot create 11th + assert_noop!( + Multisig::propose( + RuntimeOrigin::signed(bob()), + multisig_address.clone(), + make_call(vec![99]), + 3000 + ), + Error::::TooManyProposalsPerSigner + ); + }); +} + +#[test] +fn auto_cleanup_allows_new_proposals() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + let creator = alice(); + let signers = vec![bob(), charlie()]; + assert_ok!(Multisig::create_multisig( + RuntimeOrigin::signed(creator.clone()), + signers.clone(), + 2 + )); + let multisig_address = Multisig::derive_multisig_address(&signers, 0); + + // Bob creates 10 proposals, all expire at block 100 (at per-signer limit) + for i in 0..10 { + assert_ok!(Multisig::propose( + RuntimeOrigin::signed(bob()), + multisig_address.clone(), + make_call(vec![i]), + 100 + )); + } + // Bob: 10 Active (at per-signer limit) + + // Bob cannot create more (at limit) + assert_noop!( + Multisig::propose( + RuntimeOrigin::signed(bob()), + multisig_address.clone(), + make_call(vec![99]), + 200 + ), + Error::::TooManyProposalsPerSigner + ); + + // Move past expiry + System::set_block_number(101); + + // Now Bob can create new - propose() auto-cleans expired + assert_ok!(Multisig::propose( + RuntimeOrigin::signed(bob()), + multisig_address.clone(), + make_call(vec![99]), + 200 + )); + + // Verify old proposals were removed + let count = crate::Proposals::::iter_prefix(&multisig_address).count(); + assert_eq!(count, 1); // Only the new one remains + }); +} + +#[test] +fn propose_fails_with_expiry_in_past() { + new_test_ext().execute_with(|| { + System::set_block_number(100); + + let creator = alice(); + let signers = vec![bob(), charlie()]; + assert_ok!(Multisig::create_multisig( + RuntimeOrigin::signed(creator.clone()), + signers.clone(), + 2 + )); + + let multisig_address = Multisig::derive_multisig_address(&signers, 0); + + let call = make_call(vec![1, 2, 3]); + + // Try to create proposal with expiry in the past (< current_block) + assert_noop!( + Multisig::propose( + RuntimeOrigin::signed(bob()), + multisig_address.clone(), + call.clone(), + 50 + ), + Error::::ExpiryInPast + ); + + // Try with expiry equal to current block (not > current_block) + assert_noop!( + Multisig::propose( + RuntimeOrigin::signed(bob()), + multisig_address.clone(), + call.clone(), + 100 + ), + Error::::ExpiryInPast + ); + + // Valid: expiry in the future + assert_ok!(Multisig::propose( + RuntimeOrigin::signed(bob()), + multisig_address.clone(), + call, + 101 + )); + }); +} + +#[test] +fn propose_fails_with_expiry_too_far() { + new_test_ext().execute_with(|| { + System::set_block_number(100); + + let creator = alice(); + let signers = vec![bob(), charlie()]; + assert_ok!(Multisig::create_multisig( + RuntimeOrigin::signed(creator.clone()), + signers.clone(), + 2 + )); + + let multisig_address = Multisig::derive_multisig_address(&signers, 0); + + let call = make_call(vec![1, 2, 3]); + + // MaxExpiryDurationParam = 10000 blocks (from mock.rs) + // Current block = 100 + // Max allowed expiry = 100 + 10000 = 10100 + + // Try to create proposal with expiry too far in the future + assert_noop!( + Multisig::propose( + RuntimeOrigin::signed(bob()), + multisig_address.clone(), + call.clone(), + 10101 + ), + Error::::ExpiryTooFar + ); + + // Try with expiry way beyond the limit + assert_noop!( + Multisig::propose( + RuntimeOrigin::signed(bob()), + multisig_address.clone(), + call.clone(), + 20000 + ), + Error::::ExpiryTooFar + ); + + // Valid: expiry exactly at max allowed + assert_ok!(Multisig::propose( + RuntimeOrigin::signed(bob()), + multisig_address.clone(), + call.clone(), + 10100 + )); + + // Move to next block and try again + System::set_block_number(101); + // Now max allowed = 101 + 10000 = 10101 + assert_ok!(Multisig::propose( + RuntimeOrigin::signed(bob()), + multisig_address.clone(), + call, + 10101 + )); + }); +} + +#[test] +fn propose_charges_correct_fee_with_signer_factor() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + let creator = alice(); + // 3 Signers: Bob, Charlie, Dave + let signers = vec![bob(), charlie(), dave()]; + assert_ok!(Multisig::create_multisig( + RuntimeOrigin::signed(creator.clone()), + signers.clone(), + 2 + )); + + let multisig_address = Multisig::derive_multisig_address(&signers, 0); + + let proposer = bob(); + let call = make_call(vec![1, 2, 3]); + let initial_balance = Balances::free_balance(proposer.clone()); + + assert_ok!(Multisig::propose( + RuntimeOrigin::signed(proposer.clone()), + multisig_address, + call, + 1000 + )); + + // ProposalFeeParam = 1000 + // SignerStepFactor = 1% + // Signers = 3 + // Calculation: 1000 + (1000 * 1% * 3) = 1000 + 30 = 1030 + let expected_fee = 1030; + let deposit = 100; // ProposalDepositParam + + assert_eq!( + Balances::free_balance(proposer.clone()), + initial_balance - deposit - expected_fee + ); + // Fee is burned (reduces total issuance) + }); +} + +#[test] +fn dissolve_multisig_works() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let creator = alice(); + let signers = vec![bob(), charlie()]; + let deposit = 500; + let fee = 1000; + let initial_balance = Balances::free_balance(creator.clone()); + + // Create + assert_ok!(Multisig::create_multisig( + RuntimeOrigin::signed(creator.clone()), + signers.clone(), + 2 + )); + assert_eq!(Balances::reserved_balance(creator.clone()), deposit); + + let multisig_address = Multisig::derive_multisig_address(&signers, 0); + + // Try to dissolve immediately (success) + assert_ok!(Multisig::dissolve_multisig( + RuntimeOrigin::signed(creator.clone()), + multisig_address.clone() + )); + + // Check cleanup + assert!(!Multisigs::::contains_key(&multisig_address)); + assert_eq!(Balances::reserved_balance(creator.clone()), 0); + // Balance returned (minus burned fee) + assert_eq!(Balances::free_balance(creator.clone()), initial_balance - fee); + }); +} + +#[test] +fn dissolve_multisig_fails_with_proposals() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let creator = alice(); + let signers = vec![bob(), charlie()]; + assert_ok!(Multisig::create_multisig( + RuntimeOrigin::signed(creator.clone()), + signers.clone(), + 2 + )); + let multisig_address = Multisig::derive_multisig_address(&signers, 0); + + // Create proposal + let call = make_call(vec![1]); + assert_ok!(Multisig::propose( + RuntimeOrigin::signed(bob()), + multisig_address.clone(), + call, + 100 + )); + + // Try to dissolve + assert_noop!( + Multisig::dissolve_multisig( + RuntimeOrigin::signed(creator.clone()), + multisig_address.clone() + ), + Error::::ProposalsExist + ); + }); +} + +#[test] +fn per_signer_proposal_limit_enforced() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let creator = alice(); + let signers = vec![bob(), charlie()]; + assert_ok!(Multisig::create_multisig( + RuntimeOrigin::signed(creator.clone()), + signers.clone(), + 2 + )); + let multisig_address = Multisig::derive_multisig_address(&signers, 0); + + // MaxTotalProposalsInStorage = 20 + // With 2 signers, each can have max 20/2 = 10 proposals + // Only Active proposals count (Executed/Cancelled auto-removed) + + // Bob creates 10 active proposals (at per-signer limit) + for i in 0..10 { + assert_ok!(Multisig::propose( + RuntimeOrigin::signed(bob()), + multisig_address.clone(), + make_call(vec![i]), + 1000 + )); + } + + // Bob at limit - tries to create 11th + assert_noop!( + Multisig::propose( + RuntimeOrigin::signed(bob()), + multisig_address.clone(), + make_call(vec![99]), + 2000 + ), + Error::::TooManyProposalsPerSigner + ); + + // But Charlie can still create (independent limit) + assert_ok!(Multisig::propose( + RuntimeOrigin::signed(charlie()), + multisig_address.clone(), + make_call(vec![100]), + 2000 + )); + }); +} + +#[test] +fn propose_with_threshold_one_executes_immediately() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + let creator = alice(); + let signers = vec![alice(), bob(), charlie()]; + let threshold = 1; // Only 1 approval needed + + // Create multisig with threshold=1 + assert_ok!(Multisig::create_multisig( + RuntimeOrigin::signed(creator.clone()), + signers.clone(), + threshold + )); + + let multisig_address = Multisig::derive_multisig_address(&signers, 0); + + // Fund multisig account for balance transfer + as Mutate<_>>::mint_into(&multisig_address, 50000).unwrap(); + + let initial_dave_balance = Balances::free_balance(&dave()); + + // Alice proposes a transfer - should execute immediately since threshold=1 + let transfer_call = RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive { + dest: dave(), + value: 1000, + }); + + assert_ok!(Multisig::propose( + RuntimeOrigin::signed(alice()), + multisig_address.clone(), + transfer_call.encode(), + 100 + )); + + let proposal_id = 0; // First proposal + + // Verify the proposal was executed immediately (should NOT exist anymore) + assert!(Proposals::::get(&multisig_address, proposal_id).is_none()); + + // Verify the transfer actually happened + assert_eq!(Balances::free_balance(&dave()), initial_dave_balance + 1000); + + // Verify ProposalExecuted event was emitted + System::assert_has_event( + Event::ProposalExecuted { + multisig_address: multisig_address.clone(), + proposal_id, + proposer: alice(), + call: transfer_call.encode(), + approvers: vec![alice()], + result: Ok(()), + } + .into(), + ); + + // Verify deposit was returned to Alice (execution removes proposal) + let alice_reserved = Balances::reserved_balance(&alice()); + assert_eq!(alice_reserved, 500); // Only MultisigDeposit, no ProposalDeposit + + // Verify active_proposals counter was decremented back to 0 + let multisig_data = Multisigs::::get(&multisig_address).unwrap(); + assert_eq!(multisig_data.active_proposals, 0); + }); +} + +#[test] +fn propose_with_threshold_two_waits_for_approval() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + let creator = alice(); + let signers = vec![alice(), bob(), charlie()]; + let threshold = 2; // Need 2 approvals + + // Create multisig with threshold=2 + assert_ok!(Multisig::create_multisig( + RuntimeOrigin::signed(creator.clone()), + signers.clone(), + threshold + )); + + let multisig_address = Multisig::derive_multisig_address(&signers, 0); + + // Fund multisig account + as Mutate<_>>::mint_into(&multisig_address, 50000).unwrap(); + + let initial_dave_balance = Balances::free_balance(&dave()); + + // Alice proposes a transfer - should NOT execute yet + let transfer_call = RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive { + dest: dave(), + value: 1000, + }); + + assert_ok!(Multisig::propose( + RuntimeOrigin::signed(alice()), + multisig_address.clone(), + transfer_call.encode(), + 100 + )); + + let proposal_id = 0; + + // Verify the proposal still exists (waiting for more approvals) + let proposal = Proposals::::get(&multisig_address, proposal_id).unwrap(); + assert_eq!(proposal.status, ProposalStatus::Active); + assert_eq!(proposal.approvals.len(), 1); // Only Alice so far + + // Verify the transfer did NOT happen yet + assert_eq!(Balances::free_balance(&dave()), initial_dave_balance); + + // Bob approves - NOW it should execute (threshold=2 reached) + assert_ok!(Multisig::approve( + RuntimeOrigin::signed(bob()), + multisig_address.clone(), + proposal_id + )); + + // Now proposal should be executed and removed + assert!(Proposals::::get(&multisig_address, proposal_id).is_none()); + + // Verify the transfer happened + assert_eq!(Balances::free_balance(&dave()), initial_dave_balance + 1000); + }); +} diff --git a/pallets/multisig/src/weights.rs b/pallets/multisig/src/weights.rs new file mode 100644 index 00000000..140a6e07 --- /dev/null +++ b/pallets/multisig/src/weights.rs @@ -0,0 +1,323 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +//! Autogenerated weights for `pallet_multisig` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 49.1.0 +//! DATE: 2026-01-27, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `coldbook.local`, CPU: `` +//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` + +// Executed Command: +// ./target/release/quantus-node +// benchmark +// pallet +// --chain=dev +// --pallet=pallet_multisig +// --extrinsic=* +// --steps=50 +// --repeat=20 +// --output=./pallets/multisig/src/weights.rs +// --template=./.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] +#![allow(dead_code)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for `pallet_multisig`. +pub trait WeightInfo { + fn create_multisig() -> Weight; + fn propose(c: u32, e: u32, ) -> Weight; + fn approve(c: u32, ) -> Weight; + fn approve_and_execute(c: u32, ) -> Weight; + fn cancel(c: u32, ) -> Weight; + fn remove_expired() -> Weight; + fn claim_deposits(p: u32, ) -> Weight; + fn dissolve_multisig() -> Weight; +} + +/// Weights for `pallet_multisig` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `Multisig::GlobalNonce` (r:1 w:1) + /// Proof: `Multisig::GlobalNonce` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Multisig::Multisigs` (r:1 w:1) + /// Proof: `Multisig::Multisigs` (`max_values`: None, `max_size`: Some(6924), added: 9399, mode: `MaxEncodedLen`) + fn create_multisig() -> Weight { + // Proof Size summary in bytes: + // Measured: `152` + // Estimated: `10389` + // Minimum execution time: 192_000_000 picoseconds. + Weight::from_parts(197_000_000, 10389) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `Multisig::Multisigs` (r:1 w:1) + /// Proof: `Multisig::Multisigs` (`max_values`: None, `max_size`: Some(6924), added: 9399, mode: `MaxEncodedLen`) + /// Storage: `Multisig::Proposals` (r:201 w:201) + /// Proof: `Multisig::Proposals` (`max_values`: None, `max_size`: Some(13557), added: 16032, mode: `MaxEncodedLen`) + /// The range of component `c` is `[0, 10140]`. + /// The range of component `e` is `[0, 200]`. + fn propose(_c: u32, e: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `458 + e * (215 ±0)` + // Estimated: `17022 + e * (16032 ±0)` + // Minimum execution time: 40_000_000 picoseconds. + Weight::from_parts(162_343_032, 17022) + // Standard Error: 41_034 + .saturating_add(Weight::from_parts(14_232_109, 0).saturating_mul(e.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(e.into()))) + .saturating_add(T::DbWeight::get().writes(2_u64)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(e.into()))) + .saturating_add(Weight::from_parts(0, 16032).saturating_mul(e.into())) + } + /// Storage: `Multisig::Multisigs` (r:1 w:1) + /// Proof: `Multisig::Multisigs` (`max_values`: None, `max_size`: Some(6924), added: 9399, mode: `MaxEncodedLen`) + /// Storage: `Multisig::Proposals` (r:1 w:1) + /// Proof: `Multisig::Proposals` (`max_values`: None, `max_size`: Some(13557), added: 16032, mode: `MaxEncodedLen`) + /// The range of component `c` is `[0, 10140]`. + fn approve(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `766 + c * (1 ±0)` + // Estimated: `17022` + // Minimum execution time: 14_000_000 picoseconds. + Weight::from_parts(16_119_988, 17022) + // Standard Error: 42 + .saturating_add(Weight::from_parts(766, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `Multisig::Multisigs` (r:1 w:1) + /// Proof: `Multisig::Multisigs` (`max_values`: None, `max_size`: Some(6924), added: 9399, mode: `MaxEncodedLen`) + /// Storage: `Multisig::Proposals` (r:1 w:1) + /// Proof: `Multisig::Proposals` (`max_values`: None, `max_size`: Some(13557), added: 16032, mode: `MaxEncodedLen`) + /// The range of component `c` is `[0, 10140]`. + fn approve_and_execute(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `734 + c * (1 ±0)` + // Estimated: `17022` + // Minimum execution time: 25_000_000 picoseconds. + Weight::from_parts(33_804_547, 17022) + // Standard Error: 105 + .saturating_add(Weight::from_parts(802, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `Multisig::Proposals` (r:1 w:1) + /// Proof: `Multisig::Proposals` (`max_values`: None, `max_size`: Some(13557), added: 16032, mode: `MaxEncodedLen`) + /// Storage: `Multisig::Multisigs` (r:1 w:1) + /// Proof: `Multisig::Multisigs` (`max_values`: None, `max_size`: Some(6924), added: 9399, mode: `MaxEncodedLen`) + /// The range of component `c` is `[0, 10140]`. + fn cancel(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `734 + c * (1 ±0)` + // Estimated: `17022` + // Minimum execution time: 19_000_000 picoseconds. + Weight::from_parts(24_824_223, 17022) + // Standard Error: 65 + .saturating_add(Weight::from_parts(117, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `Multisig::Multisigs` (r:1 w:1) + /// Proof: `Multisig::Multisigs` (`max_values`: None, `max_size`: Some(6924), added: 9399, mode: `MaxEncodedLen`) + /// Storage: `Multisig::Proposals` (r:1 w:1) + /// Proof: `Multisig::Proposals` (`max_values`: None, `max_size`: Some(13557), added: 16032, mode: `MaxEncodedLen`) + fn remove_expired() -> Weight { + // Proof Size summary in bytes: + // Measured: `764` + // Estimated: `17022` + // Minimum execution time: 20_000_000 picoseconds. + Weight::from_parts(24_000_000, 17022) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `Multisig::Proposals` (r:201 w:200) + /// Proof: `Multisig::Proposals` (`max_values`: None, `max_size`: Some(13557), added: 16032, mode: `MaxEncodedLen`) + /// Storage: `Multisig::Multisigs` (r:1 w:1) + /// Proof: `Multisig::Multisigs` (`max_values`: None, `max_size`: Some(6924), added: 9399, mode: `MaxEncodedLen`) + /// The range of component `p` is `[1, 200]`. + fn claim_deposits(p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `625 + p * (237 ±0)` + // Estimated: `17022 + p * (16032 ±0)` + // Minimum execution time: 24_000_000 picoseconds. + Weight::from_parts(27_900_496, 17022) + // Standard Error: 31_493 + .saturating_add(Weight::from_parts(13_930_528, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(p.into()))) + .saturating_add(T::DbWeight::get().writes(1_u64)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(p.into()))) + .saturating_add(Weight::from_parts(0, 16032).saturating_mul(p.into())) + } + /// Storage: `Multisig::Multisigs` (r:1 w:1) + /// Proof: `Multisig::Multisigs` (`max_values`: None, `max_size`: Some(6924), added: 9399, mode: `MaxEncodedLen`) + /// Storage: `Multisig::Proposals` (r:1 w:0) + /// Proof: `Multisig::Proposals` (`max_values`: None, `max_size`: Some(13557), added: 16032, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn dissolve_multisig() -> Weight { + // Proof Size summary in bytes: + // Measured: `538` + // Estimated: `17022` + // Minimum execution time: 20_000_000 picoseconds. + Weight::from_parts(20_000_000, 17022) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: `Multisig::GlobalNonce` (r:1 w:1) + /// Proof: `Multisig::GlobalNonce` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Multisig::Multisigs` (r:1 w:1) + /// Proof: `Multisig::Multisigs` (`max_values`: None, `max_size`: Some(6924), added: 9399, mode: `MaxEncodedLen`) + fn create_multisig() -> Weight { + // Proof Size summary in bytes: + // Measured: `152` + // Estimated: `10389` + // Minimum execution time: 192_000_000 picoseconds. + Weight::from_parts(197_000_000, 10389) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `Multisig::Multisigs` (r:1 w:1) + /// Proof: `Multisig::Multisigs` (`max_values`: None, `max_size`: Some(6924), added: 9399, mode: `MaxEncodedLen`) + /// Storage: `Multisig::Proposals` (r:201 w:201) + /// Proof: `Multisig::Proposals` (`max_values`: None, `max_size`: Some(13557), added: 16032, mode: `MaxEncodedLen`) + /// The range of component `c` is `[0, 10140]`. + /// The range of component `e` is `[0, 200]`. + fn propose(_c: u32, e: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `458 + e * (215 ±0)` + // Estimated: `17022 + e * (16032 ±0)` + // Minimum execution time: 40_000_000 picoseconds. + Weight::from_parts(162_343_032, 17022) + // Standard Error: 41_034 + .saturating_add(Weight::from_parts(14_232_109, 0).saturating_mul(e.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(e.into()))) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(e.into()))) + .saturating_add(Weight::from_parts(0, 16032).saturating_mul(e.into())) + } + /// Storage: `Multisig::Multisigs` (r:1 w:1) + /// Proof: `Multisig::Multisigs` (`max_values`: None, `max_size`: Some(6924), added: 9399, mode: `MaxEncodedLen`) + /// Storage: `Multisig::Proposals` (r:1 w:1) + /// Proof: `Multisig::Proposals` (`max_values`: None, `max_size`: Some(13557), added: 16032, mode: `MaxEncodedLen`) + /// The range of component `c` is `[0, 10140]`. + fn approve(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `766 + c * (1 ±0)` + // Estimated: `17022` + // Minimum execution time: 14_000_000 picoseconds. + Weight::from_parts(16_119_988, 17022) + // Standard Error: 42 + .saturating_add(Weight::from_parts(766, 0).saturating_mul(c.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `Multisig::Multisigs` (r:1 w:1) + /// Proof: `Multisig::Multisigs` (`max_values`: None, `max_size`: Some(6924), added: 9399, mode: `MaxEncodedLen`) + /// Storage: `Multisig::Proposals` (r:1 w:1) + /// Proof: `Multisig::Proposals` (`max_values`: None, `max_size`: Some(13557), added: 16032, mode: `MaxEncodedLen`) + /// The range of component `c` is `[0, 10140]`. + fn approve_and_execute(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `734 + c * (1 ±0)` + // Estimated: `17022` + // Minimum execution time: 25_000_000 picoseconds. + Weight::from_parts(33_804_547, 17022) + // Standard Error: 105 + .saturating_add(Weight::from_parts(802, 0).saturating_mul(c.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `Multisig::Proposals` (r:1 w:1) + /// Proof: `Multisig::Proposals` (`max_values`: None, `max_size`: Some(13557), added: 16032, mode: `MaxEncodedLen`) + /// Storage: `Multisig::Multisigs` (r:1 w:1) + /// Proof: `Multisig::Multisigs` (`max_values`: None, `max_size`: Some(6924), added: 9399, mode: `MaxEncodedLen`) + /// The range of component `c` is `[0, 10140]`. + fn cancel(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `734 + c * (1 ±0)` + // Estimated: `17022` + // Minimum execution time: 19_000_000 picoseconds. + Weight::from_parts(24_824_223, 17022) + // Standard Error: 65 + .saturating_add(Weight::from_parts(117, 0).saturating_mul(c.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `Multisig::Multisigs` (r:1 w:1) + /// Proof: `Multisig::Multisigs` (`max_values`: None, `max_size`: Some(6924), added: 9399, mode: `MaxEncodedLen`) + /// Storage: `Multisig::Proposals` (r:1 w:1) + /// Proof: `Multisig::Proposals` (`max_values`: None, `max_size`: Some(13557), added: 16032, mode: `MaxEncodedLen`) + fn remove_expired() -> Weight { + // Proof Size summary in bytes: + // Measured: `764` + // Estimated: `17022` + // Minimum execution time: 20_000_000 picoseconds. + Weight::from_parts(24_000_000, 17022) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `Multisig::Proposals` (r:201 w:200) + /// Proof: `Multisig::Proposals` (`max_values`: None, `max_size`: Some(13557), added: 16032, mode: `MaxEncodedLen`) + /// Storage: `Multisig::Multisigs` (r:1 w:1) + /// Proof: `Multisig::Multisigs` (`max_values`: None, `max_size`: Some(6924), added: 9399, mode: `MaxEncodedLen`) + /// The range of component `p` is `[1, 200]`. + fn claim_deposits(p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `625 + p * (237 ±0)` + // Estimated: `17022 + p * (16032 ±0)` + // Minimum execution time: 24_000_000 picoseconds. + Weight::from_parts(27_900_496, 17022) + // Standard Error: 31_493 + .saturating_add(Weight::from_parts(13_930_528, 0).saturating_mul(p.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(p.into()))) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(p.into()))) + .saturating_add(Weight::from_parts(0, 16032).saturating_mul(p.into())) + } + /// Storage: `Multisig::Multisigs` (r:1 w:1) + /// Proof: `Multisig::Multisigs` (`max_values`: None, `max_size`: Some(6924), added: 9399, mode: `MaxEncodedLen`) + /// Storage: `Multisig::Proposals` (r:1 w:0) + /// Proof: `Multisig::Proposals` (`max_values`: None, `max_size`: Some(13557), added: 16032, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn dissolve_multisig() -> Weight { + // Proof Size summary in bytes: + // Measured: `538` + // Estimated: `17022` + // Minimum execution time: 20_000_000 picoseconds. + Weight::from_parts(20_000_000, 17022) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } +} diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 6f039fe1..fe292af7 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -32,6 +32,7 @@ pallet-assets-holder = { workspace = true, default-features = false } pallet-balances.workspace = true pallet-conviction-voting.workspace = true pallet-mining-rewards.workspace = true +pallet-multisig.workspace = true pallet-preimage.workspace = true pallet-qpow.workspace = true pallet-ranked-collective.workspace = true @@ -95,6 +96,7 @@ std = [ "pallet-balances/std", "pallet-conviction-voting/std", "pallet-mining-rewards/std", + "pallet-multisig/std", "pallet-preimage/std", "pallet-qpow/std", "pallet-ranked-collective/std", @@ -142,6 +144,7 @@ runtime-benchmarks = [ "pallet-balances/runtime-benchmarks", "pallet-conviction-voting/runtime-benchmarks", "pallet-mining-rewards/runtime-benchmarks", + "pallet-multisig/runtime-benchmarks", "pallet-preimage/runtime-benchmarks", "pallet-qpow/runtime-benchmarks", "pallet-ranked-collective/runtime-benchmarks", diff --git a/runtime/src/benchmarks.rs b/runtime/src/benchmarks.rs index 6efb6e0c..cf13e3e1 100644 --- a/runtime/src/benchmarks.rs +++ b/runtime/src/benchmarks.rs @@ -31,6 +31,7 @@ frame_benchmarking::define_benchmarks!( [pallet_sudo, Sudo] [pallet_reversible_transfers, ReversibleTransfers] [pallet_mining_rewards, MiningRewards] + [pallet_multisig, Multisig] [pallet_scheduler, Scheduler] [pallet_qpow, QPoW] ); diff --git a/runtime/src/configs/mod.rs b/runtime/src/configs/mod.rs index 8905376a..a439621c 100644 --- a/runtime/src/configs/mod.rs +++ b/runtime/src/configs/mod.rs @@ -563,6 +563,37 @@ impl pallet_assets_holder::Config for Runtime { type RuntimeHoldReason = RuntimeHoldReason; } +// Multisig configuration +parameter_types! { + pub const MultisigPalletId: PalletId = PalletId(*b"py/mltsg"); + pub const MaxSigners: u32 = 100; + pub const MaxTotalProposalsInStorage: u32 = 200; // Max total in storage (Active + Executed + Cancelled) + pub const MaxCallSize: u32 = 10240; // 10KB + pub const MultisigFee: Balance = 100 * MILLI_UNIT; // 0.1 UNIT (non-refundable) + pub const MultisigDeposit: Balance = 500 * MILLI_UNIT; // 0.5 UNIT (refundable) + pub const ProposalDeposit: Balance = 1000 * MILLI_UNIT; // 1 UNIT (locked until cleanup) + pub const ProposalFee: Balance = 1000 * MILLI_UNIT; // 1 UNIT (non-refundable) + pub const SignerStepFactorParam: Permill = Permill::from_percent(1); + pub const MaxExpiryDuration: BlockNumber = 100_800; // ~2 weeks at 12s blocks (14 days * 24h * 60m * 60s / 12s) +} + +/// Whitelist for calls that can be proposed in multisigs +impl pallet_multisig::Config for Runtime { + type RuntimeCall = RuntimeCall; + type Currency = Balances; + type MaxSigners = MaxSigners; + type MaxTotalProposalsInStorage = MaxTotalProposalsInStorage; + type MaxCallSize = MaxCallSize; + type MultisigFee = MultisigFee; + type MultisigDeposit = MultisigDeposit; + type ProposalDeposit = ProposalDeposit; + type ProposalFee = ProposalFee; + type SignerStepFactor = SignerStepFactorParam; + type MaxExpiryDuration = MaxExpiryDuration; + type PalletId = MultisigPalletId; + type WeightInfo = pallet_multisig::weights::SubstrateWeight; +} + impl TryFrom for pallet_balances::Call { type Error = (); fn try_from(call: RuntimeCall) -> Result { diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 01b17135..d1ef9c8f 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -253,4 +253,7 @@ mod runtime { #[runtime::pallet_index(22)] pub type AssetsHolder = pallet_assets_holder; + + #[runtime::pallet_index(23)] + pub type Multisig = pallet_multisig; } diff --git a/runtime/tests/common.rs b/runtime/tests/common.rs index d4eed8c8..452351ae 100644 --- a/runtime/tests/common.rs +++ b/runtime/tests/common.rs @@ -38,8 +38,8 @@ impl TestCommons { /// Create a test externality with governance track timing based on feature flags /// - Without `production-governance-tests`: Uses fast 2-block periods for all governance tracks - /// - With `production-governance-tests`: Uses production timing (hours/days) - /// This allows CI to test both fast (for speed) and slow (for correctness) governance + /// - With `production-governance-tests`: Uses production timing (hours/days) This allows CI to + /// test both fast (for speed) and slow (for correctness) governance pub fn new_fast_governance_test_ext() -> sp_io::TestExternalities { #[cfg(feature = "production-governance-tests")] { diff --git a/runtime/tests/transactions/integration.rs b/runtime/tests/transactions/integration.rs index 4ae80b98..55ea57bc 100644 --- a/runtime/tests/transactions/integration.rs +++ b/runtime/tests/transactions/integration.rs @@ -106,11 +106,11 @@ mod tests { // Extract components into individual variables for debugging let decoded_address: Address = address; let decoded_signature: DilithiumSignatureScheme = signature; - let decoded_extra: SignedExtra = extra; + let _: SignedExtra = extra; // Debug output for each component println!("Decoded Address: {:?}", decoded_address); - println!("Decoded Extra: {:?}", decoded_extra); + println!("Decoded Extra: ()"); let DilithiumSignatureScheme::Dilithium(sig_public) = decoded_signature.clone(); let sig = sig_public.signature(); From 6049ada07500f654c40d8367df400cb5fc3c8e82 Mon Sep 17 00:00:00 2001 From: Cezary Olborski Date: Wed, 28 Jan 2026 16:13:38 +0800 Subject: [PATCH 23/27] fix: Multisig - auto-cleaning expanded (#364) * fix: Multisig - auto-cleaning expanded * fix: Weights related to storage size --- pallets/mining-rewards/src/lib.rs | 2 +- pallets/multisig/README.md | 45 ++++-- pallets/multisig/src/benchmarking.rs | 78 +++++++++-- pallets/multisig/src/lib.rs | 128 +++++++++++------ pallets/multisig/src/tests.rs | 90 +++++++++++- pallets/multisig/src/weights.rs | 174 +++++++++++++----------- pallets/reversible-transfers/src/lib.rs | 1 + 7 files changed, 367 insertions(+), 151 deletions(-) diff --git a/pallets/mining-rewards/src/lib.rs b/pallets/mining-rewards/src/lib.rs index d60557c1..4f89231e 100644 --- a/pallets/mining-rewards/src/lib.rs +++ b/pallets/mining-rewards/src/lib.rs @@ -123,7 +123,7 @@ pub mod pallet { let total_reward = remaining_supply .checked_div(&emission_divisor) - .unwrap_or_else(|| BalanceOf::::zero()); + .unwrap_or_else(BalanceOf::::zero); // Split the reward between treasury and miner let treasury_portion = T::TreasuryPortion::get(); diff --git a/pallets/multisig/README.md b/pallets/multisig/README.md index 8d259e3a..a7853087 100644 --- a/pallets/multisig/README.md +++ b/pallets/multisig/README.md @@ -77,6 +77,14 @@ Before creating a new proposal, the system **automatically removes all expired A This ensures storage is kept clean and users get their deposits back without manual intervention. +**Threshold=1 Auto-Execution:** +If the multisig has `threshold=1`, the proposal **executes immediately** after creation: +- Proposer's approval counts as the first (and only required) approval +- Call is dispatched automatically +- Proposal is removed from storage immediately +- Deposit is returned to proposer immediately +- No separate `approve()` call needed + **Economic Costs:** - **ProposalFee**: Non-refundable fee (spam prevention, scaled by signer count) → burned - **ProposalDeposit**: Refundable deposit (storage rent) → returned when proposal removed @@ -129,7 +137,7 @@ Cancels a proposal and immediately removes it from storage (proposer only). ### 5. Remove Expired Manually removes expired proposals from storage. Only signers can call this. -**Important:** This is rarely needed because expired proposals are automatically cleaned up when anyone creates a new proposal in the same multisig. +**Important:** This is rarely needed because expired proposals are automatically cleaned up on any multisig activity (`propose()`, `approve()`, `cancel()`). **Required Parameters:** - `multisig_address: AccountId` - Target multisig (REQUIRED) @@ -149,12 +157,12 @@ Manually removes expired proposals from storage. Only signers can call this. **Economic Costs:** None (deposit always returned to proposer) -**Auto-Cleanup:** When anyone calls `propose()`, all expired proposals are automatically removed first, making this function often unnecessary. +**Auto-Cleanup:** ALL expired proposals are automatically removed on any multisig activity (`propose()`, `approve()`, `cancel()`), making this function often unnecessary. ### 6. Claim Deposits Batch cleanup operation to recover all expired proposal deposits. -**Important:** This is rarely needed because expired proposals are automatically cleaned up when anyone creates a new proposal in the same multisig. +**Important:** This is rarely needed because expired proposals are automatically cleaned up on any multisig activity (`propose()`, `approve()`, `cancel()`). **Required Parameters:** - `multisig_address: AccountId` - Target multisig (REQUIRED) @@ -171,7 +179,27 @@ Batch cleanup operation to recover all expired proposal deposits. **Economic Costs:** None (only returns deposits) -**Auto-Cleanup:** When anyone calls `propose()`, all expired proposals are automatically removed first, making this function often unnecessary. +**Auto-Cleanup:** ALL expired proposals are automatically removed on any multisig activity (`propose()`, `approve()`, `cancel()`), making this function often unnecessary. + +### 7. Dissolve Multisig +Permanently removes a multisig and returns the creation deposit to the original creator. + +**Required Parameters:** +- `multisig_address: AccountId` - Target multisig (REQUIRED) + +**Pre-conditions:** +- NO proposals can exist (any status) +- Multisig balance MUST be zero +- Caller must be creator OR any signer + +**Post-conditions:** +- MultisigDeposit returned to **original creator** (not caller) +- Multisig removed from storage +- Cannot be used after dissolution + +**Economic Costs:** None (returns MultisigDeposit) + +**Important:** MultisigFee is NEVER returned - only the MultisigDeposit. ## Use Cases @@ -228,10 +256,11 @@ matches!(call, - **Auto-Returned Immediately:** - When proposal executed (threshold reached) - When proposal cancelled (proposer cancels) - - **Manual Cleanup Required:** - - Expired proposals: Must be manually removed OR auto-cleaned on next `propose()` - - **Auto-Cleanup:** When anyone creates new proposal, all expired proposals cleaned automatically - - No grace period needed - executed/cancelled proposals auto-removed + - **Auto-Cleanup:** ALL expired proposals are automatically removed on ANY multisig activity + - Triggered by: `propose()`, `approve()`, `cancel()` + - Deposits returned to original proposers + - No manual cleanup needed for active multisigs + - **Manual Cleanup:** Only needed for inactive multisigs via `remove_expired()` or `claim_deposits()` ### Storage Limits & Configuration **Purpose:** Prevent unbounded storage growth and resource exhaustion diff --git a/pallets/multisig/src/benchmarking.rs b/pallets/multisig/src/benchmarking.rs index 40c28a2e..03db467f 100644 --- a/pallets/multisig/src/benchmarking.rs +++ b/pallets/multisig/src/benchmarking.rs @@ -133,18 +133,19 @@ mod benchmarks { #[benchmark] fn approve( c: Linear<0, { T::MaxCallSize::get().saturating_sub(100) }>, + e: Linear<0, { T::MaxTotalProposalsInStorage::get() }>, // expired proposals to cleanup ) -> Result<(), BenchmarkError> { // Setup: Create multisig and proposal directly in storage // Threshold is 3, so adding one more approval won't trigger execution let caller: T::AccountId = whitelisted_caller(); - fund_account::(&caller, BalanceOf2::::from(10000u128)); + fund_account::(&caller, BalanceOf2::::from(100000u128)); let signer1: T::AccountId = benchmark_account("signer1", 0, SEED); let signer2: T::AccountId = benchmark_account("signer2", 1, SEED); let signer3: T::AccountId = benchmark_account("signer3", 2, SEED); - fund_account::(&signer1, BalanceOf2::::from(10000u128)); - fund_account::(&signer2, BalanceOf2::::from(10000u128)); - fund_account::(&signer3, BalanceOf2::::from(10000u128)); + fund_account::(&signer1, BalanceOf2::::from(100000u128)); + fund_account::(&signer2, BalanceOf2::::from(100000u128)); + fund_account::(&signer3, BalanceOf2::::from(100000u128)); let mut signers = vec![caller.clone(), signer1.clone(), signer2.clone(), signer3.clone()]; let threshold = 3u32; // Need 3 approvals @@ -159,16 +160,39 @@ mod benchmarks { signers: bounded_signers, threshold, nonce: 0, - proposal_nonce: 1, // We'll insert proposal with id 0 + proposal_nonce: e + 1, // We'll insert e expired proposals + 1 active creator: caller.clone(), deposit: T::MultisigDeposit::get(), last_activity: frame_system::Pallet::::block_number(), - active_proposals: 1, + active_proposals: e + 1, proposals_per_signer: BoundedBTreeMap::new(), }; Multisigs::::insert(&multisig_address, multisig_data); - // Directly insert proposal into storage with 1 approval + // Insert e expired proposals (worst case for auto-cleanup) + let expired_block = 10u32.into(); + for i in 0..e { + let system_call = frame_system::Call::::remark { remark: vec![i as u8; 10] }; + let call = ::RuntimeCall::from(system_call); + let encoded_call = call.encode(); + let bounded_call: BoundedCallOf = encoded_call.try_into().unwrap(); + let bounded_approvals: BoundedApprovalsOf = vec![caller.clone()].try_into().unwrap(); + + let proposal_data = ProposalDataOf:: { + proposer: caller.clone(), + call: bounded_call, + expiry: expired_block, + approvals: bounded_approvals, + deposit: 10u32.into(), + status: ProposalStatus::Active, + }; + Proposals::::insert(&multisig_address, i, proposal_data); + } + + // Move past expiry so proposals are expired + frame_system::Pallet::::set_block_number(100u32.into()); + + // Directly insert active proposal into storage with 1 approval // Create a remark call where the remark itself is c bytes let system_call = frame_system::Call::::remark { remark: vec![1u8; c as usize] }; let call = ::RuntimeCall::from(system_call); @@ -186,7 +210,7 @@ mod benchmarks { status: ProposalStatus::Active, }; - let proposal_id = 0u32; + let proposal_id = e; // Active proposal after expired ones Proposals::::insert(&multisig_address, proposal_id, proposal_data); #[extrinsic_call] @@ -271,15 +295,16 @@ mod benchmarks { #[benchmark] fn cancel( c: Linear<0, { T::MaxCallSize::get().saturating_sub(100) }>, + e: Linear<0, { T::MaxTotalProposalsInStorage::get() }>, // expired proposals to cleanup ) -> Result<(), BenchmarkError> { // Setup: Create multisig and proposal directly in storage let caller: T::AccountId = whitelisted_caller(); - fund_account::(&caller, BalanceOf2::::from(10000u128)); + fund_account::(&caller, BalanceOf2::::from(100000u128)); let signer1: T::AccountId = benchmark_account("signer1", 0, SEED); let signer2: T::AccountId = benchmark_account("signer2", 1, SEED); - fund_account::(&signer1, BalanceOf2::::from(10000u128)); - fund_account::(&signer2, BalanceOf2::::from(10000u128)); + fund_account::(&signer1, BalanceOf2::::from(100000u128)); + fund_account::(&signer2, BalanceOf2::::from(100000u128)); let mut signers = vec![caller.clone(), signer1.clone(), signer2.clone()]; let threshold = 2u32; @@ -294,16 +319,39 @@ mod benchmarks { signers: bounded_signers, threshold, nonce: 0, - proposal_nonce: 1, // We'll insert proposal with id 0 + proposal_nonce: e + 1, // We'll insert e expired proposals + 1 active creator: caller.clone(), deposit: T::MultisigDeposit::get(), last_activity: frame_system::Pallet::::block_number(), - active_proposals: 1, + active_proposals: e + 1, proposals_per_signer: BoundedBTreeMap::new(), }; Multisigs::::insert(&multisig_address, multisig_data); - // Directly insert proposal into storage + // Insert e expired proposals (worst case for auto-cleanup) + let expired_block = 10u32.into(); + for i in 0..e { + let system_call = frame_system::Call::::remark { remark: vec![i as u8; 10] }; + let call = ::RuntimeCall::from(system_call); + let encoded_call = call.encode(); + let bounded_call: BoundedCallOf = encoded_call.try_into().unwrap(); + let bounded_approvals: BoundedApprovalsOf = vec![caller.clone()].try_into().unwrap(); + + let proposal_data = ProposalDataOf:: { + proposer: caller.clone(), + call: bounded_call, + expiry: expired_block, + approvals: bounded_approvals, + deposit: 10u32.into(), + status: ProposalStatus::Active, + }; + Proposals::::insert(&multisig_address, i, proposal_data); + } + + // Move past expiry so proposals are expired + frame_system::Pallet::::set_block_number(100u32.into()); + + // Directly insert active proposal into storage // Create a remark call where the remark itself is c bytes let system_call = frame_system::Call::::remark { remark: vec![1u8; c as usize] }; let call = ::RuntimeCall::from(system_call); @@ -321,7 +369,7 @@ mod benchmarks { status: ProposalStatus::Active, }; - let proposal_id = 0u32; + let proposal_id = e; // Active proposal after expired ones Proposals::::insert(&multisig_address, proposal_id, proposal_data); #[extrinsic_call] diff --git a/pallets/multisig/src/lib.rs b/pallets/multisig/src/lib.rs index 2ac3fda1..3befe0fc 100644 --- a/pallets/multisig/src/lib.rs +++ b/pallets/multisig/src/lib.rs @@ -482,11 +482,14 @@ pub mod pallet { /// - `expiry`: Block number when this proposal expires /// /// The proposer must be a signer and must pay: - /// - A deposit (locked until proposal is removed after grace period) + /// - A deposit (refundable - returned immediately on execution/cancellation) /// - A fee (non-refundable, burned immediately) /// - /// The proposal remains in storage even after execution/cancellation. - /// Use `remove_expired()` or `claim_deposits()` after grace period to recover the deposit. + /// **Auto-cleanup:** Before creating a new proposal, ALL expired proposals are + /// automatically removed and deposits returned to original proposers. This is the primary + /// cleanup mechanism. + /// + /// **For threshold=1:** If the multisig threshold is 1, the proposal executes immediately. #[pallet::call_index(1)] #[pallet::weight(::WeightInfo::propose( call.len() as u32, @@ -500,48 +503,19 @@ pub mod pallet { ) -> DispatchResult { let proposer = ensure_signed(origin)?; - // Check if proposer is a signer and active proposals limit + // Check if proposer is a signer let multisig_data = Multisigs::::get(&multisig_address).ok_or(Error::::MultisigNotFound)?; ensure!(multisig_data.signers.contains(&proposer), Error::::NotASigner); // Auto-cleanup expired proposals before creating new one - // This ensures storage is managed proactively during normal operation - let current_block = frame_system::Pallet::::block_number(); - let expired_proposals: Vec<(u32, T::AccountId, BalanceOf)> = - Proposals::::iter_prefix(&multisig_address) - .filter_map(|(id, proposal)| { - if proposal.status == ProposalStatus::Active && - current_block > proposal.expiry - { - Some((id, proposal.proposer, proposal.deposit)) - } else { - None - } - }) - .collect(); - - // Remove expired proposals and return deposits - for (id, expired_proposer, deposit) in expired_proposals.iter() { - Self::remove_proposal_and_return_deposit( - &multisig_address, - *id, - expired_proposer, - *deposit, - ); - - // Emit event for each removed proposal - Self::deposit_event(Event::ProposalRemoved { - multisig_address: multisig_address.clone(), - proposal_id: *id, - proposer: expired_proposer.clone(), - removed_by: proposer.clone(), - }); - } + // This is the primary cleanup mechanism for active multisigs + Self::auto_cleanup_expired_proposals(&multisig_address, &proposer); // Reload multisig data after potential cleanup let multisig_data = Multisigs::::get(&multisig_address).ok_or(Error::::MultisigNotFound)?; + let current_block = frame_system::Pallet::::block_number(); // Get signers count (used for multiple checks below) let signers_count = multisig_data.signers.len() as u32; @@ -679,13 +653,19 @@ pub mod pallet { /// If this approval brings the total approvals to or above the threshold, /// the transaction will be automatically executed. /// + /// **Auto-cleanup:** Before processing the approval, ALL expired proposals are + /// automatically removed and deposits returned to original proposers. + /// /// Parameters: /// - `multisig_address`: The multisig account /// - `proposal_id`: ID (nonce) of the proposal to approve /// - /// Weight: Charges for MAX call size, but refunds based on actual call size + /// Weight: Charges for MAX call size and MAX expired proposals, refunds based on actual #[pallet::call_index(2)] - #[pallet::weight(::WeightInfo::approve(T::MaxCallSize::get()))] + #[pallet::weight(::WeightInfo::approve( + T::MaxCallSize::get(), + T::MaxTotalProposalsInStorage::get() + ))] #[allow(clippy::useless_conversion)] pub fn approve( origin: OriginFor, @@ -697,13 +677,19 @@ pub mod pallet { // Check if approver is a signer let multisig_data = Self::ensure_is_signer(&multisig_address, &approver)?; + // Auto-cleanup expired proposals on any multisig activity + // Returns count of proposals in storage (which determines iteration cost) + let iterated_count = Self::auto_cleanup_expired_proposals(&multisig_address, &approver); + // Get proposal let mut proposal = Proposals::::get(&multisig_address, proposal_id) .ok_or(Error::::ProposalNotFound)?; - // Calculate actual weight based on real call size (for refund) + // Calculate actual weight based on real call size and actual storage size + // We charge for worst-case (e=Max), but refund based on actual storage size let actual_call_size = proposal.call.len() as u32; - let actual_weight = ::WeightInfo::approve(actual_call_size); + let actual_weight = + ::WeightInfo::approve(actual_call_size, iterated_count); // Check if not expired let current_block = frame_system::Pallet::::block_number(); @@ -750,13 +736,19 @@ pub mod pallet { /// Cancel a proposed transaction (only by proposer) /// + /// **Auto-cleanup:** Before processing the cancellation, ALL expired proposals are + /// automatically removed and deposits returned to original proposers. + /// /// Parameters: /// - `multisig_address`: The multisig account /// - `proposal_id`: ID (nonce) of the proposal to cancel /// - /// Weight: Charges for MAX call size, but refunds based on actual call size + /// Weight: Charges for MAX call size and MAX expired proposals, refunds based on actual #[pallet::call_index(3)] - #[pallet::weight(::WeightInfo::cancel(T::MaxCallSize::get()))] + #[pallet::weight(::WeightInfo::cancel( + T::MaxCallSize::get(), + T::MaxTotalProposalsInStorage::get() + ))] #[allow(clippy::useless_conversion)] pub fn cancel( origin: OriginFor, @@ -765,13 +757,19 @@ pub mod pallet { ) -> DispatchResultWithPostInfo { let canceller = ensure_signed(origin)?; + // Auto-cleanup expired proposals on any multisig activity + // Returns count of proposals in storage (which determines iteration cost) + let iterated_count = + Self::auto_cleanup_expired_proposals(&multisig_address, &canceller); + // Get proposal let proposal = Proposals::::get(&multisig_address, proposal_id) .ok_or(Error::::ProposalNotFound)?; - // Calculate actual weight based on real call size (for refund) + // Calculate actual weight based on real call size and actual storage size + // We charge for worst-case (e=Max), but refund based on actual storage size let actual_call_size = proposal.call.len() as u32; - let actual_weight = ::WeightInfo::cancel(actual_call_size); + let actual_weight = ::WeightInfo::cancel(actual_call_size, iterated_count); // Check if caller is the proposer ensure!(canceller == proposal.proposer, Error::::NotProposer); @@ -1019,6 +1017,48 @@ pub mod pallet { Ok(multisig_data) } + /// Auto-cleanup expired proposals at the start of any multisig activity + /// This is the primary cleanup mechanism for active multisigs + /// Returns deposits to original proposers and emits cleanup events + fn auto_cleanup_expired_proposals( + multisig_address: &T::AccountId, + caller: &T::AccountId, + ) -> u32 { + let current_block = frame_system::Pallet::::block_number(); + let mut iterated_count = 0u32; + let mut expired_proposals: Vec<(u32, T::AccountId, BalanceOf)> = Vec::new(); + + // Iterate through all proposals to count them AND identify expired ones + for (id, proposal) in Proposals::::iter_prefix(multisig_address) { + iterated_count += 1; + if proposal.status == ProposalStatus::Active && current_block > proposal.expiry { + expired_proposals.push((id, proposal.proposer, proposal.deposit)); + } + } + + // Remove expired proposals and return deposits + for (id, expired_proposer, deposit) in expired_proposals.iter() { + Self::remove_proposal_and_return_deposit( + multisig_address, + *id, + expired_proposer, + *deposit, + ); + + // Emit event for each removed proposal + Self::deposit_event(Event::ProposalRemoved { + multisig_address: multisig_address.clone(), + proposal_id: *id, + proposer: expired_proposer.clone(), + removed_by: caller.clone(), + }); + } + + // Return total number of proposals iterated (not cleaned) + // This reflects the actual storage read cost + iterated_count + } + /// Decrement proposal counters (active_proposals and per-signer counter) /// Used when removing proposals from storage fn decrement_proposal_counters(multisig_address: &T::AccountId, proposer: &T::AccountId) { diff --git a/pallets/multisig/src/tests.rs b/pallets/multisig/src/tests.rs index d0d4aa5b..277672ac 100644 --- a/pallets/multisig/src/tests.rs +++ b/pallets/multisig/src/tests.rs @@ -1139,7 +1139,7 @@ fn propose_with_threshold_one_executes_immediately() { // Fund multisig account for balance transfer as Mutate<_>>::mint_into(&multisig_address, 50000).unwrap(); - let initial_dave_balance = Balances::free_balance(&dave()); + let initial_dave_balance = Balances::free_balance(dave()); // Alice proposes a transfer - should execute immediately since threshold=1 let transfer_call = RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive { @@ -1160,7 +1160,7 @@ fn propose_with_threshold_one_executes_immediately() { assert!(Proposals::::get(&multisig_address, proposal_id).is_none()); // Verify the transfer actually happened - assert_eq!(Balances::free_balance(&dave()), initial_dave_balance + 1000); + assert_eq!(Balances::free_balance(dave()), initial_dave_balance + 1000); // Verify ProposalExecuted event was emitted System::assert_has_event( @@ -1176,7 +1176,7 @@ fn propose_with_threshold_one_executes_immediately() { ); // Verify deposit was returned to Alice (execution removes proposal) - let alice_reserved = Balances::reserved_balance(&alice()); + let alice_reserved = Balances::reserved_balance(alice()); assert_eq!(alice_reserved, 500); // Only MultisigDeposit, no ProposalDeposit // Verify active_proposals counter was decremented back to 0 @@ -1206,7 +1206,7 @@ fn propose_with_threshold_two_waits_for_approval() { // Fund multisig account as Mutate<_>>::mint_into(&multisig_address, 50000).unwrap(); - let initial_dave_balance = Balances::free_balance(&dave()); + let initial_dave_balance = Balances::free_balance(dave()); // Alice proposes a transfer - should NOT execute yet let transfer_call = RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive { @@ -1229,7 +1229,7 @@ fn propose_with_threshold_two_waits_for_approval() { assert_eq!(proposal.approvals.len(), 1); // Only Alice so far // Verify the transfer did NOT happen yet - assert_eq!(Balances::free_balance(&dave()), initial_dave_balance); + assert_eq!(Balances::free_balance(dave()), initial_dave_balance); // Bob approves - NOW it should execute (threshold=2 reached) assert_ok!(Multisig::approve( @@ -1242,6 +1242,84 @@ fn propose_with_threshold_two_waits_for_approval() { assert!(Proposals::::get(&multisig_address, proposal_id).is_none()); // Verify the transfer happened - assert_eq!(Balances::free_balance(&dave()), initial_dave_balance + 1000); + assert_eq!(Balances::free_balance(dave()), initial_dave_balance + 1000); + }); +} + +#[test] +fn auto_cleanup_on_approve_and_cancel() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + let creator = alice(); + let signers = vec![alice(), bob(), charlie()]; + let threshold = 3; // Need all 3 signers - prevents auto-execution during test + + // Create multisig + assert_ok!(Multisig::create_multisig( + RuntimeOrigin::signed(creator.clone()), + signers.clone(), + threshold + )); + + let multisig_address = Multisig::derive_multisig_address(&signers, 0); + + // Create two proposals + assert_ok!(Multisig::propose( + RuntimeOrigin::signed(alice()), + multisig_address.clone(), + make_call(vec![1]), + 100 // expires at block 100 + )); + + assert_ok!(Multisig::propose( + RuntimeOrigin::signed(bob()), + multisig_address.clone(), + make_call(vec![2]), + 200 // expires at block 200 + )); + + // Verify both proposals exist + assert!(Proposals::::get(&multisig_address, 0).is_some()); + assert!(Proposals::::get(&multisig_address, 1).is_some()); + + // Move time forward past first proposal expiry + System::set_block_number(101); + + // Charlie approves proposal #1 (should trigger auto-cleanup of proposal #0) + // Note: Bob is the proposer of #1, so Charlie must approve + assert_ok!(Multisig::approve( + RuntimeOrigin::signed(charlie()), + multisig_address.clone(), + 1 + )); + + // Verify proposal #0 was auto-cleaned + assert!(Proposals::::get(&multisig_address, 0).is_none()); + // Proposal #1 still exists (not expired, waiting for approval) + assert!(Proposals::::get(&multisig_address, 1).is_some()); + + // Create another proposal that will expire + assert_ok!(Multisig::propose( + RuntimeOrigin::signed(alice()), + multisig_address.clone(), + make_call(vec![3]), + 150 // expires at block 150 + )); + + // Move time forward past proposal #2 expiry + System::set_block_number(151); + + // Charlie cancels proposal #1 (should trigger auto-cleanup of proposal #2) + assert_ok!(Multisig::cancel(RuntimeOrigin::signed(bob()), multisig_address.clone(), 1)); + + // Verify proposal #2 was auto-cleaned + assert!(Proposals::::get(&multisig_address, 2).is_none()); + // Proposal #1 was cancelled + assert!(Proposals::::get(&multisig_address, 1).is_none()); + + // Verify active_proposals counter is correct (should be 0) + let multisig_data = Multisigs::::get(&multisig_address).unwrap(); + assert_eq!(multisig_data.active_proposals, 0); }); } diff --git a/pallets/multisig/src/weights.rs b/pallets/multisig/src/weights.rs index 140a6e07..65a25230 100644 --- a/pallets/multisig/src/weights.rs +++ b/pallets/multisig/src/weights.rs @@ -19,7 +19,7 @@ //! Autogenerated weights for `pallet_multisig` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 49.1.0 -//! DATE: 2026-01-27, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-01-28, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `coldbook.local`, CPU: `` //! WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` @@ -49,9 +49,9 @@ use core::marker::PhantomData; pub trait WeightInfo { fn create_multisig() -> Weight; fn propose(c: u32, e: u32, ) -> Weight; - fn approve(c: u32, ) -> Weight; + fn approve(c: u32, e: u32, ) -> Weight; fn approve_and_execute(c: u32, ) -> Weight; - fn cancel(c: u32, ) -> Weight; + fn cancel(c: u32, e: u32, ) -> Weight; fn remove_expired() -> Weight; fn claim_deposits(p: u32, ) -> Weight; fn dissolve_multisig() -> Weight; @@ -69,7 +69,7 @@ impl WeightInfo for SubstrateWeight { // Measured: `152` // Estimated: `10389` // Minimum execution time: 192_000_000 picoseconds. - Weight::from_parts(197_000_000, 10389) + Weight::from_parts(195_000_000, 10389) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -84,9 +84,9 @@ impl WeightInfo for SubstrateWeight { // Measured: `458 + e * (215 ±0)` // Estimated: `17022 + e * (16032 ±0)` // Minimum execution time: 40_000_000 picoseconds. - Weight::from_parts(162_343_032, 17022) - // Standard Error: 41_034 - .saturating_add(Weight::from_parts(14_232_109, 0).saturating_mul(e.into())) + Weight::from_parts(140_354_473, 17022) + // Standard Error: 30_916 + .saturating_add(Weight::from_parts(14_183_732, 0).saturating_mul(e.into())) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(e.into()))) .saturating_add(T::DbWeight::get().writes(2_u64)) @@ -95,51 +95,61 @@ impl WeightInfo for SubstrateWeight { } /// Storage: `Multisig::Multisigs` (r:1 w:1) /// Proof: `Multisig::Multisigs` (`max_values`: None, `max_size`: Some(6924), added: 9399, mode: `MaxEncodedLen`) - /// Storage: `Multisig::Proposals` (r:1 w:1) + /// Storage: `Multisig::Proposals` (r:202 w:201) /// Proof: `Multisig::Proposals` (`max_values`: None, `max_size`: Some(13557), added: 16032, mode: `MaxEncodedLen`) /// The range of component `c` is `[0, 10140]`. - fn approve(c: u32, ) -> Weight { + /// The range of component `e` is `[0, 200]`. + fn approve(_c: u32, e: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `766 + c * (1 ±0)` - // Estimated: `17022` - // Minimum execution time: 14_000_000 picoseconds. - Weight::from_parts(16_119_988, 17022) - // Standard Error: 42 - .saturating_add(Weight::from_parts(766, 0).saturating_mul(c.into())) - .saturating_add(T::DbWeight::get().reads(2_u64)) + // Measured: `657 + c * (1 ±0) + e * (215 ±0)` + // Estimated: `33054 + e * (16032 ±0)` + // Minimum execution time: 23_000_000 picoseconds. + Weight::from_parts(31_012_674, 33054) + // Standard Error: 25_877 + .saturating_add(Weight::from_parts(13_708_908, 0).saturating_mul(e.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(e.into()))) .saturating_add(T::DbWeight::get().writes(2_u64)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(e.into()))) + .saturating_add(Weight::from_parts(0, 16032).saturating_mul(e.into())) } /// Storage: `Multisig::Multisigs` (r:1 w:1) /// Proof: `Multisig::Multisigs` (`max_values`: None, `max_size`: Some(6924), added: 9399, mode: `MaxEncodedLen`) - /// Storage: `Multisig::Proposals` (r:1 w:1) + /// Storage: `Multisig::Proposals` (r:2 w:1) /// Proof: `Multisig::Proposals` (`max_values`: None, `max_size`: Some(13557), added: 16032, mode: `MaxEncodedLen`) /// The range of component `c` is `[0, 10140]`. fn approve_and_execute(c: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `734 + c * (1 ±0)` - // Estimated: `17022` - // Minimum execution time: 25_000_000 picoseconds. - Weight::from_parts(33_804_547, 17022) - // Standard Error: 105 - .saturating_add(Weight::from_parts(802, 0).saturating_mul(c.into())) - .saturating_add(T::DbWeight::get().reads(2_u64)) + // Measured: `790 + c * (1 ±0)` + // Estimated: `33054` + // Minimum execution time: 29_000_000 picoseconds. + Weight::from_parts(29_907_548, 33054) + // Standard Error: 17 + .saturating_add(Weight::from_parts(782, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } - /// Storage: `Multisig::Proposals` (r:1 w:1) + /// Storage: `Multisig::Proposals` (r:202 w:201) /// Proof: `Multisig::Proposals` (`max_values`: None, `max_size`: Some(13557), added: 16032, mode: `MaxEncodedLen`) /// Storage: `Multisig::Multisigs` (r:1 w:1) /// Proof: `Multisig::Multisigs` (`max_values`: None, `max_size`: Some(6924), added: 9399, mode: `MaxEncodedLen`) /// The range of component `c` is `[0, 10140]`. - fn cancel(c: u32, ) -> Weight { + /// The range of component `e` is `[0, 200]`. + fn cancel(c: u32, e: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `734 + c * (1 ±0)` - // Estimated: `17022` - // Minimum execution time: 19_000_000 picoseconds. - Weight::from_parts(24_824_223, 17022) - // Standard Error: 65 - .saturating_add(Weight::from_parts(117, 0).saturating_mul(c.into())) - .saturating_add(T::DbWeight::get().reads(2_u64)) + // Measured: `625 + c * (1 ±0) + e * (215 ±0)` + // Estimated: `33054 + e * (16032 ±0)` + // Minimum execution time: 27_000_000 picoseconds. + Weight::from_parts(22_414_315, 33054) + // Standard Error: 576 + .saturating_add(Weight::from_parts(1_526, 0).saturating_mul(c.into())) + // Standard Error: 29_178 + .saturating_add(Weight::from_parts(13_866_655, 0).saturating_mul(e.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(e.into()))) .saturating_add(T::DbWeight::get().writes(2_u64)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(e.into()))) + .saturating_add(Weight::from_parts(0, 16032).saturating_mul(e.into())) } /// Storage: `Multisig::Multisigs` (r:1 w:1) /// Proof: `Multisig::Multisigs` (`max_values`: None, `max_size`: Some(6924), added: 9399, mode: `MaxEncodedLen`) @@ -149,8 +159,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `764` // Estimated: `17022` - // Minimum execution time: 20_000_000 picoseconds. - Weight::from_parts(24_000_000, 17022) + // Minimum execution time: 21_000_000 picoseconds. + Weight::from_parts(23_000_000, 17022) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -163,10 +173,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `625 + p * (237 ±0)` // Estimated: `17022 + p * (16032 ±0)` - // Minimum execution time: 24_000_000 picoseconds. - Weight::from_parts(27_900_496, 17022) - // Standard Error: 31_493 - .saturating_add(Weight::from_parts(13_930_528, 0).saturating_mul(p.into())) + // Minimum execution time: 23_000_000 picoseconds. + Weight::from_parts(28_491_742, 17022) + // Standard Error: 16_103 + .saturating_add(Weight::from_parts(13_535_595, 0).saturating_mul(p.into())) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(p.into()))) .saturating_add(T::DbWeight::get().writes(1_u64)) @@ -184,7 +194,7 @@ impl WeightInfo for SubstrateWeight { // Measured: `538` // Estimated: `17022` // Minimum execution time: 20_000_000 picoseconds. - Weight::from_parts(20_000_000, 17022) + Weight::from_parts(30_000_000, 17022) .saturating_add(T::DbWeight::get().reads(3_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -201,7 +211,7 @@ impl WeightInfo for () { // Measured: `152` // Estimated: `10389` // Minimum execution time: 192_000_000 picoseconds. - Weight::from_parts(197_000_000, 10389) + Weight::from_parts(195_000_000, 10389) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -216,9 +226,9 @@ impl WeightInfo for () { // Measured: `458 + e * (215 ±0)` // Estimated: `17022 + e * (16032 ±0)` // Minimum execution time: 40_000_000 picoseconds. - Weight::from_parts(162_343_032, 17022) - // Standard Error: 41_034 - .saturating_add(Weight::from_parts(14_232_109, 0).saturating_mul(e.into())) + Weight::from_parts(140_354_473, 17022) + // Standard Error: 30_916 + .saturating_add(Weight::from_parts(14_183_732, 0).saturating_mul(e.into())) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(e.into()))) .saturating_add(RocksDbWeight::get().writes(2_u64)) @@ -227,51 +237,61 @@ impl WeightInfo for () { } /// Storage: `Multisig::Multisigs` (r:1 w:1) /// Proof: `Multisig::Multisigs` (`max_values`: None, `max_size`: Some(6924), added: 9399, mode: `MaxEncodedLen`) - /// Storage: `Multisig::Proposals` (r:1 w:1) + /// Storage: `Multisig::Proposals` (r:202 w:201) /// Proof: `Multisig::Proposals` (`max_values`: None, `max_size`: Some(13557), added: 16032, mode: `MaxEncodedLen`) /// The range of component `c` is `[0, 10140]`. - fn approve(c: u32, ) -> Weight { + /// The range of component `e` is `[0, 200]`. + fn approve(_c: u32, e: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `766 + c * (1 ±0)` - // Estimated: `17022` - // Minimum execution time: 14_000_000 picoseconds. - Weight::from_parts(16_119_988, 17022) - // Standard Error: 42 - .saturating_add(Weight::from_parts(766, 0).saturating_mul(c.into())) - .saturating_add(RocksDbWeight::get().reads(2_u64)) + // Measured: `657 + c * (1 ±0) + e * (215 ±0)` + // Estimated: `33054 + e * (16032 ±0)` + // Minimum execution time: 23_000_000 picoseconds. + Weight::from_parts(31_012_674, 33054) + // Standard Error: 25_877 + .saturating_add(Weight::from_parts(13_708_908, 0).saturating_mul(e.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(e.into()))) .saturating_add(RocksDbWeight::get().writes(2_u64)) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(e.into()))) + .saturating_add(Weight::from_parts(0, 16032).saturating_mul(e.into())) } /// Storage: `Multisig::Multisigs` (r:1 w:1) /// Proof: `Multisig::Multisigs` (`max_values`: None, `max_size`: Some(6924), added: 9399, mode: `MaxEncodedLen`) - /// Storage: `Multisig::Proposals` (r:1 w:1) + /// Storage: `Multisig::Proposals` (r:2 w:1) /// Proof: `Multisig::Proposals` (`max_values`: None, `max_size`: Some(13557), added: 16032, mode: `MaxEncodedLen`) /// The range of component `c` is `[0, 10140]`. fn approve_and_execute(c: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `734 + c * (1 ±0)` - // Estimated: `17022` - // Minimum execution time: 25_000_000 picoseconds. - Weight::from_parts(33_804_547, 17022) - // Standard Error: 105 - .saturating_add(Weight::from_parts(802, 0).saturating_mul(c.into())) - .saturating_add(RocksDbWeight::get().reads(2_u64)) + // Measured: `790 + c * (1 ±0)` + // Estimated: `33054` + // Minimum execution time: 29_000_000 picoseconds. + Weight::from_parts(29_907_548, 33054) + // Standard Error: 17 + .saturating_add(Weight::from_parts(782, 0).saturating_mul(c.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } - /// Storage: `Multisig::Proposals` (r:1 w:1) + /// Storage: `Multisig::Proposals` (r:202 w:201) /// Proof: `Multisig::Proposals` (`max_values`: None, `max_size`: Some(13557), added: 16032, mode: `MaxEncodedLen`) /// Storage: `Multisig::Multisigs` (r:1 w:1) /// Proof: `Multisig::Multisigs` (`max_values`: None, `max_size`: Some(6924), added: 9399, mode: `MaxEncodedLen`) /// The range of component `c` is `[0, 10140]`. - fn cancel(c: u32, ) -> Weight { + /// The range of component `e` is `[0, 200]`. + fn cancel(c: u32, e: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `734 + c * (1 ±0)` - // Estimated: `17022` - // Minimum execution time: 19_000_000 picoseconds. - Weight::from_parts(24_824_223, 17022) - // Standard Error: 65 - .saturating_add(Weight::from_parts(117, 0).saturating_mul(c.into())) - .saturating_add(RocksDbWeight::get().reads(2_u64)) + // Measured: `625 + c * (1 ±0) + e * (215 ±0)` + // Estimated: `33054 + e * (16032 ±0)` + // Minimum execution time: 27_000_000 picoseconds. + Weight::from_parts(22_414_315, 33054) + // Standard Error: 576 + .saturating_add(Weight::from_parts(1_526, 0).saturating_mul(c.into())) + // Standard Error: 29_178 + .saturating_add(Weight::from_parts(13_866_655, 0).saturating_mul(e.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(e.into()))) .saturating_add(RocksDbWeight::get().writes(2_u64)) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(e.into()))) + .saturating_add(Weight::from_parts(0, 16032).saturating_mul(e.into())) } /// Storage: `Multisig::Multisigs` (r:1 w:1) /// Proof: `Multisig::Multisigs` (`max_values`: None, `max_size`: Some(6924), added: 9399, mode: `MaxEncodedLen`) @@ -281,8 +301,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `764` // Estimated: `17022` - // Minimum execution time: 20_000_000 picoseconds. - Weight::from_parts(24_000_000, 17022) + // Minimum execution time: 21_000_000 picoseconds. + Weight::from_parts(23_000_000, 17022) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -295,10 +315,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `625 + p * (237 ±0)` // Estimated: `17022 + p * (16032 ±0)` - // Minimum execution time: 24_000_000 picoseconds. - Weight::from_parts(27_900_496, 17022) - // Standard Error: 31_493 - .saturating_add(Weight::from_parts(13_930_528, 0).saturating_mul(p.into())) + // Minimum execution time: 23_000_000 picoseconds. + Weight::from_parts(28_491_742, 17022) + // Standard Error: 16_103 + .saturating_add(Weight::from_parts(13_535_595, 0).saturating_mul(p.into())) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(p.into()))) .saturating_add(RocksDbWeight::get().writes(1_u64)) @@ -316,7 +336,7 @@ impl WeightInfo for () { // Measured: `538` // Estimated: `17022` // Minimum execution time: 20_000_000 picoseconds. - Weight::from_parts(20_000_000, 17022) + Weight::from_parts(30_000_000, 17022) .saturating_add(RocksDbWeight::get().reads(3_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } diff --git a/pallets/reversible-transfers/src/lib.rs b/pallets/reversible-transfers/src/lib.rs index a01dfb4c..de235288 100644 --- a/pallets/reversible-transfers/src/lib.rs +++ b/pallets/reversible-transfers/src/lib.rs @@ -488,6 +488,7 @@ pub mod pallet { /// This is an emergency function for when the high security account may be compromised. #[pallet::call_index(7)] #[pallet::weight(::WeightInfo::recover_funds())] + #[allow(clippy::useless_conversion)] pub fn recover_funds( origin: OriginFor, account: T::AccountId, From ab56c6ef5fbc7fb4d9d5aafffa5b67a28ad8c911 Mon Sep 17 00:00:00 2001 From: illuzen Date: Thu, 29 Jan 2026 15:44:58 +0800 Subject: [PATCH 24/27] QUIC miner (#363) * quic implementation * refactor for readability * simplify * miner initiates, multiple miners supported * simplify loop further * short job counters * simplify new_full * gracefully handle invalid seals * remove unused and log misbehaving miners * emoji * fmt * taplo * improve readability, logs, documentation --- Cargo.lock | 255 ++++----- Cargo.toml | 1 + EXTERNAL_MINER_PROTOCOL.md | 461 ++++++++-------- client/cli/Cargo.toml | 2 - client/consensus/qpow/Cargo.toml | 6 - client/consensus/qpow/src/worker.rs | 35 ++ client/network/Cargo.toml | 1 - miner-api/Cargo.toml | 2 + miner-api/src/lib.rs | 75 ++- node/Cargo.toml | 9 +- node/src/cli.rs | 7 +- node/src/command.rs | 4 +- node/src/external_miner_client.rs | 110 ---- node/src/main.rs | 2 +- node/src/miner_server.rs | 348 ++++++++++++ node/src/prometheus.rs | 4 +- node/src/service.rs | 792 ++++++++++++++++++---------- qpow-math/Cargo.toml | 2 - qpow-math/src/lib.rs | 43 -- 19 files changed, 1327 insertions(+), 832 deletions(-) delete mode 100644 node/src/external_miner_client.rs create mode 100644 node/src/miner_server.rs diff --git a/Cargo.lock b/Cargo.lock index 9f186480..1adf393e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2953,15 +2953,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - [[package]] name = "enum-as-inner" version = "0.6.1" @@ -3842,7 +3833,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" dependencies = [ "futures-io", - "rustls", + "rustls 0.23.32", "rustls-pki-types", ] @@ -4498,8 +4489,8 @@ dependencies = [ "hyper 1.7.0", "hyper-util", "log", - "rustls", - "rustls-native-certs", + "rustls 0.23.32", + "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio 1.47.1", "tokio-rustls", @@ -4692,7 +4683,7 @@ dependencies = [ "netlink-proto", "netlink-sys", "rtnetlink", - "system-configuration 0.6.1", + "system-configuration", "tokio 1.47.1", "windows 0.53.0", ] @@ -5056,7 +5047,7 @@ dependencies = [ "http 1.3.1", "jsonrpsee-core", "pin-project", - "rustls", + "rustls 0.23.32", "rustls-pki-types", "rustls-platform-verifier", "soketto", @@ -5577,10 +5568,10 @@ dependencies = [ "libp2p-identity", "libp2p-tls", "parking_lot 0.12.4", - "quinn", + "quinn 0.11.9", "rand 0.8.5", "ring 0.17.14", - "rustls", + "rustls 0.23.32", "socket2 0.5.10", "thiserror 1.0.69", "tokio 1.47.1", @@ -5672,7 +5663,7 @@ dependencies = [ "libp2p-identity", "rcgen", "ring 0.17.14", - "rustls", + "rustls 0.23.32", "rustls-webpki 0.101.7", "thiserror 1.0.69", "x509-parser 0.16.0", @@ -6164,12 +6155,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -9070,8 +9055,6 @@ version = "0.1.0" dependencies = [ "hex", "log", - "num-bigint", - "num-traits", "primitive-types 0.13.1", "qp-poseidon-core", ] @@ -9096,6 +9079,8 @@ name = "quantus-miner-api" version = "0.0.3" dependencies = [ "serde", + "serde_json", + "tokio 1.47.1", ] [[package]] @@ -9115,15 +9100,16 @@ dependencies = [ "parity-scale-codec", "prometheus", "qp-dilithium-crypto", - "qp-rusty-crystals-dilithium", "qp-rusty-crystals-hdwallet", "qp-wormhole-circuit-builder", "qp-wormhole-verifier", "qpow-math", "quantus-miner-api", "quantus-runtime", + "quinn 0.10.2", "rand 0.8.5", - "reqwest", + "rcgen", + "rustls 0.21.12", "sc-basic-authorship", "sc-cli", "sc-client-api", @@ -9131,6 +9117,7 @@ dependencies = [ "sc-consensus-qpow", "sc-executor", "sc-network", + "sc-network-sync", "sc-offchain", "sc-service", "sc-telemetry", @@ -9153,7 +9140,6 @@ dependencies = [ "substrate-build-script-utils", "substrate-frame-rpc-system", "tokio-util", - "uuid", ] [[package]] @@ -9243,6 +9229,23 @@ dependencies = [ "unsigned-varint 0.8.0", ] +[[package]] +name = "quinn" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cc2c5017e4b43d5995dcea317bc46c1e09404c0a9664d2908f7f02dfe943d75" +dependencies = [ + "bytes 1.10.1", + "pin-project-lite 0.2.16", + "quinn-proto 0.10.6", + "quinn-udp 0.4.1", + "rustc-hash 1.1.0", + "rustls 0.21.12", + "thiserror 1.0.69", + "tokio 1.47.1", + "tracing", +] + [[package]] name = "quinn" version = "0.11.9" @@ -9253,10 +9256,10 @@ dependencies = [ "cfg_aliases 0.2.1", "futures-io", "pin-project-lite 0.2.16", - "quinn-proto", - "quinn-udp", + "quinn-proto 0.11.13", + "quinn-udp 0.5.14", "rustc-hash 2.1.1", - "rustls", + "rustls 0.23.32", "socket2 0.6.0", "thiserror 2.0.16", "tokio 1.47.1", @@ -9264,6 +9267,24 @@ dependencies = [ "web-time", ] +[[package]] +name = "quinn-proto" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "141bf7dfde2fbc246bfd3fe12f2455aa24b0fbd9af535d8c86c7bd1381ff2b1a" +dependencies = [ + "bytes 1.10.1", + "rand 0.8.5", + "ring 0.16.20", + "rustc-hash 1.1.0", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", + "slab", + "thiserror 1.0.69", + "tinyvec", + "tracing", +] + [[package]] name = "quinn-proto" version = "0.11.13" @@ -9276,7 +9297,7 @@ dependencies = [ "rand 0.9.2", "ring 0.17.14", "rustc-hash 2.1.1", - "rustls", + "rustls 0.23.32", "rustls-pki-types", "slab", "thiserror 2.0.16", @@ -9285,6 +9306,19 @@ dependencies = [ "web-time", ] +[[package]] +name = "quinn-udp" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "055b4e778e8feb9f93c4e439f71dc2156ef13360b432b799e179a8c4cdf0b1d7" +dependencies = [ + "bytes 1.10.1", + "libc", + "socket2 0.5.10", + "tracing", + "windows-sys 0.48.0", +] + [[package]] name = "quinn-udp" version = "0.5.14" @@ -9574,42 +9608,6 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" -[[package]] -name = "reqwest" -version = "0.11.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" -dependencies = [ - "base64 0.21.7", - "bytes 1.10.1", - "encoding_rs", - "futures-core", - "futures-util", - "h2 0.3.27", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", - "ipnet", - "js-sys", - "log", - "mime", - "once_cell", - "percent-encoding", - "pin-project-lite 0.2.16", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "system-configuration 0.5.1", - "tokio 1.47.1", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "winreg", -] - [[package]] name = "resolv-conf" version = "0.7.5" @@ -9850,6 +9848,17 @@ dependencies = [ "windows-sys 0.61.0", ] +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "ring 0.17.14", + "rustls-webpki 0.101.7", + "sct", +] + [[package]] name = "rustls" version = "0.23.32" @@ -9865,6 +9874,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework 2.11.1", +] + [[package]] name = "rustls-native-certs" version = "0.8.1" @@ -9874,7 +9895,16 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.5.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", ] [[package]] @@ -9898,11 +9928,11 @@ dependencies = [ "jni", "log", "once_cell", - "rustls", - "rustls-native-certs", + "rustls 0.23.32", + "rustls-native-certs 0.8.1", "rustls-platform-verifier-android", "rustls-webpki 0.103.6", - "security-framework", + "security-framework 3.5.0", "security-framework-sys", "webpki-root-certs 0.26.11", "windows-sys 0.59.0", @@ -10147,7 +10177,6 @@ dependencies = [ "parity-scale-codec", "qp-dilithium-crypto", "qp-rusty-crystals-dilithium", - "qp-rusty-crystals-hdwallet", "rand 0.8.5", "regex", "rpassword", @@ -10166,7 +10195,6 @@ dependencies = [ "sp-blockchain", "sp-core", "sp-keyring", - "sp-keystore", "sp-panic-handler", "sp-runtime", "sp-tracing", @@ -10322,9 +10350,6 @@ dependencies = [ "sc-client-api", "sc-consensus", "sc-service", - "scale-info", - "sha2 0.10.9", - "sha3", "sp-api", "sp-block-builder", "sp-blockchain", @@ -10513,7 +10538,6 @@ dependencies = [ "log", "mockall", "multistream-select", - "once_cell", "parity-scale-codec", "parking_lot 0.12.4", "partial_sort", @@ -10675,7 +10699,7 @@ dependencies = [ "parity-scale-codec", "parking_lot 0.12.4", "rand 0.8.5", - "rustls", + "rustls 0.23.32", "sc-client-api", "sc-network", "sc-network-types", @@ -11258,6 +11282,16 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + [[package]] name = "sec1" version = "0.7.3" @@ -11347,6 +11381,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.4", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + [[package]] name = "security-framework" version = "3.5.0" @@ -11475,18 +11522,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - [[package]] name = "serde_with" version = "3.14.1" @@ -13203,12 +13238,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - [[package]] name = "synstructure" version = "0.12.6" @@ -13247,17 +13276,6 @@ dependencies = [ "windows 0.52.0", ] -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "system-configuration-sys 0.5.0", -] - [[package]] name = "system-configuration" version = "0.6.1" @@ -13266,17 +13284,7 @@ checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.9.4", "core-foundation 0.9.4", - "system-configuration-sys 0.6.0", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", + "system-configuration-sys", ] [[package]] @@ -13584,7 +13592,7 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f63835928ca123f1bef57abbcd23bb2ba0ac9ae1235f1e65bda0d06e7786bd" dependencies = [ - "rustls", + "rustls 0.23.32", "tokio 1.47.1", ] @@ -13608,8 +13616,8 @@ checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" dependencies = [ "futures-util", "log", - "rustls", - "rustls-native-certs", + "rustls 0.23.32", + "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio 1.47.1", "tokio-rustls", @@ -13933,7 +13941,7 @@ dependencies = [ "httparse", "log", "rand 0.9.2", - "rustls", + "rustls 0.23.32", "rustls-pki-types", "sha1", "thiserror 2.0.16", @@ -14126,7 +14134,6 @@ checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "getrandom 0.3.3", "js-sys", - "serde", "wasm-bindgen", ] diff --git a/Cargo.toml b/Cargo.toml index a4c9f0a8..c78175f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -195,6 +195,7 @@ sc-consensus = { version = "0.50.0", default-features = false } sc-executor = { version = "0.43.0", default-features = false } sc-network = { version = "0.51.0", default-features = false } sc-network-common = { version = "0.49.0", default-features = false } +sc-network-sync = { version = "0.50.0", default-features = false } sc-network-types = { version = "0.17.0", default-features = false } sc-offchain = { version = "46.0.0", default-features = false } sc-service = { version = "0.52.0", default-features = false } diff --git a/EXTERNAL_MINER_PROTOCOL.md b/EXTERNAL_MINER_PROTOCOL.md index 187d4a76..9c17730a 100644 --- a/EXTERNAL_MINER_PROTOCOL.md +++ b/EXTERNAL_MINER_PROTOCOL.md @@ -1,256 +1,243 @@ # External Miner Protocol Specification -This document defines the JSON-based HTTP protocol for communication between the Resonance Network node and an external QPoW miner service. +This document defines the QUIC-based protocol for communication between the Quantus Network node and external QPoW miner services. ## Overview -The node delegates the mining task (finding a valid nonce) to an external service. The node provides the necessary parameters (header hash, difficulty, nonce range) and the external miner searches for a valid nonce according to the QPoW rules defined in the `qpow-math` crate. The miner returns the result, including the winning nonce, when found. +The node delegates the mining task (finding a valid nonce) to external miner services over persistent QUIC connections. The node provides the necessary parameters (header hash, difficulty) and each external miner independently searches for a valid nonce according to the QPoW rules defined in the `qpow-math` crate. Miners push results back when found. -## Data Types +### Key Benefits of QUIC -See the `resonance-miner-api` crate for the canonical Rust definitions of these structures. - -- `job_id`: String (UUID recommended) - Unique identifier for a specific mining task, generated by the node. -- `mining_hash`: String (64 hex chars, no 0x) - The header hash for which to find a nonce. -- `difficulty`: String (u64 as string) - The target difficulty for the mining job. -- `nonce_start`: String (128 hex chars, no 0x) - The starting nonce value (inclusive) for the search range. -- `nonce_end`: String (128 hex chars, no 0x) - The ending nonce value (inclusive) for the search range. -- `status`: Enum (`ApiResponseStatus`) - Indicates the state or result of an API call. -- `message`: String (optional) - Provides details for `Error` status responses. -- `nonce`: String (Hex, no 0x) - Represents the `U512` value of the current or winning nonce. -- `work`: String (128 hex chars, no 0x) - Represents the winning nonce as `[u8; 64]`. This is the value the node needs for verification. -- `hash_count`: Number (u64) - Number of nonces checked by the miner for the job. -- `elapsed_time`: Number (f64) - Time in seconds the miner spent on the job. - -## Endpoints - -### 1. Submit Mining Job - -- **Endpoint:** `POST /mine` -- **Description:** The node requests the external miner to start searching for a valid nonce. -- **Request Body (`MiningRequest`):** - ```json - { - "job_id": "...", - "mining_hash": "...", - "difficulty": "...", - "nonce_start": "...", - "nonce_end": "..." - } - ``` -- **Response Body (`MiningResponse`):** - - Success (200 OK): - ```json - { - "status": "accepted", - "job_id": "..." - } - ``` - - Error (400 Bad Request - Invalid Input / 409 Conflict - Duplicate Job ID): - ```json - { - "status": "error", - "job_id": "...", - "message": "..." // e.g., "Job already exists", "Invalid mining_hash (...)" - } - ``` - -### 2. Get Job Result - -- **Endpoint:** `GET /result/{job_id}` -- **Description:** The node polls the external miner to check the status and retrieve the result. -- **Path Parameter:** - - `job_id`: String (UUID) - The ID of the job to query. -- **Response Body (`MiningResult`):** - - Job Completed (200 OK): - ```json - { - "status": "completed", - "job_id": "...", - "nonce": "...", // U512 hex value of winning nonce - "work": "...", // [u8; 64] hex value of winning nonce - "hash_count": ..., // u64 - "elapsed_time": ... // f64 seconds - } - ``` - - Job Still Running (200 OK): - ```json - { - "status": "running", - "job_id": "...", - "nonce": "...", // Current nonce being checked (U512 hex) - "work": null, - "hash_count": ..., // u64 - "elapsed_time": ... // f64 seconds - } - ``` - - Job Failed (e.g., nonce range exhausted) (200 OK): - ```json - { - "status": "failed", - "job_id": "...", - "nonce": "...", // Final nonce checked (U512 hex) - "work": null, - "hash_count": ..., // u64 - "elapsed_time": ... // f64 seconds - } - ``` - - Job Not Found (404 Not Found): - ```json - { - "status": "not_found", - "job_id": "...", - "nonce": null, - "work": null, - "hash_count": 0, - "elapsed_time": 0.0 - } - ``` - -### 3. Cancel Mining Job - -- **Endpoint:** `POST /cancel/{job_id}` -- **Description:** The node requests the external miner to stop working on a specific job. -- **Path Parameter:** - - `job_id`: String (UUID) - The ID of the job to cancel. -- **Request Body:** (Empty) -- **Response Body (`MiningResponse`): - - Success (200 OK): - ```json - { - "status": "cancelled", - "job_id": "..." - } - ``` - - Job Not Found (404 Not Found): - ```json - { - "status": "not_found", - "job_id": "..." - } - ``` +- **Lower latency**: Results are pushed immediately when found (no polling) +- **Connection resilience**: Built-in connection migration and recovery +- **Multiplexed streams**: Multiple operations on single connection +- **Built-in TLS**: Encrypted by default -## Notes +## Architecture -- All hex values (`mining_hash`, `nonce_start`, `nonce_end`, `nonce`, `work`) should be sent **without** the `0x` prefix. -- The miner must implement the validation logic defined in `qpow_math::is_valid_nonce`. -- The node relies primarily on the `work` field in the `MiningResult` (when status is `completed`) for constructing the `QPoWSeal`. +### Connection Model -# External Miner Protocol Specification +``` + ┌─────────────────────────────────┐ + │ Node │ + │ (QUIC Server on port 9833) │ + │ │ +┌──────────┐ │ Broadcasts: NewJob │ +│ Miner 1 │ ──connect───► │ Receives: JobResult │ +└──────────┘ │ │ + │ Supports multiple miners │ +┌──────────┐ │ First valid result wins │ +│ Miner 2 │ ──connect───► │ │ +└──────────┘ └─────────────────────────────────┘ + +┌──────────┐ +│ Miner 3 │ ──connect───► +└──────────┘ +``` -This document defines the JSON-based HTTP protocol for communication between the node and an external QPoW miner. +- **Node** acts as the QUIC server, listening on port 9833 (default) +- **Miners** act as QUIC clients, connecting to the node +- Single bidirectional stream per miner connection +- Connection persists across multiple mining jobs +- Multiple miners can connect simultaneously -## Overview +### Multi-Miner Operation + +When multiple miners are connected: +1. Node broadcasts the same `NewJob` to all connected miners +2. Each miner independently selects a random starting nonce +3. First miner to find a valid solution sends `JobResult` +4. Node uses the first valid result, ignores subsequent results for same job +5. New job broadcast implicitly cancels work on all miners + +### Message Types + +The protocol uses **three message types**: + +| Direction | Message | Description | +|-----------|---------|-------------| +| Miner → Node | `Ready` | Sent immediately after connecting to establish the stream | +| Node → Miner | `NewJob` | Submit a mining job (implicitly cancels any previous job) | +| Miner → Node | `JobResult` | Mining result (completed, failed, or cancelled) | + +### Wire Format + +Messages are length-prefixed JSON: + +``` +┌─────────────────┬─────────────────────────────────┐ +│ Length (4 bytes)│ JSON payload (MinerMessage) │ +│ big-endian u32 │ │ +└─────────────────┴─────────────────────────────────┘ +``` -The node delegates the mining task to an external service. The node provides the necessary parameters (mining hash, difficulty, and a nonce range) and the external miner searches for a valid nonce within that range. The miner returns the nonce and the resulting work hash when a solution is found. +Maximum message size: 16 MB ## Data Types -- `job_id`: String (UUID recommended) - Identifier for a specific mining task. -- `mining_hash`: String (Hex-encoded, 32-byte hash, H256) - The hash derived from the block header data that the miner needs to solve. -- `difficulty`: String (Decimal representation of u64) - The target difficulty for the block. -- `nonce_start`: String (Hex-encoded, 64-byte value, U512) - The starting nonce value (inclusive). -- `nonce_end`: String (Hex-encoded, 64-byte value, U512) - The ending nonce value (inclusive). -- `nonce`: String (Hex-encoded, 64-byte value, U512) - The solution found by the miner. -- `work`: String (Hex-encoded, 32-byte hash, H256) - The hash resulting from the combination of `mining_hash` and `nonce`, meeting the difficulty requirement. -- `status`: String Enum - Indicates the state or result of an API call. - -## Endpoints - -### 1. Start Mining Job - -- **Endpoint:** `POST /mine` -- **Description:** The node requests the external miner to start searching for a valid nonce within the specified range for the given parameters. -- **Request Body (application/json):** - ```json - { - "job_id": "...", // String (UUID), generated by the node - "mining_hash": "...", // Hex String (H256) - "difficulty": "...", // String (u64 decimal) - "nonce_start": "...", // Hex String (U512 hex) - "nonce_end": "..." // Hex String (U512 hex) - } - ``` -- **Response Body (application/json):** - - Success (200 OK): - ```json - { - "status": "accepted", - "job_id": "..." // String (UUID), confirming the job ID received - } - ``` - - Error (e.g., 400 Bad Request, 500 Internal Server Error): - ```json - { - "status": "rejected", - "reason": "..." // String (Description of error) - } - ``` - -### 2. Get Job Result - -- **Endpoint:** `GET /result/{job_id}` -- **Description:** The node polls the external miner to check the status and retrieve the result of a previously submitted job. -- **Path Parameter:** - - `job_id`: String (UUID) - The ID of the job to query. -- **Response Body (application/json):** - - Solution Found (200 OK): - ```json - { - "status": "found", - "job_id": "...", // String (UUID) - "nonce": "...", // Hex String (U512 hex) - "work": "CAFEBABE01.." // Hex String (H256 hex) - } - ``` - - Still Working (200 OK): - ```json - { - "status": "working", - "job_id": "..." // String (UUID) - } - ``` - - Job Stale/Cancelled (200 OK): Indicates the job is no longer valid (e.g., the node requested cancellation or submitted work for a newer block). - ```json - { - "status": "stale", - "job_id": "..." // String (UUID) - } - ``` - - Job Not Found (404 Not Found): - ```json - { - "status": "not_found", - "job_id": "..." // String (UUID) - } - ``` - -### 3. Cancel Mining Job - -- **Endpoint:** `POST /cancel/{job_id}` -- **Description:** The node requests the external miner to stop working on a specific job. This is typically used when the node receives a new block or its mining parameters change, making the old job obsolete. -- **Path Parameter:** - - `job_id`: String (UUID) - The ID of the job to cancel. -- **Request Body:** (Empty) -- **Response Body (application/json):** - - Success (200 OK): - ```json - { - "status": "cancelled", - "job_id": "..." // String (UUID) - } - ``` - - Job Not Found (404 Not Found): - ```json - { - "status": "not_found", - "job_id": "..." // String (UUID) - } - ``` +See the `quantus-miner-api` crate for the canonical Rust definitions. + +### MinerMessage (Enum) + +```rust +pub enum MinerMessage { + Ready, // Miner → Node: establish stream + NewJob(MiningRequest), // Node → Miner: submit job + JobResult(MiningResult), // Miner → Node: return result +} +``` + +### MiningRequest + +| Field | Type | Description | +|-------|------|-------------| +| `job_id` | String | Unique identifier (UUID recommended) | +| `mining_hash` | String | Header hash (64 hex chars, no 0x prefix) | +| `distance_threshold` | String | Difficulty (U512 as decimal string) | + +Note: Nonce range is not specified - each miner independently selects a random starting point. + +### MiningResult + +| Field | Type | Description | +|-------|------|-------------| +| `status` | ApiResponseStatus | Result status (see below) | +| `job_id` | String | Job identifier | +| `nonce` | Option | Winning nonce (U512 hex, no 0x prefix) | +| `work` | Option | Winning nonce as bytes (128 hex chars) | +| `hash_count` | u64 | Number of nonces checked | +| `elapsed_time` | f64 | Time spent mining (seconds) | +| `miner_id` | Option | Miner ID (set by node, not miner) | + +### ApiResponseStatus (Enum) + +| Value | Description | +|-------|-------------| +| `completed` | Valid nonce found | +| `failed` | Nonce range exhausted without finding solution | +| `cancelled` | Job was cancelled (new job received) | +| `running` | Job still in progress (not typically sent) | + +## Protocol Flow + +### Normal Mining Flow + +``` +Miner Node + │ │ + │──── QUIC Connect ─────────────────────────►│ + │◄─── Connection Established ────────────────│ + │ │ + │──── Ready ────────────────────────────────►│ (establish stream) + │ │ + │◄─── NewJob { job_id: "abc", ... } ─────────│ + │ │ + │ (picks random nonce, starts mining) │ + │ │ + │──── JobResult { job_id: "abc", ... } ─────►│ (found solution!) + │ │ + │ (node submits block, gets new work) │ + │ │ + │◄─── NewJob { job_id: "def", ... } ─────────│ + │ │ +``` + +### Job Cancellation (Implicit) + +When a new block arrives before the miner finds a solution, the node simply sends a new `NewJob`. The miner automatically cancels the previous job: + +``` +Miner Node + │ │ + │◄─── NewJob { job_id: "abc", ... } ─────────│ + │ │ + │ (mining "abc") │ + │ │ + │ (new block arrives at node!) │ + │ │ + │◄─── NewJob { job_id: "def", ... } ─────────│ + │ │ + │ (cancels "abc", starts "def") │ + │ │ + │──── JobResult { job_id: "def", ... } ─────►│ +``` + +### Miner Connect During Active Job + +When a miner connects while a job is active, it immediately receives the current job: + +``` +Miner (new) Node + │ │ (already mining job "abc") + │──── QUIC Connect ─────────────────────────►│ + │◄─── Connection Established ────────────────│ + │ │ + │──── Ready ────────────────────────────────►│ (establish stream) + │ │ + │◄─── NewJob { job_id: "abc", ... } ─────────│ (current job sent immediately) + │ │ + │ (joins mining effort) │ +``` + +### Stale Result Handling + +If a result arrives for an old job, the node discards it: + +``` +Miner Node + │ │ + │◄─── NewJob { job_id: "abc", ... } ─────────│ + │ │ + │◄─── NewJob { job_id: "def", ... } ─────────│ (almost simultaneous) + │ │ + │──── JobResult { job_id: "abc", ... } ─────►│ (stale, node ignores) + │ │ + │──── JobResult { job_id: "def", ... } ─────►│ (current, node uses) +``` + +## Configuration + +### Node + +```bash +# Listen for external miner connections on port 9833 +quantus-node --miner-listen-port 9833 +``` + +### Miner + +```bash +# Connect to node +quantus-miner serve --node-addr 127.0.0.1:9833 +``` + +## TLS Configuration + +The node generates a self-signed TLS certificate at startup. The miner skips certificate verification by default (insecure mode). For production deployments, consider: + +1. **Certificate pinning**: Configure the miner to accept only specific certificate fingerprints +2. **Proper CA**: Use certificates signed by a trusted CA +3. **Network isolation**: Run node and miner on a private network + +## Error Handling + +### Connection Loss + +The miner automatically reconnects with exponential backoff: +- Initial delay: 1 second +- Maximum delay: 30 seconds + +The node continues operating with remaining connected miners. + +### Validation Errors + +If the miner receives an invalid `MiningRequest`, it sends a `JobResult` with status `failed`. ## Notes -- The external miner should iterate from `nonce_start` up to and including `nonce_end` when searching for a valid nonce. -- The miner should return the `nonce` and the calculated `work` hash when a solution is found. -- The node uses the returned `nonce` and `work` (along with the fetched `difficulty`) to construct the `QPoWSeal` and submit it. -- The external miner should not need to know anything about the runtime or the code; it only needs to perform the nonce search and return the results. \ No newline at end of file +- All hex values should be sent **without** the `0x` prefix +- The miner implements validation logic from `qpow_math::is_valid_nonce` +- The node uses the `work` field from `MiningResult` to construct `QPoWSeal` +- ALPN protocol identifier: `quantus-miner` +- Each miner independently generates a random nonce starting point using cryptographically secure randomness +- With a 512-bit nonce space, collision between miners is statistically impossible diff --git a/client/cli/Cargo.toml b/client/cli/Cargo.toml index e730af89..23c601e1 100644 --- a/client/cli/Cargo.toml +++ b/client/cli/Cargo.toml @@ -27,7 +27,6 @@ log = { workspace = true, default-features = true } names = { workspace = true, default-features = false } qp-dilithium-crypto = { workspace = true, features = ["full_crypto", "serde", "std"] } qp-rusty-crystals-dilithium = { workspace = true } -qp-rusty-crystals-hdwallet = { workspace = true } rand = { workspace = true, default-features = true } regex = { workspace = true } rpassword = { workspace = true } @@ -46,7 +45,6 @@ serde_json = { workspace = true, default-features = true } sp-blockchain = { workspace = true, default-features = true } sp-core = { workspace = true, default-features = true } sp-keyring = { workspace = true, default-features = true } -sp-keystore = { workspace = true, default-features = true } sp-panic-handler = { workspace = true, default-features = true } sp-runtime = { workspace = true, default-features = true } sp-version = { workspace = true, default-features = true } diff --git a/client/consensus/qpow/Cargo.toml b/client/consensus/qpow/Cargo.toml index 7c9c1de2..598bef29 100644 --- a/client/consensus/qpow/Cargo.toml +++ b/client/consensus/qpow/Cargo.toml @@ -18,9 +18,6 @@ prometheus-endpoint = { workspace = true, default-features = true } sc-client-api = { workspace = true, default-features = false } sc-consensus = { workspace = true } sc-service = { workspace = true, default-features = false } -scale-info = { workspace = true, default-features = false } -sha2.workspace = true -sha3.workspace = true sp-api = { workspace = true, default-features = false } sp-block-builder = { workspace = true, default-features = true } sp-blockchain = { workspace = true, default-features = false } @@ -37,9 +34,6 @@ default = ["std"] std = [ "codec/std", "primitive-types/std", - "scale-info/std", - "sha2/std", - "sha3/std", "sp-api/std", "sp-consensus-pow/std", "sp-consensus-qpow/std", diff --git a/client/consensus/qpow/src/worker.rs b/client/consensus/qpow/src/worker.rs index e819b8c4..ca41aa95 100644 --- a/client/consensus/qpow/src/worker.rs +++ b/client/consensus/qpow/src/worker.rs @@ -31,6 +31,7 @@ use sc_consensus::{BlockImportParams, BoxBlockImport, StateAction, StorageChange use sp_api::ProvideRuntimeApi; use sp_consensus::{BlockOrigin, Proposal}; use sp_consensus_pow::{Seal, POW_ENGINE_ID}; +use sp_consensus_qpow::QPoWApi; use sp_runtime::{ traits::{Block as BlockT, Header as HeaderT}, AccountId32, DigestItem, @@ -82,6 +83,7 @@ impl MiningHandle where Block: BlockT, AC: ProvideRuntimeApi, + AC::Api: QPoWApi, L: sc_consensus::JustificationSyncLink, { fn increment_version(&self) { @@ -133,6 +135,39 @@ where self.build.lock().as_ref().map(|b| b.metadata.clone()) } + /// Verify a seal without consuming the build. + /// + /// Returns `true` if the seal is valid for the current block, `false` otherwise. + /// Returns `false` if there's no current build. + pub fn verify_seal(&self, seal: &Seal) -> bool { + let build = self.build.lock(); + let build = match build.as_ref() { + Some(b) => b, + None => return false, + }; + + // Convert seal to nonce [u8; 64] + let nonce: [u8; 64] = match seal.as_slice().try_into() { + Ok(arr) => arr, + Err(_) => { + warn!(target: LOG_TARGET, "Seal does not have exactly 64 bytes"); + return false; + }, + }; + + let pre_hash = build.metadata.pre_hash.0; + let best_hash = build.metadata.best_hash; + + // Verify using runtime API + match self.client.runtime_api().verify_nonce_local_mining(best_hash, pre_hash, nonce) { + Ok(valid) => valid, + Err(e) => { + warn!(target: LOG_TARGET, "Runtime API error verifying seal: {:?}", e); + false + }, + } + } + /// Submit a mined seal. The seal will be validated again. Returns true if the submission is /// successful. #[allow(clippy::await_holding_lock)] diff --git a/client/network/Cargo.toml b/client/network/Cargo.toml index 5e00f677..5616dabf 100644 --- a/client/network/Cargo.toml +++ b/client/network/Cargo.toml @@ -39,7 +39,6 @@ libp2p-identity = { workspace = true, features = ["dilithium"] } linked_hash_set = { workspace = true } log = { workspace = true, default-features = true } mockall = { workspace = true } -once_cell = { workspace = true } parking_lot = { workspace = true, default-features = true } partial_sort = { workspace = true } pin-project = { workspace = true } diff --git a/miner-api/Cargo.toml b/miner-api/Cargo.toml index 142365a8..0f532cb7 100644 --- a/miner-api/Cargo.toml +++ b/miner-api/Cargo.toml @@ -16,3 +16,5 @@ version = "0.0.3" [dependencies] serde = { workspace = true, features = ["alloc", "derive"] } +serde_json = { workspace = true, features = ["std"] } +tokio = { workspace = true, features = ["io-util"] } diff --git a/miner-api/src/lib.rs b/miner-api/src/lib.rs index 869c869b..6c15fc34 100644 --- a/miner-api/src/lib.rs +++ b/miner-api/src/lib.rs @@ -1,4 +1,8 @@ use serde::{Deserialize, Serialize}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + +/// Maximum message size (16 MB) to prevent memory exhaustion attacks. +pub const MAX_MESSAGE_SIZE: u32 = 16 * 1024 * 1024; /// Status codes returned in API responses. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] @@ -13,18 +17,74 @@ pub enum ApiResponseStatus { Error, } -/// Request payload sent from Node to Miner (`/mine` endpoint). +/// QUIC protocol messages exchanged between node and miner. +/// +/// The protocol is: +/// - Miner sends `Ready` immediately after connecting to establish the stream +/// - Node sends `NewJob` to submit a mining job (implicitly cancels any previous job) +/// - Miner sends `JobResult` when mining completes +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum MinerMessage { + /// Miner → Node: Sent immediately after connecting to establish the stream. + /// This is required because QUIC streams are lazily initialized. + Ready, + + /// Node → Miner: Submit a new mining job. + /// If a job is already running, it will be cancelled and replaced. + NewJob(MiningRequest), + + /// Miner → Node: Mining result (completed, failed, or cancelled). + JobResult(MiningResult), +} + +/// Write a length-prefixed JSON message to an async writer. +/// +/// Wire format: 4-byte big-endian length prefix followed by JSON payload. +pub async fn write_message( + writer: &mut W, + msg: &MinerMessage, +) -> std::io::Result<()> { + let json = serde_json::to_vec(msg) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + let len = json.len() as u32; + writer.write_all(&len.to_be_bytes()).await?; + writer.write_all(&json).await?; + Ok(()) +} + +/// Read a length-prefixed JSON message from an async reader. +/// +/// Wire format: 4-byte big-endian length prefix followed by JSON payload. +/// Returns an error if the message exceeds MAX_MESSAGE_SIZE. +pub async fn read_message(reader: &mut R) -> std::io::Result { + let mut len_buf = [0u8; 4]; + reader.read_exact(&mut len_buf).await?; + let len = u32::from_be_bytes(len_buf); + + if len > MAX_MESSAGE_SIZE { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Message size {} exceeds maximum {}", len, MAX_MESSAGE_SIZE), + )); + } + + let mut buf = vec![0u8; len as usize]; + reader.read_exact(&mut buf).await?; + serde_json::from_slice(&buf) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) +} + +/// Request payload sent from Node to Miner. +/// +/// The miner will choose its own random starting nonce, enabling multiple +/// miners to work on the same job without coordination. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct MiningRequest { pub job_id: String, /// Hex encoded header hash (32 bytes -> 64 chars, no 0x prefix) pub mining_hash: String, - /// Distance threshold (u64 as string) + /// Distance threshold (U512 as decimal string) pub distance_threshold: String, - /// Hex encoded start nonce (U512 -> 128 chars, no 0x prefix) - pub nonce_start: String, - /// Hex encoded end nonce (U512 -> 128 chars, no 0x prefix) - pub nonce_end: String, } /// Response payload for job submission (`/mine`) and cancellation (`/cancel`). @@ -48,4 +108,7 @@ pub struct MiningResult { pub work: Option, pub hash_count: u64, pub elapsed_time: f64, + /// Miner ID assigned by the node (set server-side, not by the miner). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub miner_id: Option, } diff --git a/node/Cargo.toml b/node/Cargo.toml index 271a7813..9df4bb9e 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -33,16 +33,17 @@ pallet-transaction-payment-rpc.default-features = true pallet-transaction-payment-rpc.workspace = true prometheus.workspace = true qp-dilithium-crypto = { workspace = true } -qp-rusty-crystals-dilithium.workspace = true qp-rusty-crystals-hdwallet.workspace = true qpow-math.workspace = true quantus-miner-api = { workspace = true } quantus-runtime.workspace = true +quinn = "0.10" rand = { workspace = true, default-features = false, features = [ "alloc", "getrandom", ] } -reqwest = { workspace = true, default-features = false, features = ["json"] } +rcgen = "0.11" +rustls = { version = "0.21", default-features = false, features = ["dangerous_configuration", "quic"] } sc-basic-authorship.default-features = true sc-basic-authorship.workspace = true sc-cli.default-features = true @@ -56,6 +57,8 @@ sc-executor.default-features = true sc-executor.workspace = true sc-network.default-features = true sc-network.workspace = true +sc-network-sync.default-features = true +sc-network-sync.workspace = true sc-offchain.default-features = true sc-offchain.workspace = true sc-service.default-features = true @@ -93,7 +96,6 @@ sp-timestamp.workspace = true substrate-frame-rpc-system.default-features = true substrate-frame-rpc-system.workspace = true tokio-util.workspace = true -uuid.workspace = true [build-dependencies] qp-wormhole-circuit-builder.workspace = true @@ -115,6 +117,7 @@ std = [ "serde_json/std", "sp-consensus-qpow/std", ] +tx-logging = [] # Enable transaction pool logging for debugging # Dependencies that are only required if runtime benchmarking should be build. runtime-benchmarks = [ "frame-benchmarking-cli/runtime-benchmarks", diff --git a/node/src/cli.rs b/node/src/cli.rs index b9d3003a..f52ff1d2 100644 --- a/node/src/cli.rs +++ b/node/src/cli.rs @@ -13,9 +13,10 @@ pub struct Cli { #[arg(long, value_name = "REWARDS_ADDRESS")] pub rewards_address: Option, - /// Specify the URL of an external QPoW miner service - #[arg(long, value_name = "EXTERNAL_MINER_URL")] - pub external_miner_url: Option, + /// Port to listen for external miner connections (e.g., 9833). + /// When set, the node will wait for miners to connect instead of mining locally. + #[arg(long, value_name = "PORT")] + pub miner_listen_port: Option, /// Enable peer sharing via RPC endpoint #[arg(long)] diff --git a/node/src/command.rs b/node/src/command.rs index 654d8506..b44d75ac 100644 --- a/node/src/command.rs +++ b/node/src/command.rs @@ -480,9 +480,7 @@ pub fn run() -> sc_cli::Result<()> { quantus_runtime::opaque::Block, ::Hash, >, - >( - config, rewards_account, cli.external_miner_url.clone(), cli.enable_peer_sharing - ) + >(config, rewards_account, cli.miner_listen_port, cli.enable_peer_sharing) .map_err(sc_cli::Error::Service) }) }, diff --git a/node/src/external_miner_client.rs b/node/src/external_miner_client.rs deleted file mode 100644 index 49d7f764..00000000 --- a/node/src/external_miner_client.rs +++ /dev/null @@ -1,110 +0,0 @@ -use quantus_miner_api::{ApiResponseStatus, MiningRequest, MiningResponse, MiningResult}; -/// Functions to interact with the external miner service -use reqwest::Client; -use sp_core::{H256, U512}; - -// Make functions pub(crate) or pub as needed -pub(crate) async fn submit_mining_job( - client: &Client, - miner_url: &str, - job_id: &str, - mining_hash: &H256, - distance_threshold: U512, - nonce_start: U512, - nonce_end: U512, -) -> Result<(), String> { - let request = MiningRequest { - job_id: job_id.to_string(), - mining_hash: hex::encode(mining_hash.as_bytes()), - distance_threshold: distance_threshold.to_string(), - nonce_start: format!("{:0128x}", nonce_start), - nonce_end: format!("{:0128x}", nonce_end), - }; - - let response = client - .post(format!("{}/mine", miner_url)) - .json(&request) - .send() - .await - .map_err(|e| format!("Failed to send mining request: {}", e))?; - - let result: MiningResponse = response - .json() - .await - .map_err(|e| format!("Failed to parse mining response: {}", e))?; - - if result.status != ApiResponseStatus::Accepted { - return Err(format!("Mining job was not accepted: {:?}", result.status)); - } - - Ok(()) -} - -pub(crate) async fn check_mining_result( - client: &Client, - miner_url: &str, - job_id: &str, -) -> Result, String> { - let response = client - .get(format!("{}/result/{}", miner_url, job_id)) - .send() - .await - .map_err(|e| format!("Failed to check mining result: {}", e))?; - - let result: MiningResult = response - .json() - .await - .map_err(|e| format!("Failed to parse mining result: {}", e))?; - - match result.status { - ApiResponseStatus::Completed => - if let Some(work_hex) = result.work { - let nonce_bytes = hex::decode(&work_hex) - .map_err(|e| format!("Failed to decode work hex '{}': {}", work_hex, e))?; - if nonce_bytes.len() == 64 { - let mut nonce = [0u8; 64]; - nonce.copy_from_slice(&nonce_bytes); - Ok(Some(nonce)) - } else { - Err(format!( - "Invalid decoded work length: {} bytes (expected 64)", - nonce_bytes.len() - )) - } - } else { - Err("Missing 'work' field in completed mining result".to_string()) - }, - ApiResponseStatus::Running => Ok(None), - ApiResponseStatus::NotFound => Err("Mining job not found".to_string()), - ApiResponseStatus::Failed => Err("Mining job failed (miner reported)".to_string()), - ApiResponseStatus::Cancelled => - Err("Mining job was cancelled (miner reported)".to_string()), - ApiResponseStatus::Error => Err("Miner reported an unspecified error".to_string()), - ApiResponseStatus::Accepted => - Err("Unexpected 'Accepted' status received from result endpoint".to_string()), - } -} - -pub(crate) async fn cancel_mining_job( - client: &Client, - miner_url: &str, - job_id: &str, -) -> Result<(), String> { - let response = client - .post(format!("{}/cancel/{}", miner_url, job_id)) - .send() - .await - .map_err(|e| format!("Failed to cancel mining job: {}", e))?; - - let result: MiningResponse = response - .json() - .await - .map_err(|e| format!("Failed to parse cancel response: {}", e))?; - - if result.status == ApiResponseStatus::Cancelled || result.status == ApiResponseStatus::NotFound - { - Ok(()) - } else { - Err(format!("Failed to cancel mining job (unexpected status): {:?}", result.status)) - } -} diff --git a/node/src/main.rs b/node/src/main.rs index f1fb0e64..f0627141 100644 --- a/node/src/main.rs +++ b/node/src/main.rs @@ -5,7 +5,7 @@ mod benchmarking; mod chain_spec; mod cli; mod command; -mod external_miner_client; +mod miner_server; mod prometheus; mod rpc; mod service; diff --git a/node/src/miner_server.rs b/node/src/miner_server.rs new file mode 100644 index 00000000..17951bf9 --- /dev/null +++ b/node/src/miner_server.rs @@ -0,0 +1,348 @@ +//! QUIC server for accepting connections from external miners. +//! +//! This module provides a QUIC server that miners connect to. It supports +//! multiple concurrent miners, broadcasting jobs to all connected miners +//! and collecting results. +//! +//! # Architecture +//! +//! ```text +//! ┌──────────┐ +//! │ Miner 1 │ ────┐ +//! └──────────┘ │ +//! │ ┌─────────────────┐ +//! ┌──────────┐ ├────>│ MinerServer │ +//! │ Miner 2 │ ────┤ │ (QUIC Server) │ +//! └──────────┘ │ └─────────────────┘ +//! │ +//! ┌──────────┐ │ +//! │ Miner 3 │ ────┘ +//! └──────────┘ +//! ``` +//! +//! # Protocol +//! +//! - Node sends `MinerMessage::NewJob` to all connected miners +//! - Each miner independently selects a random nonce starting point +//! - First miner to find a valid solution sends `MinerMessage::JobResult` +//! - When a new job is broadcast, miners implicitly cancel their current work + +use std::{ + collections::HashMap, + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, + }, + time::Duration, +}; + +use jsonrpsee::tokio; +use quantus_miner_api::{read_message, write_message, MinerMessage, MiningRequest, MiningResult}; +use tokio::sync::{mpsc, RwLock}; + +/// A QUIC server that accepts connections from miners. +pub struct MinerServer { + /// Connected miners, keyed by unique ID. + miners: Arc>>, + /// Channel to receive results from any miner. + result_rx: tokio::sync::Mutex>, + /// Sender cloned to each miner connection handler. + result_tx: mpsc::Sender, + /// Current job being mined (sent to newly connecting miners). + current_job: Arc>>, + /// Counter for assigning unique miner IDs. + next_miner_id: AtomicU64, +} + +/// Handle for communicating with a connected miner. +struct MinerHandle { + /// Channel to send jobs to this miner. + job_tx: mpsc::Sender, +} + +impl MinerServer { + /// Start the QUIC server and listen for miner connections. + /// + /// This spawns a background task that accepts incoming connections. + pub async fn start(port: u16) -> Result, String> { + let (result_tx, result_rx) = mpsc::channel::(64); + + let server = Arc::new(Self { + miners: Arc::new(RwLock::new(HashMap::new())), + result_rx: tokio::sync::Mutex::new(result_rx), + result_tx, + current_job: Arc::new(RwLock::new(None)), + next_miner_id: AtomicU64::new(1), + }); + + // Start the acceptor task + let server_clone = server.clone(); + let endpoint = create_server_endpoint(port).await?; + + tokio::spawn(async move { + acceptor_task(endpoint, server_clone).await; + }); + + log::info!("⛏️ Miner server listening on port {}", port); + + Ok(server) + } + + /// Broadcast a job to all connected miners. + /// + /// This also stores the job so newly connecting miners receive it. + pub async fn broadcast_job(&self, job: MiningRequest) { + // Store as current job for new miners + { + let mut current = self.current_job.write().await; + *current = Some(job.clone()); + } + + // Send to all connected miners + let miners = self.miners.read().await; + let miner_count = miners.len(); + + if miner_count == 0 { + log::debug!("No miners connected, job queued for when miners connect"); + return; + } + + log::debug!("Broadcasting job {} to {} miner(s)", job.job_id, miner_count); + + for (id, handle) in miners.iter() { + if let Err(e) = handle.job_tx.try_send(job.clone()) { + log::warn!("Failed to send job to miner {}: {}", id, e); + } + } + } + + /// Wait for a mining result with a timeout. + pub async fn recv_result_timeout(&self, timeout: Duration) -> Option { + let mut rx = self.result_rx.lock().await; + tokio::time::timeout(timeout, rx.recv()).await.ok().flatten() + } + + /// Add a new miner connection. + async fn add_miner(&self, job_tx: mpsc::Sender) -> u64 { + let id = self.next_miner_id.fetch_add(1, Ordering::Relaxed); + let handle = MinerHandle { job_tx }; + + self.miners.write().await.insert(id, handle); + + log::info!("⛏️ Miner {} connected (total: {})", id, self.miners.read().await.len()); + + id + } + + /// Remove a miner connection. + async fn remove_miner(&self, id: u64) { + self.miners.write().await.remove(&id); + log::info!("⛏️ Miner {} disconnected (total: {})", id, self.miners.read().await.len()); + } + + /// Get the current job (if any) for newly connecting miners. + async fn get_current_job(&self) -> Option { + self.current_job.read().await.clone() + } +} + +/// Create a QUIC server endpoint with self-signed certificate. +async fn create_server_endpoint(port: u16) -> Result { + // Generate self-signed certificate + let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]) + .map_err(|e| format!("Failed to generate certificate: {}", e))?; + + let cert_der = cert + .serialize_der() + .map_err(|e| format!("Failed to serialize certificate: {}", e))?; + let key_der = cert.serialize_private_key_der(); + + let cert_chain = vec![rustls::Certificate(cert_der)]; + let key = rustls::PrivateKey(key_der); + + // Create server config + let mut server_config = rustls::ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(cert_chain, key) + .map_err(|e| format!("Failed to create server config: {}", e))?; + + // Set ALPN protocol + server_config.alpn_protocols = vec![b"quantus-miner".to_vec()]; + + let mut quinn_config = quinn::ServerConfig::with_crypto(Arc::new(server_config)); + + // Set transport config + let mut transport_config = quinn::TransportConfig::default(); + transport_config.keep_alive_interval(Some(Duration::from_secs(10))); + transport_config.max_idle_timeout(Some(Duration::from_secs(60).try_into().unwrap())); + quinn_config.transport_config(Arc::new(transport_config)); + + // Create endpoint + let addr = format!("0.0.0.0:{}", port).parse().unwrap(); + let endpoint = quinn::Endpoint::server(quinn_config, addr) + .map_err(|e| format!("Failed to create server endpoint: {}", e))?; + + Ok(endpoint) +} + +/// Background task that accepts incoming miner connections. +async fn acceptor_task(endpoint: quinn::Endpoint, server: Arc) { + log::debug!("Acceptor task started"); + + while let Some(connecting) = endpoint.accept().await { + let server = server.clone(); + + tokio::spawn(async move { + match connecting.await { + Ok(connection) => { + log::debug!("New QUIC connection from {:?}", connection.remote_address()); + handle_miner_connection(connection, server).await; + }, + Err(e) => { + log::warn!("Failed to accept connection: {}", e); + }, + } + }); + } + + log::info!("Acceptor task stopped"); +} + +/// Handle a single miner connection. +async fn handle_miner_connection(connection: quinn::Connection, server: Arc) { + let addr = connection.remote_address(); + log::info!("⛏️ New miner connection from {}", addr); + log::debug!("Waiting for miner {} to open bidirectional stream...", addr); + + // Accept bidirectional stream from miner + let (send, recv) = match connection.accept_bi().await { + Ok(streams) => { + log::info!("⛏️ Stream accepted from miner {}", addr); + streams + }, + Err(e) => { + log::warn!("Failed to accept stream from {}: {}", addr, e); + return; + }, + }; + + // Create channel for sending jobs to this miner + let (job_tx, job_rx) = mpsc::channel::(16); + + // Register miner + let miner_id = server.add_miner(job_tx).await; + + // Send current job if there is one + if let Some(job) = server.get_current_job().await { + log::debug!("Sending current job {} to newly connected miner {}", job.job_id, miner_id); + // We'll send it through the connection handler below + } + + // Handle the connection + let result = connection_handler( + miner_id, + send, + recv, + job_rx, + server.result_tx.clone(), + server.get_current_job().await, + ) + .await; + + if let Err(e) = result { + log::debug!("Miner {} connection ended: {}", miner_id, e); + } + + // Unregister miner + server.remove_miner(miner_id).await; +} + +/// Handle communication with a single miner. +async fn connection_handler( + miner_id: u64, + mut send: quinn::SendStream, + mut recv: quinn::RecvStream, + mut job_rx: mpsc::Receiver, + result_tx: mpsc::Sender, + initial_job: Option, +) -> Result<(), String> { + // Wait for Ready message from miner (required to establish the stream) + log::debug!("Waiting for Ready message from miner {}...", miner_id); + match read_message(&mut recv).await { + Ok(MinerMessage::Ready) => { + log::debug!("Received Ready from miner {}", miner_id); + }, + Ok(other) => { + log::warn!("Expected Ready from miner {}, got {:?}", miner_id, other); + return Err("Protocol error: expected Ready message".to_string()); + }, + Err(e) => { + return Err(format!("Failed to read Ready message: {}", e)); + }, + } + + // Send initial job if there is one + if let Some(job) = initial_job { + log::debug!("Sending initial job {} to miner {}", job.job_id, miner_id); + let msg = MinerMessage::NewJob(job); + write_message(&mut send, &msg) + .await + .map_err(|e| format!("Failed to send initial job: {}", e))?; + } + + loop { + tokio::select! { + // Prioritize reading to detect disconnection faster + biased; + + // Receive results from miner + msg_result = read_message(&mut recv) => { + match msg_result { + Ok(MinerMessage::JobResult(mut result)) => { + log::info!( + "⛏️ Received result from miner {}: job_id={}, status={:?}", + miner_id, + result.job_id, + result.status + ); + // Tag the result with the miner ID + result.miner_id = Some(miner_id); + if result_tx.send(result).await.is_err() { + return Err("Result channel closed".to_string()); + } + } + Ok(MinerMessage::Ready) => { + log::debug!("Ignoring duplicate Ready from miner {}", miner_id); + } + Ok(MinerMessage::NewJob(_)) => { + log::warn!("Received unexpected NewJob from miner {}", miner_id); + } + Err(e) => { + if e.kind() == std::io::ErrorKind::UnexpectedEof { + return Err("Miner disconnected".to_string()); + } + return Err(format!("Read error: {}", e)); + } + } + } + + // Send jobs to miner + job = job_rx.recv() => { + match job { + Some(job) => { + log::debug!("Sending job {} to miner {}", job.job_id, miner_id); + let msg = MinerMessage::NewJob(job); + if let Err(e) = write_message(&mut send, &msg).await { + return Err(format!("Failed to send job: {}", e)); + } + } + None => { + // Channel closed, shut down + return Ok(()); + } + } + } + } + } +} diff --git a/node/src/prometheus.rs b/node/src/prometheus.rs index e5d834b2..d8e6273d 100644 --- a/node/src/prometheus.rs +++ b/node/src/prometheus.rs @@ -7,9 +7,9 @@ use sp_consensus_qpow::QPoWApi; use sp_core::U512; use std::sync::Arc; -pub struct ResonanceBusinessMetrics; +pub struct BusinessMetrics; -impl ResonanceBusinessMetrics { +impl BusinessMetrics { /// Pack a U512 into an f64 by taking the highest-order 64 bits (8 bytes). fn pack_u512_to_f64(value: U512) -> f64 { // Convert U512 to big-endian bytes (64 bytes) diff --git a/node/src/service.rs b/node/src/service.rs index 27a9b662..9215b035 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -1,20 +1,30 @@ //! Service and ServiceFactory implementation. Specialized wrapper over substrate service. - -use futures::{FutureExt, StreamExt}; +//! +//! This module provides the main service setup for a Quantus node, including: +//! - Network configuration and setup +//! - Transaction pool management +//! - Mining infrastructure (local and external miner support) +//! - RPC endpoint configuration + +use futures::FutureExt; +#[cfg(feature = "tx-logging")] +use futures::StreamExt; use quantus_runtime::{self, apis::RuntimeApi, opaque::Block}; use sc_client_api::Backend; -use sc_consensus_qpow::ChainManagement; +use sc_consensus_qpow::{ChainManagement, MiningHandle}; use sc_service::{error::Error as ServiceError, Configuration, TaskManager}; use sc_telemetry::{Telemetry, TelemetryWorker}; -use sc_transaction_pool_api::{InPoolTransaction, OffchainTransactionPoolFactory, TransactionPool}; +#[cfg(feature = "tx-logging")] +use sc_transaction_pool_api::InPoolTransaction; +use sc_transaction_pool_api::{OffchainTransactionPoolFactory, TransactionPool}; use sp_inherents::CreateInherentDataProviders; use tokio_util::sync::CancellationToken; -use crate::{external_miner_client, prometheus::ResonanceBusinessMetrics}; +use crate::{miner_server::MinerServer, prometheus::BusinessMetrics}; use codec::Encode; use jsonrpsee::tokio; -use qpow_math::mine_range; -use reqwest::Client; +use quantus_miner_api::{ApiResponseStatus, MiningRequest, MiningResult}; +use sc_basic_authorship::ProposerFactory; use sc_cli::TransactionPoolType; use sc_transaction_pool::TransactionPoolOptions; use sp_api::ProvideRuntimeApi; @@ -22,11 +32,472 @@ use sp_consensus::SyncOracle; use sp_consensus_qpow::QPoWApi; use sp_core::{crypto::AccountId32, U512}; use std::{sync::Arc, time::Duration}; -use uuid::Uuid; /// Frequency of block import logging. Every 1000 blocks. const LOG_FREQUENCY: u64 = 1000; +// ============================================================================ +// External Mining Helper Functions +// ============================================================================ + +/// Parse a mining result and extract the seal if valid. +fn parse_mining_result(result: &MiningResult, expected_job_id: &str) -> Option> { + let miner_id = result.miner_id.unwrap_or(0); + + // Check job ID matches + if result.job_id != expected_job_id { + log::debug!(target: "miner", "Received stale result from miner {} for job {}, ignoring", miner_id, result.job_id); + return None; + } + + // Check status + if result.status != ApiResponseStatus::Completed { + match result.status { + ApiResponseStatus::Failed => log::warn!("⛏️ Mining job failed (miner {})", miner_id), + ApiResponseStatus::Cancelled => { + log::debug!(target: "miner", "Mining job was cancelled (miner {})", miner_id) + }, + _ => { + log::debug!(target: "miner", "Unexpected result status from miner {}: {:?}", miner_id, result.status) + }, + } + return None; + } + + // Extract and decode work + let work_hex = result.work.as_ref()?; + match hex::decode(work_hex) { + Ok(seal) if seal.len() == 64 => Some(seal), + Ok(seal) => { + log::error!( + "🚨🚨🚨 INVALID SEAL LENGTH FROM MINER {}! Expected 64 bytes, got {} bytes", + miner_id, + seal.len() + ); + None + }, + Err(e) => { + log::error!("🚨🚨🚨 FAILED TO DECODE SEAL HEX FROM MINER {}: {}", miner_id, e); + None + }, + } +} + +/// Wait for a mining result from the miner server. +/// +/// Returns `Some((miner_id, seal))` if a valid 64-byte seal is received, `None` otherwise +/// (interrupted, failed, invalid, or stale). +/// +/// The `should_stop` closure should return `true` if we should stop waiting +/// (e.g., new block arrived or shutdown requested). +/// +/// This function will keep waiting even if all miners disconnect, since newly +/// connecting miners automatically receive the current job and can submit results. +async fn wait_for_mining_result( + server: &Arc, + job_id: &str, + should_stop: F, +) -> Option<(u64, Vec)> +where + F: Fn() -> bool, +{ + loop { + if should_stop() { + return None; + } + + match server.recv_result_timeout(Duration::from_millis(500)).await { + Some(result) => { + let miner_id = result.miner_id.unwrap_or(0); + if let Some(seal) = parse_mining_result(&result, job_id) { + return Some((miner_id, seal)); + } + // Keep waiting for other miners (stale, failed, or invalid parse) + }, + None => { + // Timeout, continue waiting + }, + } + } +} + +// ============================================================================ +// Mining Loop Helpers +// ============================================================================ + +/// Result of attempting to mine with an external miner. +enum ExternalMiningOutcome { + /// Successfully found and imported a seal. + Success, + /// Mining was interrupted (new block, cancellation, or failure). + Interrupted, +} + +/// Handle a single round of external mining. +/// +/// Broadcasts the job to connected miners and waits for results. +/// If a seal fails validation, continues waiting for more seals. +/// Only returns when a seal is successfully imported, or when interrupted. +async fn handle_external_mining( + server: &Arc, + client: &Arc, + worker_handle: &MiningHandle< + Block, + FullClient, + Arc>, + (), + >, + cancellation_token: &CancellationToken, + job_counter: &mut u64, + mining_start_time: &mut std::time::Instant, +) -> ExternalMiningOutcome { + let metadata = match worker_handle.metadata() { + Some(m) => m, + None => return ExternalMiningOutcome::Interrupted, + }; + + // Get difficulty from runtime + let difficulty = match client.runtime_api().get_difficulty(metadata.best_hash) { + Ok(d) => d, + Err(e) => { + log::warn!("⛏️ Failed to get difficulty: {:?}", e); + return ExternalMiningOutcome::Interrupted; + }, + }; + + // Create and broadcast job + *job_counter += 1; + let job_id = job_counter.to_string(); + let mining_hash = hex::encode(metadata.pre_hash.as_bytes()); + log::info!( + "⛏️ Broadcasting job {}: pre_hash={}, difficulty={}", + job_id, + mining_hash, + difficulty + ); + let job = MiningRequest { + job_id: job_id.clone(), + mining_hash, + distance_threshold: difficulty.to_string(), + }; + + server.broadcast_job(job).await; + + // Wait for results from miners, retrying on invalid seals + let best_hash = metadata.best_hash; + loop { + let (miner_id, seal) = match wait_for_mining_result(server, &job_id, || { + cancellation_token.is_cancelled() || + worker_handle.metadata().map(|m| m.best_hash != best_hash).unwrap_or(true) + }) + .await + { + Some(result) => result, + None => return ExternalMiningOutcome::Interrupted, + }; + + // Verify the seal before attempting to submit (submit consumes the build) + if !worker_handle.verify_seal(&seal) { + log::error!( + "🚨🚨🚨 INVALID SEAL FROM MINER {}! Job {} - seal failed verification. This may indicate a miner bug or stale work. Continuing to wait for valid seals...", + miner_id, + job_id + ); + continue; + } + + // Seal is valid, submit it + if futures::executor::block_on(worker_handle.submit(seal.clone())) { + let mining_time = mining_start_time.elapsed().as_secs(); + log::info!( + "🥇 Successfully mined and submitted a new block via external miner {} (mining time: {}s)", + miner_id, + mining_time + ); + *mining_start_time = std::time::Instant::now(); + return ExternalMiningOutcome::Success; + } + + // Submit failed for some other reason (should be rare after verify_seal passed) + log::warn!( + "⛏️ Failed to submit verified seal from miner {}, continuing to wait (job {})", + miner_id, + job_id + ); + } +} + +/// Try to find a valid nonce for local mining. +/// +/// Tries 50k nonces from a random starting point, then yields to check for new blocks. +/// With Poseidon2 hashing this takes ~50-100ms, keeping the node responsive. +async fn handle_local_mining( + client: &Arc, + worker_handle: &MiningHandle< + Block, + FullClient, + Arc>, + (), + >, +) -> Option> { + let metadata = worker_handle.metadata()?; + let version = worker_handle.version(); + let block_hash = metadata.pre_hash.0; + let difficulty = client.runtime_api().get_difficulty(metadata.best_hash).unwrap_or_else(|e| { + log::warn!("API error getting difficulty: {:?}", e); + U512::zero() + }); + + if difficulty.is_zero() { + return None; + } + + let start_nonce = U512::from(rand::random::()); + let target = U512::MAX / difficulty; + + let found = tokio::task::spawn_blocking(move || { + let mut nonce = start_nonce; + for _ in 0..50_000 { + let nonce_bytes = nonce.to_big_endian(); + if qpow_math::get_nonce_hash(block_hash, nonce_bytes) < target { + return Some(nonce_bytes); + } + nonce = nonce.overflowing_add(U512::one()).0; + } + None + }) + .await + .ok() + .flatten(); + + found.filter(|_| worker_handle.version() == version).map(|nonce| nonce.encode()) +} + +/// Submit a mined seal to the worker handle. +/// +/// Returns `true` if submission was successful, `false` otherwise. +fn submit_mined_block( + worker_handle: &MiningHandle< + Block, + FullClient, + Arc>, + (), + >, + seal: Vec, + mining_start_time: &mut std::time::Instant, + source: &str, +) -> bool { + if futures::executor::block_on(worker_handle.submit(seal)) { + let mining_time = mining_start_time.elapsed().as_secs(); + log::info!( + "🥇 Successfully mined and submitted a new block{} (mining time: {}s)", + source, + mining_time + ); + *mining_start_time = std::time::Instant::now(); + true + } else { + log::warn!("⛏️ Failed to submit mined block{}", source); + false + } +} + +/// The main mining loop that coordinates local and external mining. +/// +/// This function runs continuously until the cancellation token is triggered. +/// It handles: +/// - Waiting for sync to complete +/// - Coordinating with external miners (if server is available) +/// - Falling back to local mining +async fn mining_loop( + client: Arc, + worker_handle: MiningHandle>, ()>, + sync_service: Arc>, + miner_server: Option>, + cancellation_token: CancellationToken, +) { + log::info!("⛏️ QPoW Mining task spawned"); + + let mut mining_start_time = std::time::Instant::now(); + let mut job_counter: u64 = 0; + + loop { + if cancellation_token.is_cancelled() { + log::info!("⛏️ QPoW Mining task shutting down gracefully"); + break; + } + + // Don't mine if we're still syncing + if sync_service.is_major_syncing() { + log::debug!(target: "pow", "Mining paused: node is still syncing with network"); + tokio::select! { + _ = tokio::time::sleep(Duration::from_secs(5)) => {} + _ = cancellation_token.cancelled() => continue + } + continue; + } + + // Wait for mining metadata to be available + if worker_handle.metadata().is_none() { + log::debug!(target: "pow", "No mining metadata available"); + tokio::select! { + _ = tokio::time::sleep(Duration::from_millis(250)) => {} + _ = cancellation_token.cancelled() => continue + } + continue; + } + + if let Some(ref server) = miner_server { + // External mining path + handle_external_mining( + server, + &client, + &worker_handle, + &cancellation_token, + &mut job_counter, + &mut mining_start_time, + ) + .await; + } else if let Some(seal) = handle_local_mining(&client, &worker_handle).await { + // Local mining path + submit_mined_block(&worker_handle, seal, &mut mining_start_time, ""); + } + + // Yield to let other async tasks run + tokio::task::yield_now().await; + } + + log::info!("⛏️ QPoW Mining task terminated"); +} + +/// Spawn the transaction logger task. +/// +/// This task logs transactions as they are added to the pool. +/// Only available when the `tx-logging` feature is enabled. +#[cfg(feature = "tx-logging")] +fn spawn_transaction_logger( + task_manager: &TaskManager, + transaction_pool: Arc>, + tx_stream: impl futures::Stream + Send + 'static, +) { + task_manager.spawn_handle().spawn("tx-logger", None, async move { + let tx_stream = tx_stream; + futures::pin_mut!(tx_stream); + while let Some(tx_hash) = tx_stream.next().await { + if let Some(tx) = transaction_pool.ready_transaction(&tx_hash) { + log::trace!(target: "miner", "New transaction: Hash = {:?}", tx_hash); + let extrinsic = tx.data(); + log::trace!(target: "miner", "Payload: {:?}", extrinsic); + } else { + log::warn!("⛏️ Transaction {:?} not found in pool", tx_hash); + } + } + }); +} + +/// Spawn all authority-related tasks (mining, metrics, transaction logging). +/// +/// This is only called when the node is running as an authority (block producer). +#[allow(clippy::too_many_arguments)] +fn spawn_authority_tasks( + task_manager: &mut TaskManager, + client: Arc, + transaction_pool: Arc>, + select_chain: FullSelectChain, + pow_block_import: PowBlockImport, + sync_service: Arc>, + prometheus_registry: Option, + rewards_address: AccountId32, + miner_listen_port: Option, + tx_stream_for_worker: impl futures::Stream + Send + Unpin + 'static, + #[cfg(feature = "tx-logging")] tx_stream_for_logger: impl futures::Stream + + Send + + 'static, +) { + // Create block proposer factory + let proposer = ProposerFactory::new( + task_manager.spawn_handle(), + client.clone(), + transaction_pool.clone(), + prometheus_registry.as_ref(), + None, + ); + + // Create inherent data providers + let inherent_data_providers = Box::new(move |_, _| async move { + let timestamp = sp_timestamp::InherentDataProvider::from_system_time(); + Ok(timestamp) + }) + as Box< + dyn CreateInherentDataProviders< + Block, + (), + InherentDataProviders = sp_timestamp::InherentDataProvider, + >, + >; + + // Start the mining worker (block building task) + let (worker_handle, worker_task) = sc_consensus_qpow::start_mining_worker( + Box::new(pow_block_import), + client.clone(), + select_chain, + proposer, + sync_service.clone(), + sync_service.clone(), + rewards_address, + inherent_data_providers, + tx_stream_for_worker, + Duration::from_secs(10), + ); + + task_manager + .spawn_essential_handle() + .spawn_blocking("block-producer", None, worker_task); + + // Start Prometheus business metrics monitoring + BusinessMetrics::start_monitoring_task(client.clone(), prometheus_registry, task_manager); + + // Setup graceful shutdown for mining + let mining_cancellation_token = CancellationToken::new(); + let mining_token_clone = mining_cancellation_token.clone(); + + task_manager.spawn_handle().spawn("mining-shutdown-listener", None, async move { + tokio::signal::ctrl_c().await.expect("Failed to listen for Ctrl+C"); + log::info!("🛑 Received Ctrl+C signal, shutting down qpow-mining worker"); + mining_token_clone.cancel(); + }); + + // Spawn the main mining loop + task_manager.spawn_essential_handle().spawn("qpow-mining", None, async move { + // Start miner server if port is specified + let miner_server: Option> = if let Some(port) = miner_listen_port { + match MinerServer::start(port).await { + Ok(server) => Some(server), + Err(e) => { + log::error!("⛏️ Failed to start miner server on port {}: {}", port, e); + None + }, + } + } else { + log::warn!("⚠️ No --miner-listen-port specified. Using LOCAL mining only."); + None + }; + + mining_loop(client, worker_handle, sync_service, miner_server, mining_cancellation_token) + .await; + }); + + // Spawn transaction logger (only when tx-logging feature is enabled) + #[cfg(feature = "tx-logging")] + spawn_transaction_logger(task_manager, transaction_pool, tx_stream_for_logger); + + log::info!(target: "miner", "⛏️ Pow miner spawned"); +} + +// ============================================================================ +// Type Definitions +// ============================================================================ + pub(crate) type FullClient = sc_service::TFullClient< Block, RuntimeApi, @@ -152,7 +623,7 @@ pub fn new_full< >( config: Configuration, rewards_address: AccountId32, - external_miner_url: Option, + miner_listen_port: Option, enable_peer_sharing: bool, ) -> Result { let sc_service::PartialComponents { @@ -167,6 +638,7 @@ pub fn new_full< } = new_partial(&config)?; let tx_stream_for_worker = transaction_pool.clone().import_notification_stream(); + #[cfg(feature = "tx-logging")] let tx_stream_for_logger = transaction_pool.clone().import_notification_stream(); let net_config = sc_network::config::FullNetworkConfiguration::< @@ -248,291 +720,33 @@ pub fn new_full< })?; if role.is_authority() { - let proposer = sc_basic_authorship::ProposerFactory::new( - task_manager.spawn_handle(), - client.clone(), - transaction_pool.clone(), - prometheus_registry.as_ref(), - None, // lets worry about telemetry later! TODO - ); - - let inherent_data_providers = Box::new(move |_, _| async move { - let timestamp = sp_timestamp::InherentDataProvider::from_system_time(); - Ok(timestamp) - }) - as Box< - dyn CreateInherentDataProviders< - Block, - (), - InherentDataProviders = sp_timestamp::InherentDataProvider, - >, - >; - - let (worker_handle, worker_task) = sc_consensus_qpow::start_mining_worker( - Box::new(pow_block_import), - client.clone(), + #[cfg(feature = "tx-logging")] + spawn_authority_tasks( + &mut task_manager, + client, + transaction_pool, select_chain.clone(), - proposer, - sync_service.clone(), - sync_service.clone(), + pow_block_import, + sync_service, + prometheus_registry, rewards_address, - inherent_data_providers, + miner_listen_port, tx_stream_for_worker, - Duration::from_secs(10), + tx_stream_for_logger, ); - - task_manager.spawn_essential_handle().spawn_blocking("pow", None, worker_task); - - ResonanceBusinessMetrics::start_monitoring_task( - client.clone(), - prometheus_registry.clone(), - &task_manager, + #[cfg(not(feature = "tx-logging"))] + spawn_authority_tasks( + &mut task_manager, + client, + transaction_pool, + select_chain.clone(), + pow_block_import, + sync_service, + prometheus_registry, + rewards_address, + miner_listen_port, + tx_stream_for_worker, ); - - let mining_cancellation_token = CancellationToken::new(); - let mining_token_clone = mining_cancellation_token.clone(); - - // Listen for shutdown signals - task_manager.spawn_handle().spawn("mining-shutdown-listener", None, async move { - tokio::signal::ctrl_c().await.expect("Failed to listen for Ctrl+C"); - log::info!("🛑 Received Ctrl+C signal, shutting down qpow-mining worker"); - mining_token_clone.cancel(); - }); - - task_manager.spawn_essential_handle().spawn("qpow-mining", None, async move { - log::info!("⛏️ QPoW Mining task spawned"); - let mut nonce: U512 = U512::one(); - let http_client = Client::new(); - let mut current_job_id: Option = None; - - // Submit new mining job - let mut mining_start_time = std::time::Instant::now(); - log::info!("Mining start time: {:?}", mining_start_time); - - loop { - // Check for cancellation - if mining_cancellation_token.is_cancelled() { - log::info!("⛏️ QPoW Mining task shutting down gracefully"); - - // Cancel any pending external mining job - if let Some(job_id) = ¤t_job_id { - if let Some(miner_url) = &external_miner_url { - if let Err(e) = external_miner_client::cancel_mining_job( - &http_client, - miner_url, - job_id, - ) - .await - { - log::warn!("⛏️Failed to cancel mining job during shutdown: {}", e); - } - } - } - - break; - } - - // Don't mine if we're still syncing - if sync_service.is_major_syncing() { - log::debug!(target: "pow", "Mining paused: node is still syncing with network"); - tokio::select! { - _ = tokio::time::sleep(Duration::from_secs(5)) => {}, - _ = mining_cancellation_token.cancelled() => continue, - } - continue; - } - - // Get mining metadata - let metadata = match worker_handle.metadata() { - Some(m) => m, - None => { - log::debug!(target: "pow", "No mining metadata available"); - tokio::select! { - _ = tokio::time::sleep(Duration::from_millis(250)) => {}, - _ = mining_cancellation_token.cancelled() => continue, - } - continue; - }, - }; - let version = worker_handle.version(); - - // If external miner URL is provided, use external mining - if let Some(miner_url) = &external_miner_url { - // Fire-and-forget cancellation of previous job - don't wait for confirmation - // This reduces latency when switching to a new block - if let Some(old_job_id) = current_job_id.take() { - let cancel_client = http_client.clone(); - let cancel_url = miner_url.clone(); - tokio::spawn(async move { - if let Err(e) = external_miner_client::cancel_mining_job( - &cancel_client, - &cancel_url, - &old_job_id, - ) - .await - { - log::debug!("⛏️ Failed to cancel previous mining job {}: {}", old_job_id, e); - } - }); - } - - // Get current distance_threshold from runtime - let difficulty = - match client.runtime_api().get_difficulty(metadata.best_hash) { - Ok(d) => d, - Err(e) => { - log::warn!("⛏️Failed to get difficulty: {:?}", e); - tokio::select! { - _ = tokio::time::sleep(Duration::from_millis(250)) => {}, - _ = mining_cancellation_token.cancelled() => continue, - } - continue; - }, - }; - - // Generate new job ID - let job_id = Uuid::new_v4().to_string(); - current_job_id = Some(job_id.clone()); - - if let Err(e) = external_miner_client::submit_mining_job( - &http_client, - miner_url, - &job_id, - &metadata.pre_hash, - difficulty, - nonce, - U512::max_value(), - ) - .await - { - log::warn!("⛏️Failed to submit mining job: {}", e); - tokio::select! { - _ = tokio::time::sleep(Duration::from_millis(250)) => {}, - _ = mining_cancellation_token.cancelled() => continue, - } - continue; - } - - // Poll for results - loop { - match external_miner_client::check_mining_result( - &http_client, - miner_url, - &job_id, - ) - .await - { - Ok(Some(seal)) => { - let current_version = worker_handle.version(); - if current_version == version { - if futures::executor::block_on( - worker_handle.submit(seal.encode()), - ) { - let mining_time = mining_start_time.elapsed().as_secs(); - log::info!("🥇 Successfully mined and submitted a new block via external miner (mining time: {}s)", mining_time); - nonce = U512::one(); - mining_start_time = std::time::Instant::now(); - } else { - log::warn!( - "⛏️ Failed to submit mined block from external miner" - ); - nonce += U512::one(); - } - } else { - log::debug!(target: "miner", "Work from external miner is stale, discarding."); - } - break; - }, - Ok(None) => { - // Still working, check if metadata has changed - if worker_handle - .metadata() - .map(|m| m.best_hash != metadata.best_hash) - .unwrap_or(false) - { - break; - } - tokio::select! { - _ = tokio::time::sleep(Duration::from_millis(500)) => {}, - _ = mining_cancellation_token.cancelled() => return, - } - }, - Err(e) => { - log::warn!("⛏️Polling external miner result failed: {}", e); - break; - }, - } - } - } else { - // Local mining: try a range of N sequential nonces using optimized path - let block_hash = metadata.pre_hash.0; // [u8;32] - let start_nonce_bytes = nonce.to_big_endian(); - let difficulty = client - .runtime_api() - .get_difficulty(metadata.best_hash) - .unwrap_or_else(|e| { - log::warn!("API error getting difficulty: {:?}", e); - U512::zero() - }); - let nonces_to_mine = 300u64; - - let found = match tokio::task::spawn_blocking(move || { - mine_range(block_hash, start_nonce_bytes, nonces_to_mine, difficulty) - }) - .await - { - Ok(res) => res, - Err(e) => { - log::warn!("⛏️Local mining task failed: {}", e); - None - }, - }; - - let nonce_bytes = if let Some((good_nonce, _distance)) = found { - good_nonce - } else { - nonce += U512::from(nonces_to_mine); - // Yield back to the runtime to avoid starving other tasks - tokio::task::yield_now().await; - continue; - }; - - let current_version = worker_handle.version(); - // TODO: what does this check do? - if current_version == version { - if futures::executor::block_on(worker_handle.submit(nonce_bytes.encode())) { - let mining_time = mining_start_time.elapsed().as_secs(); - log::info!("🥇 Successfully mined and submitted a new block (mining time: {}s)", mining_time); - nonce = U512::one(); - mining_start_time = std::time::Instant::now(); - } else { - log::warn!("⛏️Failed to submit mined block"); - nonce += U512::one(); - } - } - - // Yield after each mining batch to cooperate with other tasks - tokio::task::yield_now().await; - } - } - - log::info!("⛏️ QPoW Mining task terminated"); - }); - - task_manager.spawn_handle().spawn("tx-logger", None, async move { - let mut tx_stream = tx_stream_for_logger; - while let Some(tx_hash) = tx_stream.next().await { - if let Some(tx) = transaction_pool.ready_transaction(&tx_hash) { - log::trace!(target: "miner", "New transaction: Hash = {:?}", tx_hash); - let extrinsic = tx.data(); - log::trace!(target: "miner", "Payload: {:?}", extrinsic); - } else { - log::warn!("⛏️Transaction {:?} not found in pool", tx_hash); - } - } - }); - - log::info!(target: "miner", "⛏️ Pow miner spawned"); } // Start deterministic-depth finalization task diff --git a/qpow-math/Cargo.toml b/qpow-math/Cargo.toml index e241d510..2a6066bd 100644 --- a/qpow-math/Cargo.toml +++ b/qpow-math/Cargo.toml @@ -6,8 +6,6 @@ version = "0.1.0" [dependencies] hex = { workspace = true, features = ["alloc"] } log = { version = "0.4.22", default-features = false } -num-bigint = { version = "0.4", default-features = false } -num-traits = { version = "0.2", default-features = false } primitive-types = { version = "0.13.1", default-features = false } qp-poseidon-core = { workspace = true } diff --git a/qpow-math/src/lib.rs b/qpow-math/src/lib.rs index 50142ee9..af2a4f1f 100644 --- a/qpow-math/src/lib.rs +++ b/qpow-math/src/lib.rs @@ -49,49 +49,6 @@ pub fn get_nonce_hash( result } -/// Mine a contiguous range of nonces using simple incremental search. -/// Returns the first valid nonce and its hash if one is found. -/// This is called during local mining -pub fn mine_range( - block_hash: [u8; 32], - start_nonce: [u8; 64], - steps: u64, - difficulty: U512, -) -> Option<([u8; 64], U512)> { - if steps == 0 { - return None; - } - - if difficulty == U512::zero() { - log::error!( - "mine_range should not be called with 0 difficulty, but was for block_hash: {:?}", - block_hash - ); - return None; - } - - let mut nonce_u = U512::from_big_endian(&start_nonce); - let max_target = U512::MAX; - let target = max_target / difficulty; - - for _ in 0..steps { - let nonce_bytes = nonce_u.to_big_endian(); - let hash_result = get_nonce_hash(block_hash, nonce_bytes); - - if hash_result < target { - log::debug!(target: "math", "💎 Local miner found nonce {:x} with hash {:x} and target {:x} and block_hash {:?}", - nonce_u.low_u32() as u16, hash_result.low_u32() as u16, - target.low_u32() as u16, hex::encode(block_hash)); - return Some((nonce_bytes, hash_result)); - } - - // Advance to next nonce - nonce_u = nonce_u.saturating_add(U512::from(1u64)); - } - - None -} - #[cfg(test)] mod tests { use super::*; From de0874e1ead0372ebfb48117a2c32eaf476ae5c5 Mon Sep 17 00:00:00 2001 From: illuzen Date: Fri, 6 Feb 2026 16:39:11 +0800 Subject: [PATCH 25/27] use new plonky2-verifier crate --- Cargo.lock | 263 ++++++++++++++++++++-------------- Cargo.toml | 6 + node/Cargo.toml | 1 - node/build.rs | 48 ++++--- pallets/wormhole/Cargo.toml | 4 - pallets/wormhole/common.bin | Bin 1905 -> 1057 bytes pallets/wormhole/src/lib.rs | 17 ++- pallets/wormhole/verifier.bin | Bin 552 -> 552 bytes 8 files changed, 198 insertions(+), 141 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 317c5e8d..bb445382 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -148,7 +148,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b88cf92ed20685979ed1d8472422f0c6c2d010cec77caf63aaa7669cc1a7bc2" dependencies = [ "alloy-rlp", - "bytes 1.11.0", + "bytes 1.11.1", "cfg-if", "const-hex", "derive_more 2.1.1", @@ -175,7 +175,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e93e50f64a77ad9c5470bf2ad0ca02f228da70c792a8f06634801e202579f35e" dependencies = [ "arrayvec 0.7.6", - "bytes 1.11.0", + "bytes 1.11.1", ] [[package]] @@ -315,9 +315,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "approx" @@ -979,7 +979,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4057f2c32adbb2fc158e22fb38433c8e9bbf76b75a4732c7c0cbaf695fb65568" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "futures-sink", "futures-util", "memchr", @@ -992,7 +992,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a860072022177f903e59730004fb5dc13db9275b79bb2aef7ba8ce831956c233" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "futures-sink", "futures-util", "memchr", @@ -1417,9 +1417,9 @@ checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" dependencies = [ "serde", ] @@ -1669,9 +1669,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.56" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75ca66430e33a14957acc24c5077b503e7d374151b2b4b3a10c83b4ceb4be0e" +checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" dependencies = [ "clap_builder", "clap_derive", @@ -1679,9 +1679,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.56" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793207c7fa6300a0608d1080b858e5fdbe713cdc1c8db9fb17777d8a13e63df0" +checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" dependencies = [ "anstream", "anstyle", @@ -1763,7 +1763,7 @@ version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "memchr", ] @@ -2245,7 +2245,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3730b03ec9f86c53e4934f41bc19a3231a20e884ab6a0b3eb7ed93714ae2fa" dependencies = [ "array-bytes 6.2.3", - "bytes 1.11.0", + "bytes 1.11.1", "cumulus-pallet-parachain-system-proc-macro", "cumulus-primitives-core", "cumulus-primitives-parachain-inherent", @@ -3197,7 +3197,7 @@ checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" dependencies = [ "arrayvec 0.7.6", "auto_impl", - "bytes 1.11.0", + "bytes 1.11.1", ] [[package]] @@ -3208,7 +3208,7 @@ checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" dependencies = [ "arrayvec 0.7.6", "auto_impl", - "bytes 1.11.0", + "bytes 1.11.1", ] [[package]] @@ -4083,7 +4083,7 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "fnv", "futures-core", "futures-sink", @@ -4103,7 +4103,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", - "bytes 1.11.0", + "bytes 1.11.1", "fnv", "futures-core", "futures-sink", @@ -4401,7 +4401,7 @@ version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "fnv", "itoa", ] @@ -4412,7 +4412,7 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "itoa", ] @@ -4422,7 +4422,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "http 0.2.12", "pin-project-lite 0.2.16", ] @@ -4433,7 +4433,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "http 1.4.0", ] @@ -4443,7 +4443,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "futures-core", "http 1.4.0", "http-body 1.0.1", @@ -4493,7 +4493,7 @@ version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "futures-channel", "futures-core", "futures-util", @@ -4518,7 +4518,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", - "bytes 1.11.0", + "bytes 1.11.1", "futures-channel", "futures-core", "h2 0.4.13", @@ -4554,13 +4554,12 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "futures-channel", - "futures-core", "futures-util", "http 1.4.0", "http-body 1.0.1", @@ -4746,7 +4745,7 @@ checksum = "064d90fec10d541084e7b39ead8875a5a80d9114a2b18791565253bae25f49e4" dependencies = [ "async-trait", "attohttpc", - "bytes 1.11.0", + "bytes 1.11.1", "futures 0.3.31", "http 0.2.12", "hyper 0.14.32", @@ -4995,9 +4994,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jiff" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" +checksum = "d89a5b5e10d5a9ad6e5d1f4bd58225f655d6fe9767575a5e8ac5a6fe64e04495" dependencies = [ "jiff-static", "log", @@ -5008,9 +5007,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" +checksum = "ff7a39c8862fc1369215ccf0a8f12dd4598c7f6484704359f0351bd617034dbf" dependencies = [ "proc-macro2", "quote", @@ -5105,7 +5104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "348ee569eaed52926b5e740aae20863762b16596476e943c9e415a6479021622" dependencies = [ "async-trait", - "bytes 1.11.0", + "bytes 1.11.1", "futures-timer", "futures-util", "http 1.4.0", @@ -5346,7 +5345,7 @@ version = "0.54.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbe80f9c7e00526cd6b838075b9c171919404a4732cb2fa8ece0a093223bfc4" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "either", "futures 0.3.31", "futures-timer", @@ -5492,7 +5491,7 @@ checksum = "ced237d0bd84bbebb7c2cad4c073160dacb4fe40534963c32ed6d4c6bb7702a3" dependencies = [ "arrayvec 0.7.6", "asynchronous-codec 0.7.0", - "bytes 1.11.0", + "bytes 1.11.1", "either", "fnv", "futures 0.3.31", @@ -5558,7 +5557,7 @@ version = "0.45.10" source = "git+https://github.com/Quantus-Network/qp-libp2p-noise?tag=v0.45.10#901f09f30b32f910395270bba3a566191dc2f61f" dependencies = [ "asynchronous-codec 0.6.2", - "bytes 1.11.0", + "bytes 1.11.1", "clatter", "futures 0.3.31", "libp2p-core", @@ -5599,7 +5598,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46352ac5cd040c70e88e7ff8257a2ae2f891a4076abad2c439584a31c15fd24e" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "futures 0.3.31", "futures-timer", "if-watch", @@ -5917,7 +5916,7 @@ checksum = "14fb10e63363204b89d91e1292df83322fd9de5d7fa76c3d5c78ddc2f8f3efa9" dependencies = [ "async-trait", "bs58", - "bytes 1.11.0", + "bytes 1.11.1", "cid 0.11.1", "ed25519-dalek", "futures 0.3.31", @@ -6407,7 +6406,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea0df8e5eec2298a62b326ee4f0d7fe1a6b90a09dfcf9df37b38f947a8c42f19" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "futures 0.3.31", "log", "pin-project", @@ -6501,7 +6500,7 @@ version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "futures 0.3.31", "log", "netlink-packet-core", @@ -6515,7 +6514,7 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "futures-util", "libc", "log", @@ -7599,7 +7598,7 @@ dependencies = [ "qp-wormhole-circuit", "qp-wormhole-circuit-builder", "qp-wormhole-prover", - "qp-wormhole-verifier", + "qp-wormhole-verifier 1.0.1", "qp-zk-circuits-common", "scale-info", "sp-core", @@ -7653,7 +7652,7 @@ dependencies = [ "arrayvec 0.7.6", "bitvec", "byte-slice-cast", - "bytes 1.11.0", + "bytes 1.11.1", "const_format", "impl-trait-for-tuples", "parity-scale-codec-derive", @@ -7800,9 +7799,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.5" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" dependencies = [ "memchr", "ucd-trie", @@ -7810,9 +7809,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.5" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" dependencies = [ "pest", "pest_generator", @@ -7820,9 +7819,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.5" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" dependencies = [ "pest", "pest_meta", @@ -7833,9 +7832,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.5" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ "pest", "sha2 0.10.9", @@ -7935,6 +7934,10 @@ dependencies = [ "rayon", ] +[[package]] +name = "plonky2_util" +version = "1.0.0" + [[package]] name = "plonky2_util" version = "1.0.0" @@ -8775,9 +8778,9 @@ dependencies = [ [[package]] name = "proptest" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" +checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" dependencies = [ "bit-set", "bit-vec", @@ -8798,7 +8801,7 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "prost-derive 0.12.6", ] @@ -8808,7 +8811,7 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "prost-derive 0.13.5", ] @@ -8931,8 +8934,8 @@ dependencies = [ "p3-poseidon2", "p3-symmetric", "plonky2_maybe_rayon", - "plonky2_util", - "qp-plonky2-field", + "plonky2_util 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "qp-plonky2-field 1.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "qp-poseidon-constants", "rand 0.8.5", "rand_chacha 0.3.1", @@ -8942,6 +8945,20 @@ dependencies = [ "web-time", ] +[[package]] +name = "qp-plonky2-field" +version = "1.1.3" +dependencies = [ + "anyhow", + "itertools 0.11.0", + "num", + "plonky2_util 1.0.0", + "rustc_version 0.4.1", + "serde", + "static_assertions", + "unroll", +] + [[package]] name = "qp-plonky2-field" version = "1.1.3" @@ -8951,7 +8968,7 @@ dependencies = [ "anyhow", "itertools 0.11.0", "num", - "plonky2_util", + "plonky2_util 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.8.5", "rustc_version 0.4.1", "serde", @@ -8959,6 +8976,29 @@ dependencies = [ "unroll", ] +[[package]] +name = "qp-plonky2-verifier" +version = "2.0.0" +dependencies = [ + "ahash", + "anyhow", + "hashbrown 0.14.5", + "itertools 0.11.0", + "keccak-hash 0.8.0", + "log", + "num", + "p3-field", + "p3-goldilocks", + "p3-poseidon2", + "p3-symmetric", + "plonky2_util 1.0.0", + "qp-plonky2-field 1.1.3", + "qp-poseidon-constants", + "serde", + "static_assertions", + "unroll", +] + [[package]] name = "qp-poseidon" version = "1.0.7" @@ -9073,6 +9113,13 @@ dependencies = [ "qp-zk-circuits-common", ] +[[package]] +name = "qp-wormhole-inputs" +version = "1.0.1" +dependencies = [ + "anyhow", +] + [[package]] name = "qp-wormhole-prover" version = "0.1.8" @@ -9095,6 +9142,15 @@ dependencies = [ "qp-zk-circuits-common", ] +[[package]] +name = "qp-wormhole-verifier" +version = "1.0.1" +dependencies = [ + "anyhow", + "qp-plonky2-verifier", + "qp-wormhole-inputs", +] + [[package]] name = "qp-zk-circuits-common" version = "0.1.8" @@ -9161,8 +9217,7 @@ dependencies = [ "qp-poseidon", "qp-rusty-crystals-dilithium", "qp-rusty-crystals-hdwallet", - "qp-wormhole-circuit-builder", - "qp-wormhole-verifier", + "qp-wormhole-verifier 1.0.1", "qpow-math", "quantus-miner-api", "quantus-runtime", @@ -9245,7 +9300,7 @@ dependencies = [ "qp-scheduler", "qp-wormhole", "qp-wormhole-circuit", - "qp-wormhole-verifier", + "qp-wormhole-verifier 0.1.8", "qp-zk-circuits-common", "scale-info", "serde_json", @@ -9288,7 +9343,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15a0580ab32b169745d7a39db2ba969226ca16738931be152a3209b409de2474" dependencies = [ "asynchronous-codec 0.7.0", - "bytes 1.11.0", + "bytes 1.11.1", "quick-protobuf", "thiserror 1.0.69", "unsigned-varint 0.8.0", @@ -9300,7 +9355,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cc2c5017e4b43d5995dcea317bc46c1e09404c0a9664d2908f7f02dfe943d75" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "pin-project-lite 0.2.16", "quinn-proto 0.10.6", "quinn-udp 0.4.1", @@ -9317,7 +9372,7 @@ version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "cfg_aliases 0.2.1", "futures-io", "pin-project-lite 0.2.16", @@ -9338,7 +9393,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "141bf7dfde2fbc246bfd3fe12f2455aa24b0fbd9af535d8c86c7bd1381ff2b1a" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "rand 0.8.5", "ring 0.16.20", "rustc-hash 1.1.0", @@ -9356,7 +9411,7 @@ version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "getrandom 0.3.4", "lru-slab", "rand 0.9.2", @@ -9377,7 +9432,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "055b4e778e8feb9f93c4e439f71dc2156ef13360b432b799e179a8c4cdf0b1d7" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "libc", "socket2 0.5.10", "tracing", @@ -9510,9 +9565,9 @@ dependencies = [ [[package]] name = "rapidhash" -version = "4.2.1" +version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d8b5b858a440a0bc02625b62dd95131b9201aa9f69f411195dd4a7cfb1de3d7" +checksum = "71ec30b38a417407efe7676bad0ca6b78f995f810185ece9af3bd5dc561185a9" dependencies = [ "rustversion", ] @@ -9649,9 +9704,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -9661,9 +9716,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -9672,9 +9727,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "resolv-conf" @@ -9736,7 +9791,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "rustc-hex", ] @@ -9746,7 +9801,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa24e92bb2a83198bb76d661a71df9f7076b8c420b8696e4d3d97d50d94479e3" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "rustc-hex", ] @@ -9815,7 +9870,7 @@ dependencies = [ "ark-ff 0.3.0", "ark-ff 0.4.2", "ark-ff 0.5.0", - "bytes 1.11.0", + "bytes 1.11.1", "fastrlp 0.3.1", "fastrlp 0.4.0", "num-bigint", @@ -10558,7 +10613,7 @@ dependencies = [ "array-bytes 6.2.3", "arrayvec 0.7.6", "blake2 0.10.6", - "bytes 1.11.0", + "bytes 1.11.1", "futures 0.3.31", "futures-timer", "log", @@ -10587,7 +10642,7 @@ dependencies = [ "async-channel 1.9.0", "async-trait", "asynchronous-codec 0.6.2", - "bytes 1.11.0", + "bytes 1.11.1", "cid 0.9.0", "criterion", "either", @@ -10728,7 +10783,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "441af5d0adf306ff745ccf23c7426ec2edf24f6fee678fb63994e1f8d2fcc890" dependencies = [ "bs58", - "bytes 1.11.0", + "bytes 1.11.1", "ed25519-dalek", "libp2p-identity", "libp2p-kad", @@ -10749,7 +10804,7 @@ version = "46.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "029f2eb16f9510a749201e7a2b405aafcc5afcc515509518d2efb17b164ca47e" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "fnv", "futures 0.3.31", "futures-timer", @@ -11918,7 +11973,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" dependencies = [ "base64 0.22.1", - "bytes 1.11.0", + "bytes 1.11.1", "futures 0.3.31", "http 1.4.0", "httparse", @@ -12278,7 +12333,7 @@ version = "41.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3f244e9a2818d21220ceb0915ac73a462814a92d0c354a124a818abdb7f4f66" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "docify", "ed25519-dalek", "libsecp256k1", @@ -12455,7 +12510,7 @@ version = "30.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fcd9c219da8c85d45d5ae1ce80e73863a872ac27424880322903c6ac893c06e" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "impl-trait-for-tuples", "parity-scale-codec", "polkavm-derive 0.24.0", @@ -13519,9 +13574,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.46" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", @@ -13540,9 +13595,9 @@ checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.26" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -13609,7 +13664,7 @@ version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "libc", "mio", "parking_lot 0.12.5", @@ -13686,7 +13741,7 @@ version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "futures-core", "futures-io", "futures-sink", @@ -13796,7 +13851,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ "bitflags 2.10.0", - "bytes 1.11.0", + "bytes 1.11.1", "http 1.4.0", "http-body 1.0.1", "http-body-util", @@ -14003,7 +14058,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "data-encoding", "http 1.4.0", "httparse", @@ -14137,7 +14192,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6889a77d49f1f013504cec6bf97a2c730394adedaeb1deb5ea08949a50541105" dependencies = [ "asynchronous-codec 0.6.2", - "bytes 1.11.0", + "bytes 1.11.1", "futures-io", "futures-util", ] @@ -14148,7 +14203,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "tokio-util", ] @@ -14752,14 +14807,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" dependencies = [ - "webpki-root-certs 1.0.5", + "webpki-root-certs 1.0.6", ] [[package]] name = "webpki-root-certs" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" dependencies = [ "rustls-pki-types", ] @@ -15411,18 +15466,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.37" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7456cf00f0685ad319c5b1693f291a650eaf345e941d082fc4e03df8a03996ac" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.37" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1328722bbf2115db7e19d69ebcc15e795719e2d66b60827c6a69a117365e37a0" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 99ea2417..931f7405 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -240,6 +240,12 @@ sc-network = { path = "client/network" } sp-state-machine = { path = "./primitives/state-machine" } sp-trie = { path = "./primitives/trie" } +[patch."https://github.com/Quantus-Network/qp-zk-circuits"] +qp-wormhole-verifier = { path = "../qp-zk-circuits/wormhole/verifier" } +qp-wormhole-inputs = { path = "../qp-zk-circuits/wormhole/inputs" } + + + [profile.release] opt-level = 3 panic = "unwind" diff --git a/node/Cargo.toml b/node/Cargo.toml index ea57a90f..c76647eb 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -100,7 +100,6 @@ substrate-frame-rpc-system.workspace = true tokio-util.workspace = true [build-dependencies] -qp-wormhole-circuit-builder.workspace = true qp-wormhole-verifier.workspace = true substrate-build-script-utils.default-features = true substrate-build-script-utils.workspace = true diff --git a/node/build.rs b/node/build.rs index 2de7de2a..85ea5bd4 100644 --- a/node/build.rs +++ b/node/build.rs @@ -6,35 +6,37 @@ fn main() { rerun_if_git_head_changed(); - // Circuit validation and generation - validate_and_generate_circuits(); + // Validate pre-generated circuit binaries + validate_circuit_binaries(); } -fn validate_and_generate_circuits() { +fn validate_circuit_binaries() { println!("cargo:rerun-if-changed=pallets/wormhole/verifier.bin"); println!("cargo:rerun-if-changed=pallets/wormhole/common.bin"); + println!("cargo:rerun-if-changed=pallets/wormhole/aggregated_verifier.bin"); + println!("cargo:rerun-if-changed=pallets/wormhole/aggregated_common.bin"); - // Generate circuit binaries from the zk-circuits dependency - generate_circuit_binaries(); - - // Validate generated binaries + // Validate the pre-generated wormhole circuit binaries let verifier_bytes = include_bytes!("../pallets/wormhole/verifier.bin"); let common_bytes = include_bytes!("../pallets/wormhole/common.bin"); - // This will fail at build time if the binaries are invalid - WormholeVerifier::new_from_bytes(verifier_bytes, common_bytes) - .expect("CRITICAL ERROR: Failed to create WormholeVerifier from embedded data. Check verifier.bin and common.bin"); - - println!("cargo:trace=✅ Wormhole circuit binaries generated and validated successfully"); -} - -fn generate_circuit_binaries() { - println!("cargo:trace=🔧 Generating wormhole circuit binaries from zk-circuits..."); - - // Call the circuit-builder to generate binaries directly in the pallet directory - // We don't need the prover binary for the chain, only verifier and common - qp_wormhole_circuit_builder::generate_circuit_binaries("../pallets/wormhole", false) - .expect("Failed to generate circuit binaries"); - - println!("cargo:trace=✅ Circuit binaries generated successfully"); + WormholeVerifier::new_from_bytes(verifier_bytes, common_bytes).expect( + "CRITICAL ERROR: Failed to create WormholeVerifier from embedded data. \ + The verifier.bin and common.bin files must be regenerated using qp-zk-circuits \ + with a compatible qp-plonky2 version. Run: \ + cd ../qp-zk-circuits/wormhole/circuit-builder && cargo run", + ); + + // TODO: Re-enable validation once aggregated circuit binaries are regenerated + // with the new qp-plonky2 version that has updated Poseidon2 gates. + // The aggregated circuit binaries were generated with an older qp-plonky2 version + // and have incompatible gate serialization. + // + // let agg_verifier_bytes = include_bytes!("../pallets/wormhole/aggregated_verifier.bin"); + // let agg_common_bytes = include_bytes!("../pallets/wormhole/aggregated_common.bin"); + // WormholeVerifier::new_from_bytes(agg_verifier_bytes, agg_common_bytes).expect( + // "CRITICAL ERROR: Failed to create aggregated WormholeVerifier from embedded data.", + // ); + + println!("cargo:trace=✅ Wormhole circuit binaries validated successfully"); } diff --git a/pallets/wormhole/Cargo.toml b/pallets/wormhole/Cargo.toml index be13c325..baeaada7 100644 --- a/pallets/wormhole/Cargo.toml +++ b/pallets/wormhole/Cargo.toml @@ -19,9 +19,7 @@ pallet-balances.workspace = true qp-header = { workspace = true, features = ["serde"] } qp-poseidon.workspace = true qp-wormhole.workspace = true -qp-wormhole-circuit = { workspace = true, default-features = false } qp-wormhole-verifier = { workspace = true, default-features = false } -qp-zk-circuits-common = { workspace = true, default-features = false } scale-info = { workspace = true, default-features = false, features = [ "derive", ] } @@ -64,10 +62,8 @@ std = [ "pallet-balances/std", "qp-header/std", "qp-poseidon/std", - "qp-wormhole-circuit/std", "qp-wormhole-verifier/std", "qp-wormhole/std", - "qp-zk-circuits-common/std", "scale-info/std", "sp-core/std", "sp-io/std", diff --git a/pallets/wormhole/common.bin b/pallets/wormhole/common.bin index 450d440e438b0c0b814fa2b9511ae2e41474f0c2..cb13a2f2abef47342355cb114887bc79bce130df 100644 GIT binary patch delta 98 zcmey!w~&LeeP2Zc@$X=bEx7|1_SBEy>q-(-OoZ6lJbtyt0PZ*6_v} z-de*u>&*LJ>}lm$LNx@I#%}&jb3!W^)_<;`JGY(iIDC7rxZEES8@iC zGg>1Ly@LoGuQH0=j+h0PM2WI5)FYn{h+GQA^hq&yLot0*-1d)#Vz#7sT|LSk5@$o0 dC$S+-Vhf57iaFyV=JILIr0{8peEu=o>>naa7::ProofDeserializationFailed)?; - // Parse public inputs using the existing parser - let public_inputs = PublicCircuitInputs::try_from(&proof) - .map_err(|_| Error::::InvalidPublicInputs)?; + // Parse public inputs using the verifier's parser + let public_inputs = + parse_public_inputs(&proof).map_err(|_| Error::::InvalidPublicInputs)?; let nullifier_bytes = *public_inputs.nullifier; @@ -507,9 +507,8 @@ pub mod pallet { .map_err(|_| Error::::AggregatedVerificationFailed)?; // Parse aggregated public inputs - let aggregated_inputs = - AggregatedPublicCircuitInputs::try_from_slice(&proof.public_inputs) - .map_err(|_| Error::::InvalidAggregatedPublicInputs)?; + let aggregated_inputs = parse_aggregated_public_inputs(&proof) + .map_err(|_| Error::::InvalidAggregatedPublicInputs)?; // Verify all nullifiers haven't been used and then mark them as used for nullifier in &aggregated_inputs.nullifiers { diff --git a/pallets/wormhole/verifier.bin b/pallets/wormhole/verifier.bin index 2b73f221b9b3df8025d2e70a15bee929ac7444dc..25e4a4441f28ed6774056c0f4ae846d5df260707 100644 GIT binary patch literal 552 zcmV+@0@wWn000000001$vILPFm28;MFUWyYo>&T=D4{o_FU9JpsfgBfZ9%z*)?U8B z7*4ngS~a=VTJ$D0|4;YWJ#1{QVC*eh)=|N?I=^DQY%D4JdYSxjd_9Q-!{d5I`>eq?DSl8LRMEjs*=$slaI}V1ykSF4rapJGmKeo_KWpTj^o+~7 z=$tV>qQZ}}v@?@)?ayiDwS4`;@$|vFkXRY?zSYbTGSElIHNb)pL66C2(cF5|{ q1i%`nK|h>eWyz^LHr`=8Lx%^yx;I>YjF#a_Az0&~KI2zHbtJzcUJXM4 literal 552 zcmV+@0@wWn0000000030E}T~!!aAZWzVd-Jz0H(B-F&=d=;4N!=3_?$0u@P-s0`%3 zdr)y-Y5NJ!lwW~*uh%bw1YuXCv0+*5W}1z|mksP7En8R*{LHc?0;WUW2>f%TsFcAv zZ~sXrC**yZ7YY7GCH0xZJx^k*5S($PxfPB*X$jx{BC#Nr%Fcfm4c|deg5g1p(f&B4 zLV;!|Ka(xAWga+?7Kt5^^fCOXy1|2z!_!AZC`u51ad1|j16X#+r~@e(M6cw^DucJn z{XC)lpHNFT(--o2c>$Z8?2T18+Yo=%9)0WB`3d;xmSar{$1iiJs}HI469rm43eG3u z@LgbIB3@LMk3-eVxY|Yof7ms3pc0=K|?L!H@-R&aO5u(3N)&Lc`uj1L|5 z?l{cVeW?o)z^!u-@7Az=sI|s`$wv5kY94+KZ?5YIM2e}kSD2+aJAWai?MH39QsmbE zu3M_F8JsbMM5hMV$O07y{(ukCatUE<)=vHKHn3J|yt}X0TI)M-n15?Ftgos&K(<4% zL8V Date: Fri, 6 Feb 2026 18:45:40 +0800 Subject: [PATCH 26/27] fix: Remove no_random feature and patch plonky2 crates to use local versions - Remove no_random feature from qp-wormhole-verifier and qp-zk-circuits-common dependencies - Add patches to use local qp-plonky2 and qp-plonky2-field with fixed rand feature handling - These changes ensure consistent feature resolution across all workspace members --- Cargo.lock | 40 ++++++---------------------------------- Cargo.toml | 3 +++ runtime/Cargo.toml | 8 +------- 3 files changed, 10 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bb445382..e37322ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,7 +78,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "const-random", "getrandom 0.3.4", "once_cell", "version_check", @@ -7928,8 +7927,6 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plonky2_maybe_rayon" version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1e554181dc95243b8d9948ae7bae5759c7fb2502fed28f671f95ef38079406" dependencies = [ "rayon", ] @@ -7938,12 +7935,6 @@ dependencies = [ name = "plonky2_util" version = "1.0.0" -[[package]] -name = "plonky2_util" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c32c137808ca984ab2458b612b7eb0462d853ee041a3136e83d54b96074c7610" - [[package]] name = "plotters" version = "0.3.7" @@ -8918,8 +8909,6 @@ dependencies = [ [[package]] name = "qp-plonky2" version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39530b02faa85964bba211e030afa2d54995b403b0022f88e984c4c65679c4bc" dependencies = [ "ahash", "anyhow", @@ -8934,8 +8923,9 @@ dependencies = [ "p3-poseidon2", "p3-symmetric", "plonky2_maybe_rayon", - "plonky2_util 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "qp-plonky2-field 1.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "plonky2_util", + "qp-plonky2-field", + "qp-plonky2-verifier", "qp-poseidon-constants", "rand 0.8.5", "rand_chacha 0.3.1", @@ -8952,23 +8942,7 @@ dependencies = [ "anyhow", "itertools 0.11.0", "num", - "plonky2_util 1.0.0", - "rustc_version 0.4.1", - "serde", - "static_assertions", - "unroll", -] - -[[package]] -name = "qp-plonky2-field" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8d52dadf3bb92708c309922b62d7f3f2587d3047f9fe05a0c9f587e2890526" -dependencies = [ - "anyhow", - "itertools 0.11.0", - "num", - "plonky2_util 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "plonky2_util", "rand 0.8.5", "rustc_version 0.4.1", "serde", @@ -8991,8 +8965,8 @@ dependencies = [ "p3-goldilocks", "p3-poseidon2", "p3-symmetric", - "plonky2_util 1.0.0", - "qp-plonky2-field 1.1.3", + "plonky2_util", + "qp-plonky2-field", "qp-poseidon-constants", "serde", "static_assertions", @@ -9299,9 +9273,7 @@ dependencies = [ "qp-poseidon", "qp-scheduler", "qp-wormhole", - "qp-wormhole-circuit", "qp-wormhole-verifier 0.1.8", - "qp-zk-circuits-common", "scale-info", "serde_json", "sp-api", diff --git a/Cargo.toml b/Cargo.toml index 931f7405..70cfc7bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -239,6 +239,9 @@ sc-cli = { path = "./client/cli" } sc-network = { path = "client/network" } sp-state-machine = { path = "./primitives/state-machine" } sp-trie = { path = "./primitives/trie" } +# Patch plonky2 crates to use local versions with fixed rand feature handling +qp-plonky2 = { path = "../qp-plonky2/plonky2" } +qp-plonky2-field = { path = "../qp-plonky2/field" } [patch."https://github.com/Quantus-Network/qp-zk-circuits"] qp-wormhole-verifier = { path = "../qp-zk-circuits/wormhole/verifier" } diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 477477e1..f1ba2bba 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -53,13 +53,7 @@ qp-header = { workspace = true, features = ["serde"] } qp-poseidon = { workspace = true, features = ["serde"] } qp-scheduler.workspace = true qp-wormhole.workspace = true -qp-wormhole-circuit = { workspace = true, default-features = false } -qp-wormhole-verifier = { workspace = true, default-features = false, features = [ - "no_random", -] } -qp-zk-circuits-common = { workspace = true, default-features = false, features = [ - "no_random", -] } +qp-wormhole-verifier = { workspace = true, default-features = false } scale-info = { features = ["derive", "serde"], workspace = true } serde_json = { workspace = true, default-features = false, features = [ "alloc", From 90bc55f3e69871b2f7a686b4b3fa0f6839ab9096 Mon Sep 17 00:00:00 2001 From: Cezary Olborski Date: Fri, 6 Feb 2026 19:44:46 +0800 Subject: [PATCH 27/27] Revert wormhole and plonky2 changes back to QUIC miner (#363) --- Cargo.lock | 2441 +++++++++-------- Cargo.toml | 32 +- MINING.md | 124 +- README.md | 50 +- client/consensus/qpow/src/lib.rs | 12 +- client/consensus/qpow/src/worker.rs | 6 +- node/Cargo.toml | 3 +- node/build.rs | 48 +- node/src/benchmarking.rs | 4 - node/src/cli.rs | 6 +- node/src/command.rs | 35 +- node/src/service.rs | 8 +- pallets/balances/Cargo.toml | 65 + pallets/balances/README.md | 127 + pallets/balances/src/benchmarking.rs | 350 +++ pallets/balances/src/impl_currency.rs | 952 +++++++ pallets/balances/src/impl_fungible.rs | 385 +++ pallets/balances/src/impl_proofs.rs | 28 + pallets/balances/src/lib.rs | 1335 +++++++++ pallets/balances/src/migration.rs | 103 + pallets/balances/src/tests/currency_tests.rs | 1643 +++++++++++ .../balances/src/tests/dispatchable_tests.rs | 410 +++ .../src/tests/fungible_conformance_tests.rs | 141 + pallets/balances/src/tests/fungible_tests.rs | 883 ++++++ pallets/balances/src/tests/general_tests.rs | 143 + pallets/balances/src/tests/mod.rs | 332 +++ .../balances/src/tests/reentrancy_tests.rs | 212 ++ .../src/tests/transfer_counter_tests.rs | 340 +++ pallets/balances/src/types.rs | 164 ++ pallets/balances/src/weights.rs | 300 ++ pallets/mining-rewards/Cargo.toml | 3 - pallets/mining-rewards/src/lib.rs | 93 +- pallets/mining-rewards/src/mock.rs | 86 +- pallets/mining-rewards/src/tests.rs | 60 +- pallets/multisig/src/mock.rs | 3 +- .../reversible-transfers/src/tests/mock.rs | 2 +- pallets/wormhole/Cargo.toml | 20 +- pallets/wormhole/aggregated_common.bin | Bin 1905 -> 0 bytes pallets/wormhole/aggregated_verifier.bin | Bin 552 -> 0 bytes pallets/wormhole/common.bin | Bin 1057 -> 1037 bytes pallets/wormhole/proof_from_bins.hex | 2 +- pallets/wormhole/src/benchmarking.rs | 47 +- pallets/wormhole/src/lib.rs | 724 +---- pallets/wormhole/src/mock.rs | 74 +- pallets/wormhole/src/tests.rs | 269 +- pallets/wormhole/src/weights.rs | 40 +- pallets/wormhole/verifier.bin | Bin 552 -> 552 bytes primitives/wormhole/src/lib.rs | 31 +- runtime/Cargo.toml | 6 - runtime/src/benchmarks.rs | 1 - runtime/src/configs/mod.rs | 45 +- runtime/src/genesis_config_presets.rs | 15 +- runtime/src/governance/definitions.rs | 10 +- runtime/src/lib.rs | 4 - runtime/src/transaction_extensions.rs | 259 +- runtime/tests/governance/treasury.rs | 9 +- 56 files changed, 9715 insertions(+), 2770 deletions(-) create mode 100644 pallets/balances/Cargo.toml create mode 100644 pallets/balances/README.md create mode 100644 pallets/balances/src/benchmarking.rs create mode 100644 pallets/balances/src/impl_currency.rs create mode 100644 pallets/balances/src/impl_fungible.rs create mode 100644 pallets/balances/src/impl_proofs.rs create mode 100644 pallets/balances/src/lib.rs create mode 100644 pallets/balances/src/migration.rs create mode 100644 pallets/balances/src/tests/currency_tests.rs create mode 100644 pallets/balances/src/tests/dispatchable_tests.rs create mode 100644 pallets/balances/src/tests/fungible_conformance_tests.rs create mode 100644 pallets/balances/src/tests/fungible_tests.rs create mode 100644 pallets/balances/src/tests/general_tests.rs create mode 100644 pallets/balances/src/tests/mod.rs create mode 100644 pallets/balances/src/tests/reentrancy_tests.rs create mode 100644 pallets/balances/src/tests/transfer_counter_tests.rs create mode 100644 pallets/balances/src/types.rs create mode 100644 pallets/balances/src/weights.rs delete mode 100644 pallets/wormhole/aggregated_common.bin delete mode 100644 pallets/wormhole/aggregated_verifier.bin diff --git a/Cargo.lock b/Cargo.lock index e37322ff..1adf393e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,11 +23,11 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.25.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ - "gimli 0.32.3", + "gimli 0.31.1", ] [[package]] @@ -78,7 +78,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom 0.3.4", + "const-random", + "getrandom 0.3.3", "once_cell", "version_check", "zerocopy", @@ -86,9 +87,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -101,9 +102,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy-core" -version = "1.5.4" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfbc46fa201350bf859add798d818bbe68b84882a8af832e4433791d28a975d" +checksum = "bfe6c56d58fbfa9f0f6299376e8ce33091fc6494239466814c3f54b55743cb09" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -114,9 +115,9 @@ dependencies = [ [[package]] name = "alloy-dyn-abi" -version = "1.5.4" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14ff5ee5f27aa305bda825c735f686ad71bb65508158f059f513895abe69b8c3" +checksum = "a3f56873f3cac7a2c63d8e98a4314b8311aa96adb1a0f82ae923eb2119809d2c" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -130,9 +131,9 @@ dependencies = [ [[package]] name = "alloy-json-abi" -version = "1.5.4" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8708475665cc00e081c085886e68eada2f64cfa08fc668213a9231655093d4de" +checksum = "125a1c373261b252e53e04d6e92c37d881833afc1315fceab53fd46045695640" dependencies = [ "alloy-primitives", "alloy-sol-type-parser", @@ -142,78 +143,78 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "1.5.4" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b88cf92ed20685979ed1d8472422f0c6c2d010cec77caf63aaa7669cc1a7bc2" +checksum = "bc9485c56de23438127a731a6b4c87803d49faf1a7068dcd1d8768aca3a9edb9" dependencies = [ "alloy-rlp", - "bytes 1.11.1", + "bytes 1.10.1", "cfg-if", "const-hex", - "derive_more 2.1.1", - "foldhash 0.2.0", - "hashbrown 0.16.1", - "indexmap 2.13.0", + "derive_more 2.0.1", + "foldhash 0.1.5", + "hashbrown 0.15.5", + "indexmap 2.11.4", "itoa", "k256", "keccak-asm", "paste", "proptest", "rand 0.9.2", - "rapidhash", "ruint", "rustc-hash 2.1.1", "serde", "sha3", + "tiny-keccak", ] [[package]] name = "alloy-rlp" -version = "0.3.13" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93e50f64a77ad9c5470bf2ad0ca02f228da70c792a8f06634801e202579f35e" +checksum = "5f70d83b765fdc080dbcd4f4db70d8d23fe4761f2f02ebfa9146b833900634b4" dependencies = [ "arrayvec 0.7.6", - "bytes 1.11.1", + "bytes 1.10.1", ] [[package]] name = "alloy-sol-macro" -version = "1.5.4" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5fa1ca7e617c634d2bd9fa71f9ec8e47c07106e248b9fcbd3eaddc13cabd625" +checksum = "d20d867dcf42019d4779519a1ceb55eba8d7f3d0e4f0a89bcba82b8f9eb01e48" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] name = "alloy-sol-macro-expander" -version = "1.5.4" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c00c0c3a75150a9dc7c8c679ca21853a137888b4e1c5569f92d7e2b15b5102" +checksum = "b74e91b0b553c115d14bd0ed41898309356dc85d0e3d4b9014c4e7715e48c8ad" dependencies = [ "alloy-sol-macro-input", "const-hex", "heck 0.5.0", - "indexmap 2.13.0", + "indexmap 2.11.4", "proc-macro-error2", "proc-macro2", "quote", - "sha3", - "syn 2.0.114", + "syn 2.0.106", "syn-solidity", + "tiny-keccak", ] [[package]] name = "alloy-sol-macro-input" -version = "1.5.4" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297db260eb4d67c105f68d6ba11b8874eec681caec5505eab8fbebee97f790bc" +checksum = "84194d31220803f5f62d0a00f583fd3a062b36382e2bea446f1af96727754565" dependencies = [ "const-hex", "dunce", @@ -221,15 +222,15 @@ dependencies = [ "macro-string", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", "syn-solidity", ] [[package]] name = "alloy-sol-type-parser" -version = "1.5.4" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b91b13181d3bcd23680fd29d7bc861d1f33fbe90fdd0af67162434aeba902d" +checksum = "fe8c27b3cf6b2bb8361904732f955bc7c05e00be5f469cec7e2280b6167f3ff0" dependencies = [ "serde", "winnow", @@ -237,9 +238,9 @@ dependencies = [ [[package]] name = "alloy-sol-types" -version = "1.5.4" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc442cc2a75207b708d481314098a0f8b6f7b58e3148dd8d8cc7407b0d6f9385" +checksum = "f5383d34ea00079e6dd89c652bcbdb764db160cef84e6250926961a0b2295d04" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -264,9 +265,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.21" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", @@ -279,9 +280,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" @@ -294,29 +295,29 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.5" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.11" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] name = "anyhow" -version = "1.0.101" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "approx" @@ -338,16 +339,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.114", -] - -[[package]] -name = "ar_archive_writer" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" -dependencies = [ - "object 0.37.3", + "syn 2.0.106", ] [[package]] @@ -529,7 +521,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" dependencies = [ "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -567,7 +559,7 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -652,7 +644,7 @@ checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -701,9 +693,9 @@ dependencies = [ [[package]] name = "ark-vrf" -version = "0.1.1" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d63e9780640021b74d02b32895d8cec1b4abe8e5547b560a6bda6b14b78c6da" +checksum = "9501da18569b2afe0eb934fb7afd5a247d238b94116155af4dd068f319adfe6d" dependencies = [ "ark-bls12-381 0.5.0", "ark-ec 0.5.0", @@ -731,7 +723,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27d55334c98d756b32dcceb60248647ab34f027690f87f9a362fd292676ee927" dependencies = [ "smallvec", - "thiserror 2.0.18", + "thiserror 2.0.16", ] [[package]] @@ -786,7 +778,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror 2.0.18", + "thiserror 2.0.16", "time", ] @@ -798,7 +790,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", "synstructure 0.13.2", ] @@ -810,7 +802,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", "synstructure 0.13.2", ] @@ -822,7 +814,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -892,16 +884,16 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 1.1.3", + "rustix 1.1.2", "slab", - "windows-sys 0.61.2", + "windows-sys 0.61.0", ] [[package]] name = "async-lock" -version = "3.4.2" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" dependencies = [ "event-listener 5.4.1", "event-listener-strategy", @@ -934,7 +926,7 @@ dependencies = [ "cfg-if", "event-listener 5.4.1", "futures-lite", - "rustix 1.1.3", + "rustix 1.1.2", ] [[package]] @@ -949,10 +941,10 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 1.1.3", + "rustix 1.1.2", "signal-hook-registry", "slab", - "windows-sys 0.61.2", + "windows-sys 0.61.0", ] [[package]] @@ -969,7 +961,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -978,7 +970,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4057f2c32adbb2fc158e22fb38433c8e9bbf76b75a4732c7c0cbaf695fb65568" dependencies = [ - "bytes 1.11.1", + "bytes 1.10.1", "futures-sink", "futures-util", "memchr", @@ -991,7 +983,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a860072022177f903e59730004fb5dc13db9275b79bb2aef7ba8ce831956c233" dependencies = [ - "bytes 1.11.1", + "bytes 1.10.1", "futures-sink", "futures-util", "memchr", @@ -1029,7 +1021,7 @@ checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -1040,17 +1032,17 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backtrace" -version = "0.3.76" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ - "addr2line 0.25.1", + "addr2line 0.24.2", "cfg-if", "libc", "miniz_oxide", - "object 0.37.3", + "object 0.36.7", "rustc-demangle", - "windows-link", + "windows-targets 0.52.6", ] [[package]] @@ -1065,16 +1057,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" -[[package]] -name = "base256emoji" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c" -dependencies = [ - "const-str", - "match-lookup", -] - [[package]] name = "base58" version = "0.2.0" @@ -1095,15 +1077,15 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.3" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "binary-merkle-tree" -version = "16.1.0" +version = "16.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95c9f6900c9fd344d53fbdfb36e1343429079d73f4168c8ef48884bf15616dbd" +checksum = "181f5380e435b8ba6d901f8b16fc8908c6f0f8bea8973113d1c8718d89bb1809" dependencies = [ "hash-db", "log", @@ -1137,7 +1119,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -1159,11 +1141,11 @@ dependencies = [ [[package]] name = "bip39" -version = "2.2.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +checksum = "43d193de1f7487df1914d3a568b772458861d33f9c54249612cc2893d6915054" dependencies = [ - "bitcoin_hashes 0.14.1", + "bitcoin_hashes 0.13.0", "serde", "unicode-normalization", ] @@ -1191,9 +1173,9 @@ checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" [[package]] name = "bitcoin-io" -version = "0.1.4" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" +checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" [[package]] name = "bitcoin_hashes" @@ -1207,12 +1189,12 @@ dependencies = [ [[package]] name = "bitcoin_hashes" -version = "0.14.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" +checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" dependencies = [ "bitcoin-io", - "hex-conservative 0.2.2", + "hex-conservative 0.2.1", ] [[package]] @@ -1223,9 +1205,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "bitvec" @@ -1273,38 +1255,37 @@ dependencies = [ [[package]] name = "blake2b_simd" -version = "1.0.4" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b79834656f71332577234b50bfc009996f7449e0c056884e6a02492ded0ca2f3" +checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99" dependencies = [ "arrayref", "arrayvec 0.7.6", - "constant_time_eq 0.4.2", + "constant_time_eq 0.3.1", ] [[package]] name = "blake2s_simd" -version = "1.0.4" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee29928bad1e3f94c9d1528da29e07a1d3d04817ae8332de1e8b846c8439f4b3" +checksum = "e90f7deecfac93095eb874a40febd69427776e24e1bd7f87f33ac62d6f0174df" dependencies = [ "arrayref", "arrayvec 0.7.6", - "constant_time_eq 0.4.2", + "constant_time_eq 0.3.1", ] [[package]] name = "blake3" -version = "1.8.3" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" dependencies = [ "arrayref", "arrayvec 0.7.6", "cc", "cfg-if", - "constant_time_eq 0.4.2", - "cpufeatures", + "constant_time_eq 0.3.1", ] [[package]] @@ -1380,9 +1361,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "byte-slice-cast" @@ -1398,9 +1379,9 @@ checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" [[package]] name = "bytemuck" -version = "1.25.0" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" [[package]] name = "byteorder" @@ -1416,9 +1397,9 @@ checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" [[package]] name = "bytes" -version = "1.11.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" dependencies = [ "serde", ] @@ -1445,9 +1426,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.2" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +checksum = "e1de8bc0aa9e9385ceb3bf0c152e3a9b9544f6c4a912c8ae504e80c1f0368603" dependencies = [ "serde_core", ] @@ -1483,9 +1464,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.55" +version = "1.2.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9" dependencies = [ "find-msvc-tools", "jobserver", @@ -1519,9 +1500,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.4" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "cfg_aliases" @@ -1571,16 +1552,16 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.2.0", ] [[package]] @@ -1668,9 +1649,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.57" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" dependencies = [ "clap_builder", "clap_derive", @@ -1678,9 +1659,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.57" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" +checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" dependencies = [ "anstream", "anstyle", @@ -1691,21 +1672,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] name = "clap_lex" -version = "0.7.7" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "clatter" @@ -1730,9 +1711,9 @@ dependencies = [ [[package]] name = "coarsetime" -version = "0.1.37" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e58eb270476aa4fc7843849f8a35063e8743b4dbcdf6dd0f8ea0886980c204c2" +checksum = "91849686042de1b41cd81490edc83afbcb0abe5a9b6f2c4114f23ce8cca1bcf4" dependencies = [ "libc", "wasix", @@ -1741,9 +1722,9 @@ dependencies = [ [[package]] name = "codespan-reporting" -version = "0.13.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" dependencies = [ "serde", "termcolor", @@ -1762,15 +1743,15 @@ version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ - "bytes 1.11.1", + "bytes 1.10.1", "memchr", ] [[package]] name = "comfy-table" -version = "7.2.2" +version = "7.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47" +checksum = "b03b7db8e0b4b2fdad6c551e634134e99ec000e5c8c3b6856c65e8bbaded7a3b" dependencies = [ "unicode-segmentation", "unicode-width", @@ -1806,9 +1787,9 @@ dependencies = [ [[package]] name = "const-hex" -version = "1.17.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bb320cac8a0750d7f25280aa97b09c26edfe161164238ecbbb31092b079e735" +checksum = "b6407bff74dea37e0fa3dc1c1c974e5d46405f0c987bf9997a0762adce71eda6" dependencies = [ "cfg-if", "cpufeatures", @@ -1837,22 +1818,16 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom 0.2.17", + "getrandom 0.2.16", "once_cell", "tiny-keccak", ] -[[package]] -name = "const-str" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" - [[package]] name = "const_format" -version = "0.2.35" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" dependencies = [ "const_format_proc_macros", ] @@ -1876,9 +1851,9 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" [[package]] name = "constant_time_eq" -version = "0.4.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" [[package]] name = "convert_case" @@ -1886,15 +1861,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -2079,7 +2045,7 @@ dependencies = [ "serde_derive", "serde_json", "tinytemplate", - "tokio 1.49.0", + "tokio 1.47.1", "walkdir", ] @@ -2162,9 +2128,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.7" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array 0.14.7", "rand_core 0.6.4", @@ -2239,12 +2205,11 @@ dependencies = [ [[package]] name = "cumulus-pallet-parachain-system" -version = "0.21.2" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3730b03ec9f86c53e4934f41bc19a3231a20e884ab6a0b3eb7ed93714ae2fa" +checksum = "6fa7c354e70d62b5bb6014cbad499b831e27629b8a2af2f86497cae7f74d309d" dependencies = [ - "array-bytes 6.2.3", - "bytes 1.11.1", + "bytes 1.10.1", "cumulus-pallet-parachain-system-proc-macro", "cumulus-primitives-core", "cumulus-primitives-parachain-inherent", @@ -2273,7 +2238,7 @@ dependencies = [ "sp-version", "staging-xcm", "staging-xcm-builder", - "trie-db 0.30.0", + "trie-db", ] [[package]] @@ -2285,7 +2250,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -2391,14 +2356,14 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] name = "cxx" -version = "1.0.194" +version = "1.0.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747d8437319e3a2f43d93b341c137927ca70c0f5dabeea7a005a73665e247c7e" +checksum = "2f81de88da10862f22b5b3a60f18f6f42bbe7cb8faa24845dd7b1e4e22190e77" dependencies = [ "cc", "cxx-build", @@ -2411,49 +2376,50 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.194" +version = "1.0.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0f4697d190a142477b16aef7da8a99bfdc41e7e8b1687583c0d23a79c7afc1e" +checksum = "5edd58bf75c3fdfc80d79806403af626570662f7b6cc782a7fabe156166bd6d6" dependencies = [ "cc", "codespan-reporting", - "indexmap 2.13.0", + "indexmap 2.11.4", "proc-macro2", "quote", "scratch", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] name = "cxxbridge-cmd" -version = "1.0.194" +version = "1.0.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0956799fa8678d4c50eed028f2de1c0552ae183c76e976cf7ca8c4e36a7c328" +checksum = "fd46bf2b541a4e0c2d5abba76607379ee05d68e714868e3cb406dc8d591ce2d2" dependencies = [ "clap", "codespan-reporting", - "indexmap 2.13.0", + "indexmap 2.11.4", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] name = "cxxbridge-flags" -version = "1.0.194" +version = "1.0.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23384a836ab4f0ad98ace7e3955ad2de39de42378ab487dc28d3990392cb283a" +checksum = "2c79b68f6a3a8f809d39b38ae8af61305a6113819b19b262643b9c21353b92d9" [[package]] name = "cxxbridge-macro" -version = "1.0.194" +version = "1.0.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6acc6b5822b9526adfb4fc377b67128fdd60aac757cc4a741a6278603f763cf" +checksum = "862b7fdb048ff9ef0779a0d0a03affd09746c4c875543746b640756be9cff2af" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.11.4", "proc-macro2", "quote", - "syn 2.0.114", + "rustversion", + "syn 2.0.106", ] [[package]] @@ -2487,7 +2453,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -2501,7 +2467,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -2512,7 +2478,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -2523,7 +2489,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -2536,20 +2502,20 @@ dependencies = [ "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core 0.9.12", + "parking_lot_core 0.9.11", ] [[package]] name = "data-encoding" -version = "2.10.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "data-encoding-macro" -version = "0.1.19" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8142a83c17aa9461d637e649271eae18bf2edd00e91f2e105df36c3c16355bdb" +checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" dependencies = [ "data-encoding", "data-encoding-macro-internal", @@ -2557,12 +2523,12 @@ dependencies = [ [[package]] name = "data-encoding-macro-internal" -version = "0.1.17" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" +checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" dependencies = [ "data-encoding", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -2606,9 +2572,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" dependencies = [ "powerfmt", ] @@ -2632,7 +2598,7 @@ checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -2643,7 +2609,7 @@ checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -2654,7 +2620,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -2663,11 +2629,11 @@ version = "0.99.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ - "convert_case 0.4.0", + "convert_case", "proc-macro2", "quote", "rustc_version 0.4.1", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -2681,11 +2647,11 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.1.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" dependencies = [ - "derive_more-impl 2.1.1", + "derive_more-impl 2.0.1", ] [[package]] @@ -2696,20 +2662,18 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] name = "derive_more-impl" -version = "2.1.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ - "convert_case 0.10.0", "proc-macro2", "quote", - "rustc_version 0.4.1", - "syn 2.0.114", + "syn 2.0.106", "unicode-xid", ] @@ -2808,7 +2772,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -2832,7 +2796,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.114", + "syn 2.0.106", "termcolor", "toml 0.8.23", "walkdir", @@ -2852,9 +2816,9 @@ checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" [[package]] name = "dtoa" -version = "1.0.11" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" +checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" [[package]] name = "dunce" @@ -2880,7 +2844,7 @@ checksum = "7e8671d54058979a37a26f3511fbf8d198ba1aa35ffb202c42587d918d77213a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -2954,7 +2918,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -2998,34 +2962,34 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] name = "enum-ordinalize" -version = "4.3.2" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" dependencies = [ "enum-ordinalize-derive", ] [[package]] name = "enum-ordinalize-derive" -version = "4.3.2" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] name = "env_filter" -version = "0.1.4" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", "regex", @@ -3076,7 +3040,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.61.0", ] [[package]] @@ -3167,7 +3131,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -3196,7 +3160,7 @@ checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" dependencies = [ "arrayvec 0.7.6", "auto_impl", - "bytes 1.11.1", + "bytes 1.10.1", ] [[package]] @@ -3207,7 +3171,7 @@ checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" dependencies = [ "arrayvec 0.7.6", "auto_impl", - "bytes 1.11.1", + "bytes 1.10.1", ] [[package]] @@ -3227,11 +3191,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb42427514b063d97ce21d5199f36c0c307d981434a6be32582bc79fe5bd2303" dependencies = [ "expander", - "indexmap 2.13.0", + "indexmap 2.11.4", "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -3282,13 +3246,14 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.27" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" dependencies = [ "cfg-if", "libc", "libredox", + "windows-sys 0.60.2", ] [[package]] @@ -3303,15 +3268,15 @@ dependencies = [ "log", "num-traits", "parity-scale-codec", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "scale-info", ] [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" [[package]] name = "fixed-hash" @@ -3340,12 +3305,6 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - [[package]] name = "fnv" version = "1.0.7" @@ -3495,7 +3454,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7cb8796f93fa038f979a014234d632e9688a120e745f936e2635123c77537f7" dependencies = [ - "frame-metadata 21.0.0", + "frame-metadata 20.0.0", "parity-scale-codec", "scale-decode", "scale-info", @@ -3512,7 +3471,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -3535,9 +3494,9 @@ dependencies = [ [[package]] name = "frame-executive" -version = "41.0.2" +version = "41.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11e495475817addd877fa08bc17fc92d7308f4d5e99be5488433ccf19d222122" +checksum = "d7e5477db02bf54b4413611f55ec59673fa7b071eef08b7097e739f999a215cd" dependencies = [ "aquamarine", "frame-support", @@ -3566,20 +3525,9 @@ dependencies = [ [[package]] name = "frame-metadata" -version = "21.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20dfd1d7eae1d94e32e869e2fb272d81f52dd8db57820a373adb83ea24d7d862" -dependencies = [ - "cfg-if", - "parity-scale-codec", - "scale-info", -] - -[[package]] -name = "frame-metadata" -version = "23.0.1" +version = "23.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba5be0edbdb824843a0f9c6f0906ecfc66c5316218d74457003218b24909ed0" +checksum = "d8c26fcb0454397c522c05fdad5380c4e622f8a875638af33bff5a320d1fc965" dependencies = [ "cfg-if", "parity-scale-codec", @@ -3621,9 +3569,9 @@ dependencies = [ [[package]] name = "frame-support" -version = "41.0.1" +version = "41.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9162e30a146eabaa72feaf67a79b32d4adbebfc0566b5544937e44786734ac7b" +checksum = "00861648586bca196780b311ca1f345752ee5d971fa1a027f3213955bc493434" dependencies = [ "aquamarine", "array-bytes 6.2.3", @@ -3631,7 +3579,7 @@ dependencies = [ "bitflags 1.3.2", "docify", "environmental", - "frame-metadata 23.0.1", + "frame-metadata 23.0.0", "frame-support-procedural", "impl-trait-for-tuples", "k256", @@ -3679,7 +3627,7 @@ dependencies = [ "proc-macro2", "quote", "sp-crypto-hashing", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -3692,7 +3640,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -3703,14 +3651,14 @@ checksum = "ed971c6435503a099bdac99fe4c5bea08981709e5b5a0a8535a1856f48561191" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] name = "frame-system" -version = "41.0.2" +version = "41.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4189198074d7964bd6cb9d1cf69d77d5d224026bce95bb0ca7efc7768bb8e29c" +checksum = "1ce7df989cefbaab681101774748a1bbbcd23d0cc66e392f8f22d3d3159914db" dependencies = [ "cfg-if", "docify", @@ -3875,7 +3823,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -3885,7 +3833,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" dependencies = [ "futures-io", - "rustls 0.23.36", + "rustls 0.23.32", "rustls-pki-types", ] @@ -3935,6 +3883,20 @@ dependencies = [ "byteorder", ] +[[package]] +name = "generator" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "605183a538e3e2a9c1038635cc5c2d194e2ee8fd0d1b66b8349fad7dbacce5a2" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows 0.61.3", +] + [[package]] name = "generic-array" version = "0.12.4" @@ -3967,28 +3929,28 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.17" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.4" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", - "wasip2", + "wasi 0.14.7+wasi-0.2.4", "wasm-bindgen", ] @@ -4033,12 +3995,6 @@ dependencies = [ "stable_deref_trait", ] -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - [[package]] name = "glob" version = "0.3.3" @@ -4057,7 +4013,7 @@ dependencies = [ "futures-timer", "no-std-compat", "nonzero_ext", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "portable-atomic", "quanta", "rand 0.8.5", @@ -4082,47 +4038,46 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" dependencies = [ - "bytes 1.11.1", + "bytes 1.10.1", "fnv", "futures-core", "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.13.0", + "indexmap 2.11.4", "slab", - "tokio 1.49.0", + "tokio 1.47.1", "tokio-util", "tracing", ] [[package]] name = "h2" -version = "0.4.13" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", - "bytes 1.11.1", + "bytes 1.10.1", "fnv", "futures-core", "futures-sink", - "http 1.4.0", - "indexmap 2.13.0", + "http 1.3.1", + "indexmap 2.11.4", "slab", - "tokio 1.49.0", + "tokio 1.47.1", "tokio-util", "tracing", ] [[package]] name = "half" -version = "2.7.1" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" dependencies = [ "cfg-if", "crunchy", - "zerocopy", ] [[package]] @@ -4190,17 +4145,7 @@ dependencies = [ "allocator-api2", "equivalent", "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "foldhash 0.2.0", "serde", - "serde_core", ] [[package]] @@ -4250,9 +4195,9 @@ checksum = "212ab92002354b4819390025006c897e8140934349e8635c9b077f47b4dcbd20" [[package]] name = "hex-conservative" -version = "0.2.2" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" dependencies = [ "arrayvec 0.7.6", ] @@ -4283,7 +4228,7 @@ dependencies = [ "socket2 0.5.10", "thiserror 1.0.69", "tinyvec", - "tokio 1.49.0", + "tokio 1.47.1", "tracing", "url", ] @@ -4306,9 +4251,9 @@ dependencies = [ "once_cell", "rand 0.9.2", "ring 0.17.14", - "thiserror 2.0.18", + "thiserror 2.0.16", "tinyvec", - "tokio 1.49.0", + "tokio 1.47.1", "tracing", "url", ] @@ -4325,12 +4270,12 @@ dependencies = [ "ipconfig", "lru-cache", "once_cell", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "rand 0.8.5", "resolv-conf", "smallvec", "thiserror 1.0.69", - "tokio 1.49.0", + "tokio 1.47.1", "tracing", ] @@ -4346,12 +4291,12 @@ dependencies = [ "ipconfig", "moka", "once_cell", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "rand 0.9.2", "resolv-conf", "smallvec", - "thiserror 2.0.18", - "tokio 1.49.0", + "thiserror 2.0.16", + "tokio 1.47.1", "tracing", ] @@ -4400,18 +4345,19 @@ version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ - "bytes 1.11.1", + "bytes 1.10.1", "fnv", "itoa", ] [[package]] name = "http" -version = "1.4.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ - "bytes 1.11.1", + "bytes 1.10.1", + "fnv", "itoa", ] @@ -4421,7 +4367,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ - "bytes 1.11.1", + "bytes 1.10.1", "http 0.2.12", "pin-project-lite 0.2.16", ] @@ -4432,8 +4378,8 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ - "bytes 1.11.1", - "http 1.4.0", + "bytes 1.10.1", + "http 1.3.1", ] [[package]] @@ -4442,9 +4388,9 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ - "bytes 1.11.1", + "bytes 1.10.1", "futures-core", - "http 1.4.0", + "http 1.3.1", "http-body 1.0.1", "pin-project-lite 0.2.16", ] @@ -4492,7 +4438,7 @@ version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ - "bytes 1.11.1", + "bytes 1.10.1", "futures-channel", "futures-core", "futures-util", @@ -4504,7 +4450,7 @@ dependencies = [ "itoa", "pin-project-lite 0.2.16", "socket2 0.5.10", - "tokio 1.49.0", + "tokio 1.47.1", "tower-service", "tracing", "want", @@ -4512,16 +4458,16 @@ dependencies = [ [[package]] name = "hyper" -version = "1.8.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ "atomic-waker", - "bytes 1.11.1", + "bytes 1.10.1", "futures-channel", "futures-core", - "h2 0.4.13", - "http 1.4.0", + "h2 0.4.12", + "http 1.3.1", "http-body 1.0.1", "httparse", "httpdate", @@ -4529,7 +4475,7 @@ dependencies = [ "pin-project-lite 0.2.16", "pin-utils", "smallvec", - "tokio 1.49.0", + "tokio 1.47.1", "want", ] @@ -4539,43 +4485,44 @@ version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http 1.4.0", - "hyper 1.8.1", + "http 1.3.1", + "hyper 1.7.0", "hyper-util", "log", - "rustls 0.23.36", - "rustls-native-certs 0.8.3", + "rustls 0.23.32", + "rustls-native-certs 0.8.1", "rustls-pki-types", - "tokio 1.49.0", + "tokio 1.47.1", "tokio-rustls", "tower-service", ] [[package]] name = "hyper-util" -version = "0.1.20" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ - "bytes 1.11.1", + "bytes 1.10.1", "futures-channel", + "futures-core", "futures-util", - "http 1.4.0", + "http 1.3.1", "http-body 1.0.1", - "hyper 1.8.1", + "hyper 1.7.0", "libc", "pin-project-lite 0.2.16", - "socket2 0.6.2", - "tokio 1.49.0", + "socket2 0.6.0", + "tokio 1.47.1", "tower-service", "tracing", ] [[package]] name = "iana-time-zone" -version = "0.1.65" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -4583,7 +4530,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core 0.62.0", ] [[package]] @@ -4597,9 +4544,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", "potential_utf", @@ -4610,9 +4557,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", @@ -4623,10 +4570,11 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ + "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -4637,38 +4585,42 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ + "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", + "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", "icu_locale_core", + "stable_deref_trait", + "tinystr", "writeable", "yoke", "zerofrom", @@ -4732,7 +4684,7 @@ dependencies = [ "netlink-sys", "rtnetlink", "system-configuration", - "tokio 1.49.0", + "tokio 1.47.1", "windows 0.53.0", ] @@ -4744,13 +4696,13 @@ checksum = "064d90fec10d541084e7b39ead8875a5a80d9114a2b18791565253bae25f49e4" dependencies = [ "async-trait", "attohttpc", - "bytes 1.11.1", + "bytes 1.10.1", "futures 0.3.31", "http 0.2.12", "hyper 0.14.32", "log", "rand 0.8.5", - "tokio 1.49.0", + "tokio 1.47.1", "url", "xmltree", ] @@ -4810,7 +4762,7 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -4845,12 +4797,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.15.5", "serde", "serde_core", ] @@ -4899,6 +4851,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags 2.9.4", + "cfg-if", + "libc", +] + [[package]] name = "ip_network" version = "0.4.1" @@ -4925,20 +4888,20 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "is-terminal" -version = "0.4.17" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi 0.5.2", "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.2" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" @@ -4987,32 +4950,32 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.19" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89a5b5e10d5a9ad6e5d1f4bd58225f655d6fe9767575a5e8ac5a6fe64e04495" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", - "serde_core", + "serde", ] [[package]] name = "jiff-static" -version = "0.2.19" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff7a39c8862fc1369215ccf0a8f12dd4598c7f6484704359f0351bd617034dbf" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -5043,15 +5006,15 @@ version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.3.3", "libc", ] [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" dependencies = [ "once_cell", "wasm-bindgen", @@ -5059,9 +5022,9 @@ dependencies = [ [[package]] name = "jsonrpsee" -version = "0.24.10" +version = "0.24.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e281ae70cc3b98dac15fced3366a880949e65fc66e345ce857a5682d152f3e62" +checksum = "37b26c20e2178756451cfeb0661fb74c47dd5988cb7e3939de7e9241fd604d42" dependencies = [ "jsonrpsee-client-transport", "jsonrpsee-core", @@ -5069,27 +5032,27 @@ dependencies = [ "jsonrpsee-server", "jsonrpsee-types", "jsonrpsee-ws-client", - "tokio 1.49.0", + "tokio 1.47.1", "tracing", ] [[package]] name = "jsonrpsee-client-transport" -version = "0.24.10" +version = "0.24.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4280b709ac3bb5e16cf3bad5056a0ec8df55fa89edfe996361219aadc2c7ea" +checksum = "bacb85abf4117092455e1573625e21b8f8ef4dec8aff13361140b2dc266cdff2" dependencies = [ "base64 0.22.1", "futures-util", - "http 1.4.0", + "http 1.3.1", "jsonrpsee-core", "pin-project", - "rustls 0.23.36", + "rustls 0.23.32", "rustls-pki-types", "rustls-platform-verifier", "soketto", "thiserror 1.0.69", - "tokio 1.49.0", + "tokio 1.47.1", "tokio-rustls", "tokio-util", "tracing", @@ -5098,54 +5061,54 @@ dependencies = [ [[package]] name = "jsonrpsee-core" -version = "0.24.10" +version = "0.24.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "348ee569eaed52926b5e740aae20863762b16596476e943c9e415a6479021622" +checksum = "456196007ca3a14db478346f58c7238028d55ee15c1df15115596e411ff27925" dependencies = [ "async-trait", - "bytes 1.11.1", + "bytes 1.10.1", "futures-timer", "futures-util", - "http 1.4.0", + "http 1.3.1", "http-body 1.0.1", "http-body-util", "jsonrpsee-types", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "pin-project", "rand 0.8.5", "rustc-hash 2.1.1", "serde", "serde_json", "thiserror 1.0.69", - "tokio 1.49.0", + "tokio 1.47.1", "tokio-stream", "tracing", ] [[package]] name = "jsonrpsee-proc-macros" -version = "0.24.10" +version = "0.24.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7398cddf5013cca4702862a2692b66c48a3bd6cf6ec681a47453c93d63cf8de5" +checksum = "5e65763c942dfc9358146571911b0cd1c361c2d63e2d2305622d40d36376ca80" dependencies = [ "heck 0.5.0", "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] name = "jsonrpsee-server" -version = "0.24.10" +version = "0.24.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21429bcdda37dcf2d43b68621b994adede0e28061f816b038b0f18c70c143d51" +checksum = "55e363146da18e50ad2b51a0a7925fc423137a0b1371af8235b1c231a0647328" dependencies = [ "futures-util", - "http 1.4.0", + "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.8.1", + "hyper 1.7.0", "hyper-util", "jsonrpsee-core", "jsonrpsee-types", @@ -5155,7 +5118,7 @@ dependencies = [ "serde_json", "soketto", "thiserror 1.0.69", - "tokio 1.49.0", + "tokio 1.47.1", "tokio-stream", "tokio-util", "tower", @@ -5164,11 +5127,11 @@ dependencies = [ [[package]] name = "jsonrpsee-types" -version = "0.24.10" +version = "0.24.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0f05e0028e55b15dbd2107163b3c744cd3bb4474f193f95d9708acbf5677e44" +checksum = "08a8e70baf945b6b5752fc8eb38c918a48f1234daf11355e07106d963f860089" dependencies = [ - "http 1.4.0", + "http 1.3.1", "serde", "serde_json", "thiserror 1.0.69", @@ -5176,11 +5139,11 @@ dependencies = [ [[package]] name = "jsonrpsee-ws-client" -version = "0.24.10" +version = "0.24.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78fc744f17e7926d57f478cf9ca6e1ee5d8332bf0514860b1a3cdf1742e614cc" +checksum = "01b3323d890aa384f12148e8d2a1fd18eb66e9e7e825f9de4fa53bcc19b93eef" dependencies = [ - "http 1.4.0", + "http 1.3.1", "jsonrpsee-client-transport", "jsonrpsee-core", "jsonrpsee-types", @@ -5213,9 +5176,9 @@ dependencies = [ [[package]] name = "keccak-asm" -version = "0.1.5" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b646a74e746cd25045aa0fd42f4f7f78aa6d119380182c7e63a5593c4ab8df6f" +checksum = "505d1856a39b200489082f90d897c3f07c455563880bc5952e38eabf731c83b6" dependencies = [ "digest 0.10.7", "sha3-asm", @@ -5284,7 +5247,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf7a85fe66f9ff9cd74e169fdd2c94c6e1e74c412c99a73b4df3200b5d3760b2" dependencies = [ "kvdb", - "parking_lot 0.12.5", + "parking_lot 0.12.4", ] [[package]] @@ -5295,7 +5258,7 @@ checksum = "b644c70b92285f66bfc2032922a79000ea30af7bc2ab31902992a5dcb9b434f6" dependencies = [ "kvdb", "num_cpus", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "regex", "rocksdb", "smallvec", @@ -5318,9 +5281,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.180" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libloading" @@ -5329,14 +5292,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-link", + "windows-link 0.2.0", ] [[package]] name = "libm" -version = "0.2.16" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libp2p" @@ -5344,11 +5307,11 @@ version = "0.54.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbe80f9c7e00526cd6b838075b9c171919404a4732cb2fa8ece0a093223bfc4" dependencies = [ - "bytes 1.11.1", + "bytes 1.10.1", "either", "futures 0.3.31", "futures-timer", - "getrandom 0.2.17", + "getrandom 0.2.16", "libp2p-allow-block-list", "libp2p-connection-limits", "libp2p-core", @@ -5412,7 +5375,7 @@ dependencies = [ "multihash 0.19.3", "multistream-select", "once_cell", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "pin-project", "quick-protobuf", "rand 0.8.5", @@ -5436,7 +5399,7 @@ dependencies = [ "hickory-resolver 0.24.4", "libp2p-core", "libp2p-identity", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "smallvec", "tracing", ] @@ -5490,7 +5453,7 @@ checksum = "ced237d0bd84bbebb7c2cad4c073160dacb4fe40534963c32ed6d4c6bb7702a3" dependencies = [ "arrayvec 0.7.6", "asynchronous-codec 0.7.0", - "bytes 1.11.1", + "bytes 1.10.1", "either", "fnv", "futures 0.3.31", @@ -5527,7 +5490,7 @@ dependencies = [ "rand 0.8.5", "smallvec", "socket2 0.5.10", - "tokio 1.49.0", + "tokio 1.47.1", "tracing", "void", ] @@ -5556,7 +5519,7 @@ version = "0.45.10" source = "git+https://github.com/Quantus-Network/qp-libp2p-noise?tag=v0.45.10#901f09f30b32f910395270bba3a566191dc2f61f" dependencies = [ "asynchronous-codec 0.6.2", - "bytes 1.11.1", + "bytes 1.10.1", "clatter", "futures 0.3.31", "libp2p-core", @@ -5597,21 +5560,21 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46352ac5cd040c70e88e7ff8257a2ae2f891a4076abad2c439584a31c15fd24e" dependencies = [ - "bytes 1.11.1", + "bytes 1.10.1", "futures 0.3.31", "futures-timer", "if-watch", "libp2p-core", "libp2p-identity", "libp2p-tls", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "quinn 0.11.9", "rand 0.8.5", "ring 0.17.14", - "rustls 0.23.36", + "rustls 0.23.32", "socket2 0.5.10", "thiserror 1.0.69", - "tokio 1.49.0", + "tokio 1.47.1", "tracing", ] @@ -5653,7 +5616,7 @@ dependencies = [ "once_cell", "rand 0.8.5", "smallvec", - "tokio 1.49.0", + "tokio 1.47.1", "tracing", "void", "web-time", @@ -5668,7 +5631,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -5684,7 +5647,7 @@ dependencies = [ "libp2p-core", "libp2p-identity", "socket2 0.5.10", - "tokio 1.49.0", + "tokio 1.47.1", "tracing", ] @@ -5700,7 +5663,7 @@ dependencies = [ "libp2p-identity", "rcgen", "ring 0.17.14", - "rustls 0.23.36", + "rustls 0.23.32", "rustls-webpki 0.101.7", "thiserror 1.0.69", "x509-parser 0.16.0", @@ -5718,7 +5681,7 @@ dependencies = [ "igd-next", "libp2p-core", "libp2p-swarm", - "tokio 1.49.0", + "tokio 1.47.1", "tracing", "void", ] @@ -5734,7 +5697,7 @@ dependencies = [ "futures-rustls", "libp2p-core", "libp2p-identity", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "pin-project-lite 0.2.16", "rw-stream-sink", "soketto", @@ -5756,18 +5719,18 @@ dependencies = [ "thiserror 1.0.69", "tracing", "yamux 0.12.1", - "yamux 0.13.8", + "yamux 0.13.6", ] [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.9.4", "libc", - "redox_syscall 0.7.0", + "redox_syscall 0.5.17", ] [[package]] @@ -5835,9 +5798,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.23" +version = "1.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" +checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" dependencies = [ "cc", "pkg-config", @@ -5861,9 +5824,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linked_hash_set" -version = "0.1.6" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "984fb35d06508d1e69fc91050cceba9c0b748f983e6739fa2c7a9237154c52c8" +checksum = "bae85b5be22d9843c80e5fc80e9b64c8a3b1f98f867c709956eca3efff4e92e2" dependencies = [ "linked-hash-map", ] @@ -5903,9 +5866,9 @@ dependencies = [ [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "litep2p" @@ -5915,19 +5878,19 @@ checksum = "14fb10e63363204b89d91e1292df83322fd9de5d7fa76c3d5c78ddc2f8f3efa9" dependencies = [ "async-trait", "bs58", - "bytes 1.11.1", + "bytes 1.10.1", "cid 0.11.1", "ed25519-dalek", "futures 0.3.31", "futures-timer", "hickory-resolver 0.25.2", - "indexmap 2.13.0", + "indexmap 2.11.4", "libc", "mockall", "multiaddr 0.17.1", "multihash 0.17.0", "network-interface", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "pin-project", "prost 0.13.5", "prost-build", @@ -5938,8 +5901,8 @@ dependencies = [ "smallvec", "snow", "socket2 0.5.10", - "thiserror 2.0.18", - "tokio 1.49.0", + "thiserror 2.0.16", + "tokio 1.47.1", "tokio-stream", "tokio-tungstenite", "tokio-util", @@ -5949,25 +5912,39 @@ dependencies = [ "url", "x25519-dalek", "x509-parser 0.17.0", - "yamux 0.13.8", + "yamux 0.13.6", "yasna", "zeroize", ] [[package]] name = "lock_api" -version = "0.4.14" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ + "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.29" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "loom" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] [[package]] name = "lru" @@ -6029,7 +6006,7 @@ checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -6041,7 +6018,7 @@ dependencies = [ "macro_magic_core", "macro_magic_macros", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -6055,7 +6032,7 @@ dependencies = [ "macro_magic_core_macros", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -6066,7 +6043,7 @@ checksum = "b02abfe41815b5bd98dbd4260173db2c116dda171dc0fe7838cb206333b83308" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -6077,27 +6054,16 @@ checksum = "73ea28ee64b88876bf45277ed9a5817c1817df061a74f2b988971a12570e5869" dependencies = [ "macro_magic_core", "quote", - "syn 2.0.114", -] - -[[package]] -name = "match-lookup" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "757aee279b8bdbb9f9e676796fd459e4207a1f986e87886700abf589f5abf771" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] name = "matchers" -version = "0.2.0" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ - "regex-automata", + "regex-automata 0.1.10", ] [[package]] @@ -6112,9 +6078,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memfd" @@ -6122,7 +6088,7 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" dependencies = [ - "rustix 1.1.3", + "rustix 1.1.2", ] [[package]] @@ -6136,9 +6102,9 @@ dependencies = [ [[package]] name = "memmap2" -version = "0.9.9" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" dependencies = [ "libc", ] @@ -6171,7 +6137,7 @@ checksum = "b3e3e3f549d27d2dc054372f320ddf68045a833fab490563ff70d4cf1b9d91ea" dependencies = [ "array-bytes 9.3.0", "blake3", - "frame-metadata 23.0.1", + "frame-metadata 23.0.0", "parity-scale-codec", "scale-decode", "scale-info", @@ -6206,13 +6172,13 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi", - "windows-sys 0.61.2", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", ] [[package]] @@ -6231,7 +6197,7 @@ dependencies = [ "hashlink", "lioness", "log", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "rand 0.8.5", "rand_chacha 0.3.1", "rand_distr", @@ -6242,9 +6208,9 @@ dependencies = [ [[package]] name = "ml-kem" -version = "0.2.2" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcaee19a45f916d98f24a551cc9a2cdae705a040e66f3cbc4f3a282ea6a2e982" +checksum = "97befee0c869cb56f3118f49d0f9bb68c9e3f380dec23c1100aedc4ec3ba239a" dependencies = [ "hybrid-array", "kem", @@ -6276,23 +6242,25 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] name = "moka" -version = "0.12.13" +version = "0.12.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e" +checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926" dependencies = [ "crossbeam-channel", "crossbeam-epoch", "crossbeam-utils", - "equivalent", - "parking_lot 0.12.5", + "loom", + "parking_lot 0.12.4", "portable-atomic", + "rustc_version 0.4.1", "smallvec", "tagptr", + "thiserror 1.0.69", "uuid", ] @@ -6342,12 +6310,11 @@ dependencies = [ [[package]] name = "multibase" -version = "0.9.2" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77" +checksum = "9b3539ec3c1f04ac9748a260728e855f261b4977f5c3406612c884564f329404" dependencies = [ "base-x", - "base256emoji", "data-encoding", "data-encoding-macro", ] @@ -6405,7 +6372,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea0df8e5eec2298a62b326ee4f0d7fe1a6b90a09dfcf9df37b38f947a8c42f19" dependencies = [ - "bytes 1.11.1", + "bytes 1.10.1", "futures 0.3.31", "log", "pin-project", @@ -6499,36 +6466,36 @@ version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60" dependencies = [ - "bytes 1.11.1", + "bytes 1.10.1", "futures 0.3.31", "log", "netlink-packet-core", "netlink-sys", - "thiserror 2.0.18", + "thiserror 2.0.16", ] [[package]] name = "netlink-sys" -version = "0.8.8" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" +checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23" dependencies = [ - "bytes 1.11.1", - "futures-util", + "bytes 1.10.1", + "futures 0.3.31", "libc", "log", - "tokio 1.49.0", + "tokio 1.47.1", ] [[package]] name = "network-interface" -version = "2.0.5" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ddcb8865ad3d9950f22f42ffa0ef0aecbfbf191867b3122413602b0a360b2a6" +checksum = "07709a6d4eba90ab10ec170a0530b3aafc81cb8a2d380e4423ae41fc55fe5745" dependencies = [ "cc", "libc", - "thiserror 2.0.18", + "thiserror 2.0.16", "winapi", ] @@ -6585,20 +6552,21 @@ checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" [[package]] name = "ntapi" -version = "0.4.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c70f219e21142367c70c0b30c6a9e3a14d55b4d12a204d897fbec83a0363f081" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" dependencies = [ "winapi", ] [[package]] name = "nu-ansi-term" -version = "0.50.3" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ - "windows-sys 0.61.2", + "overload", + "winapi", ] [[package]] @@ -6638,9 +6606,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-derive" @@ -6650,7 +6618,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -6735,15 +6703,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - [[package]] name = "oid-registry" version = "0.7.1" @@ -6774,9 +6733,9 @@ dependencies = [ [[package]] name = "once_cell_polyfill" -version = "1.70.2" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "oorandom" @@ -6802,12 +6761,6 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - [[package]] name = "option-ext" version = "0.2.0" @@ -6838,15 +6791,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43dfaf083aef571385fccfdc3a2f8ede8d0a1863160455d4f2b014d8f7d04a3f" dependencies = [ "expander", - "indexmap 2.13.0", + "indexmap 2.11.4", "itertools 0.11.0", - "petgraph 0.6.5", + "petgraph", "proc-macro-crate 3.4.0", "proc-macro2", "quote", "syn 1.0.109", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "p3-dft" version = "0.3.0" @@ -7073,17 +7032,38 @@ dependencies = [ [[package]] name = "pallet-balances" -version = "42.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a4d7dbabb4976ebd37e386ed3abba847f4599a026602439f289a8a93b8c9bd5" +version = "40.0.1" dependencies = [ "docify", "frame-benchmarking", "frame-support", "frame-system", "log", + "pallet-transaction-payment", "parity-scale-codec", - "scale-info", + "paste", + "qp-poseidon", + "qp-wormhole", + "scale-info", + "sp-core", + "sp-io", + "sp-metadata-ir", + "sp-runtime", +] + +[[package]] +name = "pallet-balances" +version = "42.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a4d7dbabb4976ebd37e386ed3abba847f4599a026602439f289a8a93b8c9bd5" +dependencies = [ + "docify", + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "parity-scale-codec", + "scale-info", "sp-core", "sp-runtime", ] @@ -7152,9 +7132,8 @@ dependencies = [ "frame-support", "frame-system", "log", - "pallet-balances", + "pallet-balances 40.0.1", "parity-scale-codec", - "qp-poseidon", "qp-wormhole", "scale-info", "sp-consensus-pow", @@ -7184,7 +7163,7 @@ dependencies = [ "frame-support", "frame-system", "log", - "pallet-balances", + "pallet-balances 40.0.1", "pallet-timestamp", "parity-scale-codec", "scale-info", @@ -7235,9 +7214,9 @@ dependencies = [ [[package]] name = "pallet-ranked-collective" -version = "41.0.2" +version = "41.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae595fd2ce7b0889680e173fe7faf743f127143faf5f2e716bae12cd8c0b354" +checksum = "bf43c766b69c37ab964cf076f605d3993357124fcdd14a8ba3ecc169e2d0fc9c" dependencies = [ "frame-benchmarking", "frame-support", @@ -7292,7 +7271,7 @@ dependencies = [ "log", "pallet-assets", "pallet-assets-holder", - "pallet-balances", + "pallet-balances 40.0.1", "pallet-preimage", "pallet-recovery", "pallet-scheduler", @@ -7309,9 +7288,9 @@ dependencies = [ [[package]] name = "pallet-revive" -version = "0.7.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1459d118aeccd7fef1dfb7e3ea002a376c8f07c61053eb2875acc38b1ceee4d9" +checksum = "474840408264f98eea7f187839ff2157f83a92bec6f3f3503dbf855c38f4de6b" dependencies = [ "alloy-core", "derive_more 0.99.20", @@ -7377,7 +7356,7 @@ checksum = "63c2dc2fc6961da23fefc54689ce81a8e006f6988bc465dcc9ab9db905d31766" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -7551,7 +7530,7 @@ dependencies = [ "frame-system", "impl-trait-for-tuples", "log", - "pallet-balances", + "pallet-balances 42.0.0", "parity-scale-codec", "scale-info", "serde", @@ -7585,27 +7564,16 @@ dependencies = [ "hex", "lazy_static", "log", - "pallet-assets", - "pallet-balances", + "pallet-balances 40.0.1", "parity-scale-codec", - "qp-dilithium-crypto", - "qp-header", - "qp-plonky2", - "qp-poseidon", - "qp-poseidon-core", "qp-wormhole", "qp-wormhole-circuit", - "qp-wormhole-circuit-builder", - "qp-wormhole-prover", - "qp-wormhole-verifier 1.0.1", + "qp-wormhole-verifier", "qp-zk-circuits-common", "scale-info", "sp-core", "sp-io", - "sp-metadata-ir", "sp-runtime", - "sp-state-machine", - "sp-trie", ] [[package]] @@ -7635,7 +7603,7 @@ dependencies = [ "log", "lz4", "memmap2 0.5.10", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "rand 0.8.5", "siphasher 0.3.11", "snap", @@ -7651,7 +7619,7 @@ dependencies = [ "arrayvec 0.7.6", "bitvec", "byte-slice-cast", - "bytes 1.11.1", + "bytes 1.10.1", "const_format", "impl-trait-for-tuples", "parity-scale-codec-derive", @@ -7668,7 +7636,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -7696,12 +7664,12 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.5" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", - "parking_lot_core 0.9.12", + "parking_lot_core 0.9.11", ] [[package]] @@ -7720,15 +7688,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.12" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.18", + "redox_syscall 0.5.17", "smallvec", - "windows-link", + "windows-targets 0.52.6", ] [[package]] @@ -7773,12 +7741,12 @@ checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "pem" -version = "3.0.6" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" dependencies = [ "base64 0.22.1", - "serde_core", + "serde", ] [[package]] @@ -7798,19 +7766,20 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.6" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" dependencies = [ "memchr", + "thiserror 2.0.16", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.8.6" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +checksum = "bc58706f770acb1dbd0973e6530a3cff4746fb721207feb3a8a6064cd0b6c663" dependencies = [ "pest", "pest_generator", @@ -7818,22 +7787,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.6" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +checksum = "6d4f36811dfe07f7b8573462465d5cb8965fffc2e71ae377a33aecf14c2c9a2f" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] name = "pest_meta" -version = "2.8.6" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +checksum = "42919b05089acbd0a5dcd5405fb304d17d1053847b81163d09c4ad18ce8e8420" dependencies = [ "pest", "sha2 0.10.9", @@ -7845,18 +7814,8 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ - "fixedbitset 0.4.2", - "indexmap 2.13.0", -] - -[[package]] -name = "petgraph" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" -dependencies = [ - "fixedbitset 0.5.7", - "indexmap 2.13.0", + "fixedbitset", + "indexmap 2.11.4", ] [[package]] @@ -7876,7 +7835,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -7927,6 +7886,8 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plonky2_maybe_rayon" version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1e554181dc95243b8d9948ae7bae5759c7fb2502fed28f671f95ef38079406" dependencies = [ "rayon", ] @@ -7934,6 +7895,8 @@ dependencies = [ [[package]] name = "plonky2_util" version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c32c137808ca984ab2458b612b7eb0462d853ee041a3136e83d54b96074c7610" [[package]] name = "plotters" @@ -8031,9 +7994,9 @@ dependencies = [ [[package]] name = "polkadot-node-primitives" -version = "20.1.0" +version = "20.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01b9c91614ec9e0502c547792dd72b00cf3c73d7290f8e611008129207124e4f" +checksum = "498dd752f08ff2a3f92fa67e5f5cbca65a342c924465ca8cc7e4b3902a6f8613" dependencies = [ "bitvec", "bounded-vec", @@ -8166,9 +8129,9 @@ dependencies = [ [[package]] name = "polkadot-runtime-parachains" -version = "20.0.3" +version = "20.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5eccffd0a38fc4cdbf9b3b8c0bc9d140fc2cb49e1f073208b88c9c9138ae039" +checksum = "7692d2a6109ef7b4749c51d5c950fa15f8e38fed439d5d520ba49fa322466c45" dependencies = [ "bitflags 1.3.2", "bitvec", @@ -8181,7 +8144,7 @@ dependencies = [ "pallet-authority-discovery", "pallet-authorship", "pallet-babe", - "pallet-balances", + "pallet-balances 42.0.0", "pallet-broker", "pallet-message-queue", "pallet-mmr", @@ -8352,7 +8315,7 @@ dependencies = [ "polkavm-common 0.21.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -8364,7 +8327,7 @@ dependencies = [ "polkavm-common 0.24.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -8374,7 +8337,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36837f6b7edfd6f4498f8d25d81da16cf03bd6992c3e56f3d477dfc90f4fefca" dependencies = [ "polkavm-derive-impl 0.21.0", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -8384,7 +8347,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba0ef0f17ad81413ea1ca5b1b67553aedf5650c88269b673d3ba015c83bc2651" dependencies = [ "polkavm-derive-impl 0.24.0", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -8441,8 +8404,8 @@ dependencies = [ "concurrent-queue", "hermit-abi 0.5.2", "pin-project-lite 0.2.16", - "rustix 1.1.3", - "windows-sys 0.61.2", + "rustix 1.1.2", + "windows-sys 0.61.0", ] [[package]] @@ -8470,24 +8433,24 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable-atomic-util" -version = "0.2.5" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" dependencies = [ "zerovec", ] @@ -8515,7 +8478,7 @@ checksum = "b4a326caf27cbf2ac291ca7fd56300497ba9e76a8cc6a7d95b7a18b57f22b61d" dependencies = [ "cc", "dunce", - "getrandom 0.3.4", + "getrandom 0.3.3", "libc", ] @@ -8581,7 +8544,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -8661,7 +8624,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.23.10+spec-1.0.0", + "toml_edit 0.23.6", ] [[package]] @@ -8707,7 +8670,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -8718,14 +8681,14 @@ checksum = "75eea531cfcd120e0851a3f8aed42c4841f78c889eefafd96339c72677ae42c3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -8740,7 +8703,7 @@ dependencies = [ "fnv", "lazy_static", "memchr", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "thiserror 1.0.69", ] @@ -8752,7 +8715,7 @@ checksum = "504ee9ff529add891127c4827eb481bd69dc0ebc72e9a682e187db4caa60c3ca" dependencies = [ "dtoa", "itoa", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "prometheus-client-derive-encode", ] @@ -8764,23 +8727,24 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] name = "proptest" -version = "1.10.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" +checksum = "2bb0be07becd10686a0bb407298fb425360a5c44a663774406340c59a22de4ce" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.10.0", + "bitflags 2.9.4", + "lazy_static", "num-traits", "rand 0.9.2", "rand_chacha 0.9.0", "rand_xorshift", - "regex-syntax", + "regex-syntax 0.8.6", "rusty-fork", "tempfile", "unarray", @@ -8792,7 +8756,7 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ - "bytes 1.11.1", + "bytes 1.10.1", "prost-derive 0.12.6", ] @@ -8802,7 +8766,7 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" dependencies = [ - "bytes 1.11.1", + "bytes 1.10.1", "prost-derive 0.13.5", ] @@ -8817,12 +8781,12 @@ dependencies = [ "log", "multimap", "once_cell", - "petgraph 0.7.1", + "petgraph", "prettyplease", "prost 0.13.5", "prost-types", "regex", - "syn 2.0.114", + "syn 2.0.106", "tempfile", ] @@ -8836,7 +8800,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -8849,7 +8813,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -8863,11 +8827,10 @@ dependencies = [ [[package]] name = "psm" -version = "0.1.29" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa96cb91275ed31d6da3e983447320c4eb219ac180fa1679a0889ff32861e2d" +checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f" dependencies = [ - "ar_archive_writer", "cc", ] @@ -8908,25 +8871,21 @@ dependencies = [ [[package]] name = "qp-plonky2" -version = "1.1.3" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "068162bd3730e381744eca219f98f0db22874afc8fa24631611e975c93d6cf68" dependencies = [ "ahash", "anyhow", - "getrandom 0.2.17", + "getrandom 0.2.16", "hashbrown 0.14.5", "itertools 0.11.0", "keccak-hash 0.8.0", "log", "num", - "p3-field", - "p3-goldilocks", - "p3-poseidon2", - "p3-symmetric", "plonky2_maybe_rayon", "plonky2_util", "qp-plonky2-field", - "qp-plonky2-verifier", - "qp-poseidon-constants", "rand 0.8.5", "rand_chacha 0.3.1", "serde", @@ -8937,7 +8896,9 @@ dependencies = [ [[package]] name = "qp-plonky2-field" -version = "1.1.3" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b2f7e0854f9e14aa9b3f6d544ccf71087e751d6adbe6a7e50d2c75fb561d97" dependencies = [ "anyhow", "itertools 0.11.0", @@ -8950,34 +8911,11 @@ dependencies = [ "unroll", ] -[[package]] -name = "qp-plonky2-verifier" -version = "2.0.0" -dependencies = [ - "ahash", - "anyhow", - "hashbrown 0.14.5", - "itertools 0.11.0", - "keccak-hash 0.8.0", - "log", - "num", - "p3-field", - "p3-goldilocks", - "p3-poseidon2", - "p3-symmetric", - "plonky2_util", - "qp-plonky2-field", - "qp-poseidon-constants", - "serde", - "static_assertions", - "unroll", -] - [[package]] name = "qp-poseidon" -version = "1.0.7" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4214ec389bff0c21c6ef815cf0ff00656586344dbe20f6441d23a1a6a7f56e84" +checksum = "0353086f7af1df7d45a1ecb995cf84b583c8211d7122f542044b37388b5effcd" dependencies = [ "log", "p3-field", @@ -8994,9 +8932,9 @@ dependencies = [ [[package]] name = "qp-poseidon-constants" -version = "1.0.2" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15005ef7ac88e170a9461990130619855284bd524e8ad059491ed291222d1553" +checksum = "4d56b56652e9f44a43de9593e75d7c3e0c3a352e10675cf3024e5b3175711cd3" dependencies = [ "p3-field", "p3-goldilocks", @@ -9007,15 +8945,14 @@ dependencies = [ [[package]] name = "qp-poseidon-core" -version = "1.0.7" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f65766d6de64eff741c7f402002a3322f5e563d53e0e9040aeab4921ff24f2b" +checksum = "e658a373a7fb22babeda9ffcc8af0a894e6e3c008272ed735509eccb7769ead3" dependencies = [ "p3-field", "p3-goldilocks", "p3-poseidon2", "p3-symmetric", - "qp-plonky2", "qp-poseidon-constants", "rand_chacha 0.9.0", ] @@ -9045,10 +8982,10 @@ dependencies = [ "qp-poseidon-core", "qp-rusty-crystals-dilithium", "rand_chacha 0.9.0", - "rand_core 0.9.5", + "rand_core 0.9.3", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.16", ] [[package]] @@ -9067,8 +9004,9 @@ version = "0.1.0" [[package]] name = "qp-wormhole-circuit" -version = "0.1.8" -source = "git+https://github.com/Quantus-Network/qp-zk-circuits?branch=illuzen%2Fagg-fees#67cf016f6f0237f61dab644d0724124311a15e1f" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea70a3a1bf545450cb6320782389cfbfe58fcf0ecf724aa666383807bf5548b" dependencies = [ "anyhow", "hex", @@ -9078,26 +9016,9 @@ dependencies = [ [[package]] name = "qp-wormhole-circuit-builder" -version = "0.1.8" -source = "git+https://github.com/Quantus-Network/qp-zk-circuits?branch=illuzen%2Fagg-fees#67cf016f6f0237f61dab644d0724124311a15e1f" -dependencies = [ - "anyhow", - "qp-plonky2", - "qp-wormhole-circuit", - "qp-zk-circuits-common", -] - -[[package]] -name = "qp-wormhole-inputs" -version = "1.0.1" -dependencies = [ - "anyhow", -] - -[[package]] -name = "qp-wormhole-prover" -version = "0.1.8" -source = "git+https://github.com/Quantus-Network/qp-zk-circuits?branch=illuzen%2Fagg-fees#67cf016f6f0237f61dab644d0724124311a15e1f" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d6486fbfd79005d520caff6a32a8fde4eab978110101c9612d140f3e0ebdfd0" dependencies = [ "anyhow", "qp-plonky2", @@ -9107,8 +9028,9 @@ dependencies = [ [[package]] name = "qp-wormhole-verifier" -version = "0.1.8" -source = "git+https://github.com/Quantus-Network/qp-zk-circuits?branch=illuzen%2Fagg-fees#67cf016f6f0237f61dab644d0724124311a15e1f" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f83ee7ce0fdb40f34eed7a31a575e857d24135a383cdfd6f2429a7ce048b665" dependencies = [ "anyhow", "qp-plonky2", @@ -9116,24 +9038,14 @@ dependencies = [ "qp-zk-circuits-common", ] -[[package]] -name = "qp-wormhole-verifier" -version = "1.0.1" -dependencies = [ - "anyhow", - "qp-plonky2-verifier", - "qp-wormhole-inputs", -] - [[package]] name = "qp-zk-circuits-common" -version = "0.1.8" -source = "git+https://github.com/Quantus-Network/qp-zk-circuits?branch=illuzen%2Fagg-fees#67cf016f6f0237f61dab644d0724124311a15e1f" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a2a62915be0513d045f4d92829c0a26d6f3e8897353e8f8356a7eabef025329" dependencies = [ "anyhow", - "hex", "qp-plonky2", - "qp-poseidon-core", "serde", ] @@ -9157,7 +9069,7 @@ dependencies = [ "libc", "once_cell", "raw-cpuid", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "web-sys", "winapi", ] @@ -9168,7 +9080,7 @@ version = "0.0.3" dependencies = [ "serde", "serde_json", - "tokio 1.49.0", + "tokio 1.47.1", ] [[package]] @@ -9188,10 +9100,9 @@ dependencies = [ "parity-scale-codec", "prometheus", "qp-dilithium-crypto", - "qp-poseidon", - "qp-rusty-crystals-dilithium", "qp-rusty-crystals-hdwallet", - "qp-wormhole-verifier 1.0.1", + "qp-wormhole-circuit-builder", + "qp-wormhole-verifier", "qpow-math", "quantus-miner-api", "quantus-runtime", @@ -9248,7 +9159,7 @@ dependencies = [ "log", "pallet-assets", "pallet-assets-holder", - "pallet-balances", + "pallet-balances 40.0.1", "pallet-conviction-voting", "pallet-mining-rewards", "pallet-multisig", @@ -9265,15 +9176,12 @@ dependencies = [ "pallet-transaction-payment-rpc-runtime-api", "pallet-treasury", "pallet-utility", - "pallet-wormhole", "parity-scale-codec", "primitive-types 0.13.1", "qp-dilithium-crypto", "qp-header", "qp-poseidon", "qp-scheduler", - "qp-wormhole", - "qp-wormhole-verifier 0.1.8", "scale-info", "serde_json", "sp-api", @@ -9315,7 +9223,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15a0580ab32b169745d7a39db2ba969226ca16738931be152a3209b409de2474" dependencies = [ "asynchronous-codec 0.7.0", - "bytes 1.11.1", + "bytes 1.10.1", "quick-protobuf", "thiserror 1.0.69", "unsigned-varint 0.8.0", @@ -9327,14 +9235,14 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cc2c5017e4b43d5995dcea317bc46c1e09404c0a9664d2908f7f02dfe943d75" dependencies = [ - "bytes 1.11.1", + "bytes 1.10.1", "pin-project-lite 0.2.16", "quinn-proto 0.10.6", "quinn-udp 0.4.1", "rustc-hash 1.1.0", "rustls 0.21.12", "thiserror 1.0.69", - "tokio 1.49.0", + "tokio 1.47.1", "tracing", ] @@ -9344,17 +9252,17 @@ version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ - "bytes 1.11.1", + "bytes 1.10.1", "cfg_aliases 0.2.1", "futures-io", "pin-project-lite 0.2.16", "quinn-proto 0.11.13", "quinn-udp 0.5.14", "rustc-hash 2.1.1", - "rustls 0.23.36", - "socket2 0.6.2", - "thiserror 2.0.18", - "tokio 1.49.0", + "rustls 0.23.32", + "socket2 0.6.0", + "thiserror 2.0.16", + "tokio 1.47.1", "tracing", "web-time", ] @@ -9365,7 +9273,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "141bf7dfde2fbc246bfd3fe12f2455aa24b0fbd9af535d8c86c7bd1381ff2b1a" dependencies = [ - "bytes 1.11.1", + "bytes 1.10.1", "rand 0.8.5", "ring 0.16.20", "rustc-hash 1.1.0", @@ -9383,16 +9291,16 @@ version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ - "bytes 1.11.1", - "getrandom 0.3.4", + "bytes 1.10.1", + "getrandom 0.3.3", "lru-slab", "rand 0.9.2", "ring 0.17.14", "rustc-hash 2.1.1", - "rustls 0.23.36", + "rustls 0.23.32", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror 2.0.16", "tinyvec", "tracing", "web-time", @@ -9404,7 +9312,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "055b4e778e8feb9f93c4e439f71dc2156ef13360b432b799e179a8c4cdf0b1d7" dependencies = [ - "bytes 1.11.1", + "bytes 1.10.1", "libc", "socket2 0.5.10", "tracing", @@ -9420,16 +9328,16 @@ dependencies = [ "cfg_aliases 0.2.1", "libc", "once_cell", - "socket2 0.6.2", + "socket2 0.6.0", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.44" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] @@ -9464,7 +9372,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.5", + "rand_core 0.9.3", "serde", ] @@ -9485,7 +9393,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.5", + "rand_core 0.9.3", ] [[package]] @@ -9494,16 +9402,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.17", + "getrandom 0.2.16", ] [[package]] name = "rand_core" -version = "0.9.5" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.3.3", "serde", ] @@ -9532,16 +9440,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" dependencies = [ - "rand_core 0.9.5", -] - -[[package]] -name = "rapidhash" -version = "4.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71ec30b38a417407efe7676bad0ca6b78f995f810185ece9af3bd5dc561185a9" -dependencies = [ - "rustversion", + "rand_core 0.9.3", ] [[package]] @@ -9550,7 +9449,7 @@ version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.9.4", ] [[package]] @@ -9602,20 +9501,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "redox_syscall" -version = "0.7.0" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.9.4", ] [[package]] @@ -9624,29 +9514,29 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom 0.2.17", + "getrandom 0.2.16", "libredox", "thiserror 1.0.69", ] [[package]] name = "ref-cast" -version = "1.0.25" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.25" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -9676,38 +9566,53 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.3" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.10", + "regex-syntax 0.8.6", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.6", ] [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "resolv-conf" -version = "0.7.6" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" +checksum = "6b3789b30bd25ba102de4beabd95d21ac45b69b1be7d14522bab988c526d6799" [[package]] name = "rfc6979" @@ -9742,7 +9647,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.17", + "getrandom 0.2.16", "libc", "untrusted 0.9.0", "windows-sys 0.52.0", @@ -9763,7 +9668,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" dependencies = [ - "bytes 1.11.1", + "bytes 1.10.1", "rustc-hex", ] @@ -9773,7 +9678,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa24e92bb2a83198bb76d661a71df9f7076b8c420b8696e4d3d97d50d94479e3" dependencies = [ - "bytes 1.11.1", + "bytes 1.10.1", "rustc-hex", ] @@ -9819,7 +9724,7 @@ dependencies = [ "netlink-sys", "nix", "thiserror 1.0.69", - "tokio 1.49.0", + "tokio 1.47.1", ] [[package]] @@ -9834,15 +9739,14 @@ dependencies = [ [[package]] name = "ruint" -version = "1.17.2" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c141e807189ad38a07276942c6623032d3753c8859c146104ac2e4d68865945a" +checksum = "9ecb38f82477f20c5c3d62ef52d7c4e536e38ea9b73fb570a20c5cae0e14bcf6" dependencies = [ "alloy-rlp", "ark-ff 0.3.0", "ark-ff 0.4.2", - "ark-ff 0.5.0", - "bytes 1.11.1", + "bytes 1.10.1", "fastrlp 0.3.1", "fastrlp 0.4.0", "num-bigint", @@ -9855,7 +9759,7 @@ dependencies = [ "rand 0.9.2", "rlp 0.5.2", "ruint-macro", - "serde_core", + "serde", "valuable", "zeroize", ] @@ -9868,9 +9772,9 @@ checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" [[package]] name = "rustc-demangle" -version = "0.1.27" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" @@ -9933,15 +9837,15 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.9.4", "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.61.2", + "windows-sys 0.61.0", ] [[package]] @@ -9957,15 +9861,15 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" dependencies = [ "log", "once_cell", "ring 0.17.14", "rustls-pki-types", - "rustls-webpki 0.103.9", + "rustls-webpki 0.103.6", "subtle 2.6.1", "zeroize", ] @@ -9976,7 +9880,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" dependencies = [ - "openssl-probe 0.1.6", + "openssl-probe", "rustls-pemfile", "schannel", "security-framework 2.11.1", @@ -9984,14 +9888,14 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" dependencies = [ - "openssl-probe 0.2.1", + "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.5.1", + "security-framework 3.5.0", ] [[package]] @@ -10005,9 +9909,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ "web-time", "zeroize", @@ -10024,11 +9928,11 @@ dependencies = [ "jni", "log", "once_cell", - "rustls 0.23.36", - "rustls-native-certs 0.8.3", + "rustls 0.23.32", + "rustls-native-certs 0.8.1", "rustls-platform-verifier-android", - "rustls-webpki 0.103.9", - "security-framework 3.5.1", + "rustls-webpki 0.103.6", + "security-framework 3.5.0", "security-framework-sys", "webpki-root-certs 0.26.11", "windows-sys 0.59.0", @@ -10052,9 +9956,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" dependencies = [ "ring 0.17.14", "rustls-pki-types", @@ -10069,9 +9973,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rusty-fork" -version = "0.3.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" dependencies = [ "fnv", "quick-error", @@ -10100,6 +10004,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "safe_arch" version = "0.7.4" @@ -10168,7 +10078,7 @@ dependencies = [ "sp-runtime", "substrate-prometheus-endpoint", "thiserror 1.0.69", - "tokio 1.49.0", + "tokio 1.47.1", ] [[package]] @@ -10217,7 +10127,7 @@ checksum = "c25df970b58c05e8381a1ead6f5708e3539aefa9212d311866fed64f141214e7" dependencies = [ "array-bytes 6.2.3", "docify", - "memmap2 0.9.9", + "memmap2 0.9.8", "parity-scale-codec", "sc-chain-spec-derive", "sc-client-api", @@ -10245,7 +10155,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -10291,7 +10201,7 @@ dependencies = [ "sp-version", "tempfile", "thiserror 1.0.69", - "tokio 1.49.0", + "tokio 1.47.1", ] [[package]] @@ -10304,7 +10214,7 @@ dependencies = [ "futures 0.3.31", "log", "parity-scale-codec", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "sc-executor", "sc-transaction-pool-api", "sc-utils", @@ -10335,7 +10245,7 @@ dependencies = [ "log", "parity-db", "parity-scale-codec", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "sc-client-api", "sc-state-db", "schnellru", @@ -10360,7 +10270,7 @@ dependencies = [ "futures 0.3.31", "log", "mockall", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "sc-client-api", "sc-network-types", "sc-utils", @@ -10388,7 +10298,7 @@ dependencies = [ "num-rational", "num-traits", "parity-scale-codec", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "sc-client-api", "sc-consensus", "sc-consensus-epochs", @@ -10435,7 +10345,7 @@ dependencies = [ "hex", "log", "parity-scale-codec", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "primitive-types 0.13.1", "sc-client-api", "sc-consensus", @@ -10484,7 +10394,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfd7a23eebd1fea90534994440f0ef516cbd8c0ef749e272c6c77fd729bbca71" dependencies = [ "parity-scale-codec", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "sc-executor-common", "sc-executor-polkavm", "sc-executor-wasmtime", @@ -10535,7 +10445,7 @@ checksum = "a5980897e2915ef027560886a2bb52f49a2cea4a9b9f5c75fead841201d03705" dependencies = [ "anyhow", "log", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "rustix 0.36.17", "sc-allocator", "sc-executor-common", @@ -10568,7 +10478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08c28fc85c00ddf64f32f68111c61521b1f260f8dfa1eb98f9ed4401aa5d0ce0" dependencies = [ "array-bytes 6.2.3", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "serde_json", "sp-application-crypto", "sp-core", @@ -10585,13 +10495,13 @@ dependencies = [ "array-bytes 6.2.3", "arrayvec 0.7.6", "blake2 0.10.6", - "bytes 1.11.1", + "bytes 1.10.1", "futures 0.3.31", "futures-timer", "log", "mixnet", "parity-scale-codec", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "sc-client-api", "sc-network", "sc-network-types", @@ -10614,7 +10524,7 @@ dependencies = [ "async-channel 1.9.0", "async-trait", "asynchronous-codec 0.6.2", - "bytes 1.11.1", + "bytes 1.10.1", "cid 0.9.0", "criterion", "either", @@ -10629,7 +10539,7 @@ dependencies = [ "mockall", "multistream-select", "parity-scale-codec", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "partial_sort", "pin-project", "prost 0.12.6", @@ -10650,7 +10560,7 @@ dependencies = [ "substrate-prometheus-endpoint", "tempfile", "thiserror 1.0.69", - "tokio 1.49.0", + "tokio 1.47.1", "tokio-stream", "tokio-util", "unsigned-varint 0.7.2", @@ -10724,7 +10634,7 @@ dependencies = [ "sp-runtime", "substrate-prometheus-endpoint", "thiserror 1.0.69", - "tokio 1.49.0", + "tokio 1.47.1", "tokio-stream", ] @@ -10755,7 +10665,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "441af5d0adf306ff745ccf23c7426ec2edf24f6fee678fb63994e1f8d2fcc890" dependencies = [ "bs58", - "bytes 1.11.1", + "bytes 1.10.1", "ed25519-dalek", "libp2p-identity", "libp2p-kad", @@ -10776,20 +10686,20 @@ version = "46.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "029f2eb16f9510a749201e7a2b405aafcc5afcc515509518d2efb17b164ca47e" dependencies = [ - "bytes 1.11.1", + "bytes 1.10.1", "fnv", "futures 0.3.31", "futures-timer", "http-body-util", - "hyper 1.8.1", + "hyper 1.7.0", "hyper-rustls", "hyper-util", "num_cpus", "once_cell", "parity-scale-codec", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "rand 0.8.5", - "rustls 0.23.36", + "rustls 0.23.32", "sc-client-api", "sc-network", "sc-network-types", @@ -10825,7 +10735,7 @@ dependencies = [ "jsonrpsee", "log", "parity-scale-codec", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "sc-block-builder", "sc-chain-spec", "sc-client-api", @@ -10845,7 +10755,7 @@ dependencies = [ "sp-session", "sp-statement-store", "sp-version", - "tokio 1.49.0", + "tokio 1.47.1", ] [[package]] @@ -10879,9 +10789,9 @@ dependencies = [ "forwarded-header-value", "futures 0.3.31", "governor", - "http 1.4.0", + "http 1.3.1", "http-body-util", - "hyper 1.8.1", + "hyper 1.7.0", "ip_network", "jsonrpsee", "log", @@ -10889,7 +10799,7 @@ dependencies = [ "serde", "serde_json", "substrate-prometheus-endpoint", - "tokio 1.49.0", + "tokio 1.47.1", "tower", "tower-http", ] @@ -10908,7 +10818,7 @@ dependencies = [ "jsonrpsee", "log", "parity-scale-codec", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "rand 0.8.5", "sc-chain-spec", "sc-client-api", @@ -10924,7 +10834,7 @@ dependencies = [ "sp-version", "substrate-prometheus-endpoint", "thiserror 1.0.69", - "tokio 1.49.0", + "tokio 1.47.1", "tokio-stream", ] @@ -10958,7 +10868,7 @@ dependencies = [ "jsonrpsee", "log", "parity-scale-codec", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "pin-project", "rand 0.8.5", "sc-chain-spec", @@ -11004,7 +10914,7 @@ dependencies = [ "substrate-prometheus-endpoint", "tempfile", "thiserror 1.0.69", - "tokio 1.49.0", + "tokio 1.47.1", "tracing", "tracing-futures", ] @@ -11017,7 +10927,7 @@ checksum = "b81d0da325c141081336e8d8724f5342f2586bbb574fde81f716f7fab447325a" dependencies = [ "log", "parity-scale-codec", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "sp-core", ] @@ -11052,7 +10962,7 @@ dependencies = [ "futures 0.3.31", "libp2p", "log", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "pin-project", "rand 0.8.5", "sc-utils", @@ -11064,18 +10974,17 @@ dependencies = [ [[package]] name = "sc-tracing" -version = "40.0.2" +version = "40.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a7afd6f7dc499ae1bedd1e29feb611ffa38bfc33c325a25cc9b00eba0e83f0" +checksum = "d80e2558a0100794d5b4f4d75cb45cae65c061aaf386fd807d7a7e2c272251d2" dependencies = [ "chrono", "console", - "frame-metadata 23.0.1", "is-terminal", "libc", "log", "parity-scale-codec", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "rustc-hash 1.1.0", "sc-client-api", "sc-tracing-proc-macro", @@ -11086,7 +10995,6 @@ dependencies = [ "sp-rpc", "sp-runtime", "sp-tracing", - "sp-trie", "thiserror 1.0.69", "tracing", "tracing-log", @@ -11102,7 +11010,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -11114,11 +11022,11 @@ dependencies = [ "async-trait", "futures 0.3.31", "futures-timer", - "indexmap 2.13.0", + "indexmap 2.11.4", "itertools 0.11.0", "linked-hash-map", "parity-scale-codec", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "sc-client-api", "sc-transaction-pool-api", "sc-utils", @@ -11132,7 +11040,7 @@ dependencies = [ "sp-transaction-pool", "substrate-prometheus-endpoint", "thiserror 1.0.69", - "tokio 1.49.0", + "tokio 1.47.1", "tokio-stream", "tracing", ] @@ -11145,7 +11053,7 @@ checksum = "c27a9cb54784cf7a1a607d4314f1a4437279ce6d4070eb810f3e4fbfff9b1ba7" dependencies = [ "async-trait", "futures 0.3.31", - "indexmap 2.13.0", + "indexmap 2.11.4", "log", "parity-scale-codec", "serde", @@ -11165,7 +11073,7 @@ dependencies = [ "futures 0.3.31", "futures-timer", "log", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "prometheus", "sp-arithmetic", ] @@ -11184,9 +11092,9 @@ dependencies = [ [[package]] name = "scale-decode" -version = "0.16.2" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d6ed61699ad4d54101ab5a817169259b5b0efc08152f8632e61482d8a27ca3d" +checksum = "4d78196772d25b90a98046794ce0fe2588b39ebdfbdc1e45b4c6c85dd43bebad" dependencies = [ "parity-scale-codec", "primitive-types 0.13.1", @@ -11194,26 +11102,26 @@ dependencies = [ "scale-decode-derive", "scale-type-resolver", "smallvec", - "thiserror 2.0.18", + "thiserror 2.0.16", ] [[package]] name = "scale-decode-derive" -version = "0.16.2" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65cb245f7fdb489e7ba43a616cbd34427fe3ba6fe0edc1d0d250085e6c84f3ec" +checksum = "2f4b54a1211260718b92832b661025d1f1a4b6930fbadd6908e00edd265fa5f7" dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] name = "scale-encode" -version = "0.10.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2a976d73564a59e482b74fd5d95f7518b79ca8c8ca5865398a4d629dd15ee50" +checksum = "64901733157f9d25ef86843bd783eda439fac7efb0ad5a615d12d2cf3a29464b" dependencies = [ "parity-scale-codec", "primitive-types 0.13.1", @@ -11221,20 +11129,20 @@ dependencies = [ "scale-encode-derive", "scale-type-resolver", "smallvec", - "thiserror 2.0.18", + "thiserror 2.0.16", ] [[package]] name = "scale-encode-derive" -version = "0.10.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17020f2d59baabf2ddcdc20a4e567f8210baf089b8a8d4785f5fd5e716f92038" +checksum = "78a3993a13b4eafa89350604672c8757b7ea84c7c5947d4b3691e3169c96379b" dependencies = [ "darling 0.20.11", "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -11260,7 +11168,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -11282,15 +11190,15 @@ dependencies = [ "proc-macro2", "quote", "scale-info", - "syn 2.0.114", - "thiserror 2.0.18", + "syn 2.0.106", + "thiserror 2.0.16", ] [[package]] name = "scale-value" -version = "0.18.1" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884aab179aba344c67ddcd1d7dd8e3f8fee202f2e570d97ec34ec8688442a5b3" +checksum = "8ca8b26b451ecb7fd7b62b259fa28add63d12ec49bbcac0e01fcb4b5ae0c09aa" dependencies = [ "base58", "blake2 0.10.6", @@ -11301,7 +11209,7 @@ dependencies = [ "scale-encode", "scale-type-resolver", "serde", - "thiserror 2.0.18", + "thiserror 2.0.16", "yap", ] @@ -11311,7 +11219,7 @@ version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.61.0", ] [[package]] @@ -11344,6 +11252,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -11417,7 +11331,7 @@ version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" dependencies = [ - "bitcoin_hashes 0.14.1", + "bitcoin_hashes 0.14.0", "rand 0.8.5", "secp256k1-sys 0.10.1", ] @@ -11473,7 +11387,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.9.4", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -11482,11 +11396,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.5.1" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "cc198e42d9b7510827939c9a15f5062a0c913f3371d765977e586d2fe6c16f4a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.9.4", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -11548,9 +11462,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.228" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" dependencies = [ "serde_core", "serde_derive", @@ -11568,35 +11482,35 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", + "ryu", "serde", "serde_core", - "zmij", ] [[package]] @@ -11610,14 +11524,15 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.16.1" +version = "3.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" dependencies = [ "base64 0.22.1", "chrono", "hex", - "serde_core", + "serde", + "serde_derive", "serde_json", "serde_with_macros", "time", @@ -11625,14 +11540,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.16.1" +version = "3.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e" dependencies = [ "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -11692,9 +11607,9 @@ dependencies = [ [[package]] name = "sha3-asm" -version = "0.1.5" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b31139435f327c93c6038ed350ae4588e2c70a13d50599509fee6349967ba35a" +checksum = "c28efc5e327c837aa837c59eae585fc250715ef939ac32881bcc11677cd02d46" dependencies = [ "cc", "cfg-if", @@ -11717,11 +11632,10 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.8" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ - "errno", "libc", ] @@ -11754,7 +11668,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dee851d0e5e7af3721faea1843e8015e820a234f81fda3dea9247e15bac9a86a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.9.4", ] [[package]] @@ -11771,15 +11685,15 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "siphasher" -version = "1.0.2" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" -version = "0.4.12" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "slice-group-by" @@ -11854,7 +11768,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "sha3", - "siphasher 1.0.2", + "siphasher 1.0.1", "slab", "smallvec", "soketto", @@ -11887,13 +11801,13 @@ dependencies = [ "itertools 0.13.0", "log", "lru", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "pin-project", "rand 0.8.5", "rand_chacha 0.3.1", "serde", "serde_json", - "siphasher 1.0.2", + "siphasher 1.0.1", "slab", "smol", "smoldot", @@ -11930,12 +11844,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -11945,9 +11859,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" dependencies = [ "base64 0.22.1", - "bytes 1.11.1", + "bytes 1.10.1", "futures 0.3.31", - "http 1.4.0", + "http 1.3.1", "httparse", "log", "rand 0.8.5", @@ -11989,7 +11903,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -12052,7 +11966,7 @@ checksum = "849f1cfcf170048d59c8d3d1175feea2a5cd41fe39742660b9ed542f0d1be7b0" dependencies = [ "futures 0.3.31", "parity-scale-codec", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "schnellru", "sp-api", "sp-consensus", @@ -12191,7 +12105,7 @@ dependencies = [ "merlin", "parity-bip39", "parity-scale-codec", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "paste", "primitive-types 0.13.1", "rand 0.8.5", @@ -12237,17 +12151,17 @@ checksum = "b85d0f1f1e44bd8617eb2a48203ee854981229e3e79e6f468c7175d5fd37489b" dependencies = [ "quote", "sp-crypto-hashing", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] name = "sp-database" -version = "10.0.1" +version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c702cc7679fbaf0469d40251917cd27bfc165c506a8cd96fb4a9dd3947f06d70" +checksum = "722cbecdbf5b94578137dbd07feb51e95f7de221be0c1ff4dcfe0bb4cd986929" dependencies = [ "kvdb", - "parking_lot 0.12.5", + "parking_lot 0.12.4", ] [[package]] @@ -12258,7 +12172,7 @@ checksum = "48d09fa0a5f7299fb81ee25ae3853d26200f7a348148aed6de76be905c007dbe" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -12305,7 +12219,7 @@ version = "41.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3f244e9a2818d21220ceb0915ac73a462814a92d0c354a124a818abdb7f4f66" dependencies = [ - "bytes 1.11.1", + "bytes 1.10.1", "docify", "ed25519-dalek", "libsecp256k1", @@ -12344,16 +12258,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "269d0ee360f6d072f9203485afea35583ac151521a525cc48b2a107fc576c2d9" dependencies = [ "parity-scale-codec", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "sp-core", "sp-externalities", ] [[package]] name = "sp-maybe-compressed-blob" -version = "11.1.0" +version = "11.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd622e9c93d874f70f8df15ba1512fb95d8339aa5629157a826ec65a0c568" +checksum = "f0c768c11afbe698a090386876911da4236af199cd38a5866748df4d8628aeff" dependencies = [ "thiserror 1.0.69", "zstd 0.12.4", @@ -12361,11 +12275,11 @@ dependencies = [ [[package]] name = "sp-metadata-ir" -version = "0.11.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c327d0340a18979aa242f2ad3ad8a6f5fa76c03cc7ca5b50c4770573fd8191" +checksum = "2319040b39b9614c35c7faaf548172f4d9a3b44b6992bbae534b096d5cdb4f79" dependencies = [ - "frame-metadata 23.0.1", + "frame-metadata 23.0.0", "parity-scale-codec", "scale-info", ] @@ -12482,7 +12396,7 @@ version = "30.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fcd9c219da8c85d45d5ae1ce80e73863a872ac27424880322903c6ac893c06e" dependencies = [ - "bytes 1.11.1", + "bytes 1.10.1", "impl-trait-for-tuples", "parity-scale-codec", "polkavm-derive 0.24.0", @@ -12507,7 +12421,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -12549,7 +12463,7 @@ dependencies = [ "hash-db", "log", "parity-scale-codec", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "pretty_assertions", "rand 0.8.5", "smallvec", @@ -12560,14 +12474,14 @@ dependencies = [ "sp-trie", "thiserror 1.0.69", "tracing", - "trie-db 0.30.0", + "trie-db", ] [[package]] name = "sp-statement-store" -version = "21.2.0" +version = "21.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031e2366f3633aac66c223747f47db82d774e726e117776eab66937c0bf9f4a8" +checksum = "76d11b0753df3d68f5bb0f4d0d3975788c3a4dd2d0e479e28b7af17b52f15160" dependencies = [ "aes-gcm", "curve25519-dalek", @@ -12671,7 +12585,7 @@ dependencies = [ "memory-db", "nohash-hasher", "parity-scale-codec", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "rand 0.8.5", "scale-info", "schnellru", @@ -12682,7 +12596,7 @@ dependencies = [ "thiserror 1.0.69", "tracing", "trie-bench", - "trie-db 0.30.0", + "trie-db", "trie-root", "trie-standardmap", ] @@ -12715,7 +12629,7 @@ dependencies = [ "proc-macro-warning", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -12794,9 +12708,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "staging-xcm" @@ -12881,8 +12795,8 @@ dependencies = [ "bitflags 1.3.2", "cfg_aliases 0.2.1", "libc", - "parking_lot 0.12.5", - "parking_lot_core 0.9.12", + "parking_lot 0.12.4", + "parking_lot_core 0.9.11", "static_init_macro", "winapi", ] @@ -12960,7 +12874,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -13023,12 +12937,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d23e4bc8e910a312820d589047ab683928b761242dbe31dee081fbdb37cbe0be" dependencies = [ "http-body-util", - "hyper 1.8.1", + "hyper 1.7.0", "hyper-util", "log", "prometheus", "thiserror 1.0.69", - "tokio 1.49.0", + "tokio 1.47.1", ] [[package]] @@ -13055,16 +12969,16 @@ dependencies = [ [[package]] name = "substrate-wasm-builder" -version = "27.0.1" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb9c10bbb80741492b391478670b8cda7bd1c38cad01748e7a5978654531094" +checksum = "c57b288a411017a7e96ae36a767647cc3e66ea49423d4cd72885adac47beaf07" dependencies = [ "array-bytes 6.2.3", "build-helper", "cargo_metadata", "console", "filetime", - "frame-metadata 23.0.1", + "frame-metadata 23.0.0", "jobserver", "merkleized-metadata", "parity-scale-codec", @@ -13123,8 +13037,8 @@ dependencies = [ "subxt-macro", "subxt-metadata", "subxt-rpcs", - "thiserror 2.0.18", - "tokio 1.49.0", + "thiserror 2.0.16", + "tokio 1.47.1", "tokio-util", "tracing", "url", @@ -13144,8 +13058,8 @@ dependencies = [ "scale-info", "scale-typegen", "subxt-metadata", - "syn 2.0.114", - "thiserror 2.0.18", + "syn 2.0.106", + "thiserror 2.0.16", ] [[package]] @@ -13174,7 +13088,7 @@ dependencies = [ "serde_json", "sp-crypto-hashing", "subxt-metadata", - "thiserror 2.0.18", + "thiserror 2.0.16", "tracing", ] @@ -13189,8 +13103,8 @@ dependencies = [ "serde", "serde_json", "smoldot-light", - "thiserror 2.0.18", - "tokio 1.49.0", + "thiserror 2.0.16", + "tokio 1.47.1", "tokio-stream", "tracing", ] @@ -13208,7 +13122,7 @@ dependencies = [ "scale-typegen", "subxt-codegen", "subxt-utils-fetchmetadata", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -13223,7 +13137,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-crypto-hashing", - "thiserror 2.0.18", + "thiserror 2.0.16", ] [[package]] @@ -13244,7 +13158,7 @@ dependencies = [ "serde_json", "subxt-core", "subxt-lightclient", - "thiserror 2.0.18", + "thiserror 2.0.16", "tracing", "url", ] @@ -13275,7 +13189,7 @@ dependencies = [ "sha2 0.10.9", "sp-crypto-hashing", "subxt-core", - "thiserror 2.0.18", + "thiserror 2.0.16", "zeroize", ] @@ -13287,7 +13201,7 @@ checksum = "fc868b55fe2303788dc7703457af390111940c3da4714b510983284501780ed5" dependencies = [ "hex", "parity-scale-codec", - "thiserror 2.0.18", + "thiserror 2.0.16", ] [[package]] @@ -13303,9 +13217,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.114" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -13314,14 +13228,14 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "1.5.4" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2379beea9476b89d0237078be761cf8e012d92d5ae4ae0c9a329f974838870fc" +checksum = "a0b198d366dbec045acfcd97295eb653a7a2b40e4dc764ef1e79aafcad439d3c" dependencies = [ "paste", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -13344,7 +13258,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -13368,7 +13282,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.9.4", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -13403,15 +13317,15 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" -version = "3.24.0" +version = "3.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.3.3", "once_cell", - "rustix 1.1.3", - "windows-sys 0.61.2", + "rustix 1.1.2", + "windows-sys 0.61.0", ] [[package]] @@ -13429,7 +13343,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ - "rustix 1.1.3", + "rustix 1.1.2", "windows-sys 0.60.2", ] @@ -13450,11 +13364,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.16", ] [[package]] @@ -13465,18 +13379,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -13546,30 +13460,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde_core", + "serde", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -13586,9 +13500,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", @@ -13632,19 +13546,22 @@ dependencies = [ [[package]] name = "tokio" -version = "1.49.0" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ - "bytes 1.11.1", + "backtrace", + "bytes 1.10.1", + "io-uring", "libc", "mio", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "pin-project-lite 0.2.16", "signal-hook-registry", - "socket2 0.6.2", - "tokio-macros 2.6.0", - "windows-sys 0.61.2", + "slab", + "socket2 0.6.0", + "tokio-macros 2.5.0", + "windows-sys 0.59.0", ] [[package]] @@ -13660,34 +13577,34 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] name = "tokio-rustls" -version = "0.26.4" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +checksum = "05f63835928ca123f1bef57abbcd23bb2ba0ac9ae1235f1e65bda0d06e7786bd" dependencies = [ - "rustls 0.23.36", - "tokio 1.49.0", + "rustls 0.23.32", + "tokio 1.47.1", ] [[package]] name = "tokio-stream" -version = "0.1.18" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite 0.2.16", - "tokio 1.49.0", + "tokio 1.47.1", "tokio-util", ] @@ -13699,26 +13616,26 @@ checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" dependencies = [ "futures-util", "log", - "rustls 0.23.36", - "rustls-native-certs 0.8.3", + "rustls 0.23.32", + "rustls-native-certs 0.8.1", "rustls-pki-types", - "tokio 1.49.0", + "tokio 1.47.1", "tokio-rustls", "tungstenite", ] [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ - "bytes 1.11.1", + "bytes 1.10.1", "futures-core", "futures-io", "futures-sink", "pin-project-lite 0.2.16", - "tokio 1.49.0", + "tokio 1.47.1", ] [[package]] @@ -13753,9 +13670,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" dependencies = [ "serde_core", ] @@ -13766,7 +13683,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.11.4", "serde", "serde_spanned", "toml_datetime 0.6.11", @@ -13776,21 +13693,21 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.10+spec-1.0.0" +version = "0.23.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" dependencies = [ - "indexmap 2.13.0", - "toml_datetime 0.7.5+spec-1.1.0", + "indexmap 2.11.4", + "toml_datetime 0.7.2", "toml_parser", "winnow", ] [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" dependencies = [ "winnow", ] @@ -13822,9 +13739,9 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ - "bitflags 2.10.0", - "bytes 1.11.1", - "http 1.4.0", + "bitflags 2.9.4", + "bytes 1.10.1", + "http 1.3.1", "http-body 1.0.1", "http-body-util", "pin-project-lite 0.2.16", @@ -13846,9 +13763,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.44" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite 0.2.16", @@ -13858,20 +13775,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.31" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] name = "tracing-core" -version = "0.1.36" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", @@ -13909,7 +13826,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -13925,15 +13842,15 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "parking_lot 0.12.5", - "regex-automata", + "parking_lot 0.12.4", + "regex", "sharded-slab", "smallvec", "thread_local", @@ -13955,16 +13872,16 @@ dependencies = [ [[package]] name = "trie-bench" -version = "0.42.1" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a03bee4700c5dd6b2ceba5e4e4d5a7017704a761481824d3033d223f9660973a" +checksum = "972be214c558b1a5550d34c8c7e55a284f6439cefc51226d6ffbfc152de5cc58" dependencies = [ "criterion", "hash-db", "keccak-hasher", "memory-db", "parity-scale-codec", - "trie-db 0.31.0", + "trie-db", "trie-root", "trie-standardmap", ] @@ -13981,18 +13898,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "trie-db" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7795f2df2ef744e4ffb2125f09325e60a21d305cc3ecece0adeef03f7a9e560" -dependencies = [ - "hash-db", - "log", - "rustc-hex", - "smallvec", -] - [[package]] name = "trie-root" version = "0.18.0" @@ -14030,16 +13935,16 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" dependencies = [ - "bytes 1.11.1", + "bytes 1.10.1", "data-encoding", - "http 1.4.0", + "http 1.3.1", "httparse", "log", "rand 0.9.2", - "rustls 0.23.36", + "rustls 0.23.32", "rustls-pki-types", "sha1", - "thiserror 2.0.18", + "thiserror 2.0.16", "url", "utf-8", ] @@ -14064,9 +13969,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.19.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "ucd-trie" @@ -14106,9 +14011,9 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-normalization" @@ -14127,9 +14032,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.2.2" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" [[package]] name = "unicode-xid" @@ -14164,7 +14069,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6889a77d49f1f013504cec6bf97a2c730394adedaeb1deb5ea08949a50541105" dependencies = [ "asynchronous-codec 0.6.2", - "bytes 1.11.1", + "bytes 1.10.1", "futures-io", "futures-util", ] @@ -14175,7 +14080,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" dependencies = [ - "bytes 1.11.1", + "bytes 1.10.1", "tokio-util", ] @@ -14193,9 +14098,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.8" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -14223,11 +14128,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.20.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.3.3", "js-sys", "wasm-bindgen", ] @@ -14358,29 +14263,38 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ "wit-bindgen", ] [[package]] name = "wasix" -version = "0.13.1" +version = "0.12.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1757e0d1f8456693c7e5c6c629bdb54884e032aa0bb53c155f6a39f94440d332" +checksum = "c1fbb4ef9bbca0c1170e0b00dd28abc9e3b68669821600cad1caaed606583c6d" dependencies = [ - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" dependencies = [ "cfg-if", "once_cell", @@ -14389,14 +14303,27 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-shared", +] + [[package]] name = "wasm-bindgen-futures" -version = "0.4.58" +version = "0.4.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "a0b221ff421256839509adbb55998214a70d829d3a28c69b4a6672e9d2a42f67" dependencies = [ "cfg-if", - "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -14405,9 +14332,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -14415,22 +14342,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" dependencies = [ - "bumpalo", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", + "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" dependencies = [ "unicode-ident", ] @@ -14755,9 +14682,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc" dependencies = [ "js-sys", "wasm-bindgen", @@ -14779,14 +14706,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" dependencies = [ - "webpki-root-certs 1.0.6", + "webpki-root-certs 1.0.2", ] [[package]] name = "webpki-root-certs" -version = "1.0.6" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +checksum = "4e4ffd8df1c57e87c325000a3d6ef93db75279dc3a231125aac571650f22b12a" dependencies = [ "rustls-pki-types", ] @@ -14809,9 +14736,9 @@ dependencies = [ [[package]] name = "widestring" -version = "1.2.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" +checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" [[package]] name = "winapi" @@ -14835,7 +14762,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.61.0", ] [[package]] @@ -14864,6 +14791,28 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -14885,44 +14834,84 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.62.2" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" dependencies = [ "windows-implement", "windows-interface", - "windows-link", - "windows-result 0.4.1", - "windows-strings", + "windows-link 0.2.0", + "windows-result 0.4.0", + "windows-strings 0.5.0", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", ] [[package]] name = "windows-implement" -version = "0.60.2" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] name = "windows-interface" -version = "0.59.3" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] name = "windows-link" -version = "0.2.1" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] [[package]] name = "windows-result" @@ -14935,20 +14924,38 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.4.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +dependencies = [ + "windows-link 0.2.0", ] [[package]] name = "windows-strings" -version = "0.5.1" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" dependencies = [ - "windows-link", + "windows-link 0.2.0", ] [[package]] @@ -14993,16 +15000,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.5", + "windows-targets 0.53.3", ] [[package]] name = "windows-sys" -version = "0.61.2" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" dependencies = [ - "windows-link", + "windows-link 0.2.0", ] [[package]] @@ -15053,19 +15060,28 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.5" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link 0.1.3", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", + "windows-link 0.1.3", ] [[package]] @@ -15088,9 +15104,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.1" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" [[package]] name = "windows_aarch64_msvc" @@ -15112,9 +15128,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.1" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" [[package]] name = "windows_i686_gnu" @@ -15136,9 +15152,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.1" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" [[package]] name = "windows_i686_gnullvm" @@ -15148,9 +15164,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.1" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" [[package]] name = "windows_i686_msvc" @@ -15172,9 +15188,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.1" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" [[package]] name = "windows_x86_64_gnu" @@ -15196,9 +15212,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.1" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" [[package]] name = "windows_x86_64_gnullvm" @@ -15220,9 +15236,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.1" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" [[package]] name = "windows_x86_64_msvc" @@ -15244,15 +15260,15 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.1" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] @@ -15269,15 +15285,15 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.51.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "wyz" @@ -15330,7 +15346,7 @@ dependencies = [ "nom", "oid-registry 0.8.1", "rusticata-macros", - "thiserror 2.0.18", + "thiserror 2.0.16", "time", ] @@ -15343,14 +15359,14 @@ dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] name = "xml-rs" -version = "0.8.28" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" +checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" [[package]] name = "xmltree" @@ -15370,7 +15386,7 @@ dependencies = [ "futures 0.3.31", "log", "nohash-hasher", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "pin-project", "rand 0.8.5", "static_assertions", @@ -15378,14 +15394,14 @@ dependencies = [ [[package]] name = "yamux" -version = "0.13.8" +version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deab71f2e20691b4728b349c6cee8fc7223880fa67b6b4f92225ec32225447e5" +checksum = "2b2dd50a6d6115feb3e5d7d0efd45e8ca364b6c83722c1e9c602f5764e0e9597" dependencies = [ "futures 0.3.31", "log", "nohash-hasher", - "parking_lot 0.12.5", + "parking_lot 0.12.4", "pin-project", "rand 0.9.2", "static_assertions", @@ -15415,10 +15431,11 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ + "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -15426,34 +15443,34 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", "synstructure 0.13.2", ] [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] @@ -15473,35 +15490,35 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", "synstructure 0.13.2", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.3" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" dependencies = [ "displaydoc", "yoke", @@ -15510,9 +15527,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke", "zerofrom", @@ -15521,21 +15538,15 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.106", ] -[[package]] -name = "zmij" -version = "1.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" - [[package]] name = "zstd" version = "0.11.2+zstd.1.5.2" diff --git a/Cargo.toml b/Cargo.toml index 70cfc7bd..c78175f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "client/network", "miner-api", "node", + "pallets/balances", "pallets/mining-rewards", "pallets/multisig", "pallets/qpow", @@ -129,7 +130,7 @@ wasm-timer = { version = "0.2.5" } zeroize = { version = "1.7.0", default-features = false } # Own dependencies -pallet-balances = { version = "42.0.0", default-features = false } +pallet-balances = { path = "./pallets/balances", default-features = false } pallet-mining-rewards = { path = "./pallets/mining-rewards", default-features = false } pallet-multisig = { path = "./pallets/multisig", default-features = false } pallet-qpow = { path = "./pallets/qpow", default-features = false } @@ -148,19 +149,18 @@ sp-consensus-pow = { path = "./primitives/consensus/pow", default-features = fal sp-consensus-qpow = { path = "./primitives/consensus/qpow", default-features = false } # Quantus network dependencies -qp-plonky2 = { version = "1.1.3", default-features = false } -qp-poseidon = { version = "1.0.7", default-features = false } -qp-poseidon-core = { version = "1.0.7", package = "qp-poseidon-core", default-features = false, features = [ - "p2", - "p3", -] } +qp-poseidon = { version = "1.0.1", default-features = false } +qp-poseidon-core = { version = "1.0.1", default-features = false, features = ["p3"] } qp-rusty-crystals-dilithium = { version = "2.0.0", default-features = false } qp-rusty-crystals-hdwallet = { version = "1.0.0" } -qp-wormhole-circuit = { git = "https://github.com/Quantus-Network/qp-zk-circuits", branch = "illuzen/agg-fees", default-features = false } -qp-wormhole-circuit-builder = { git = "https://github.com/Quantus-Network/qp-zk-circuits", branch = "illuzen/agg-fees", default-features = false } -qp-wormhole-prover = { git = "https://github.com/Quantus-Network/qp-zk-circuits", branch = "illuzen/agg-fees", default-features = false } -qp-wormhole-verifier = { git = "https://github.com/Quantus-Network/qp-zk-circuits", branch = "illuzen/agg-fees", default-features = false } -qp-zk-circuits-common = { git = "https://github.com/Quantus-Network/qp-zk-circuits", branch = "illuzen/agg-fees", default-features = false } +qp-wormhole-circuit = { version = "0.1.2", default-features = false } +qp-wormhole-circuit-builder = { version = "0.1.2", default-features = false } +qp-wormhole-verifier = { version = "0.1.2", default-features = false, features = [ + "no_random", +] } +qp-zk-circuits-common = { version = "0.1.2", default-features = false, features = [ + "no_random", +] } # polkadot-sdk dependencies frame-benchmarking = { version = "41.0.0", default-features = false } @@ -239,14 +239,6 @@ sc-cli = { path = "./client/cli" } sc-network = { path = "client/network" } sp-state-machine = { path = "./primitives/state-machine" } sp-trie = { path = "./primitives/trie" } -# Patch plonky2 crates to use local versions with fixed rand feature handling -qp-plonky2 = { path = "../qp-plonky2/plonky2" } -qp-plonky2-field = { path = "../qp-plonky2/field" } - -[patch."https://github.com/Quantus-Network/qp-zk-circuits"] -qp-wormhole-verifier = { path = "../qp-zk-circuits/wormhole/verifier" } -qp-wormhole-inputs = { path = "../qp-zk-circuits/wormhole/inputs" } - [profile.release] diff --git a/MINING.md b/MINING.md index 70a8d8fb..978b0370 100644 --- a/MINING.md +++ b/MINING.md @@ -2,15 +2,6 @@ Get started mining on the Quantus Network testnet in minutes. -## Important: Wormhole Address System - -**⚠️ Mining rewards are automatically sent to wormhole addresses derived from your preimage.** - -- You provide a 32-byte preimage when starting mining -- The system derives your wormhole address using Poseidon hashing -- All mining rewards are sent to this derived wormhole address -- This ensures all miners use privacy-preserving wormhole addresses - ## System Requirements ### Minimum Requirements @@ -41,17 +32,17 @@ If you prefer manual installation or the script doesn't work for your system: ./quantus-node key generate-node-key --file ~/.quantus/node_key.p2p ``` -3. **Generate Wormhole Address & Preimage** - +3. **Generate Rewards Address** ```bash - ./quantus-node key quantus --scheme wormhole + ./quantus-node key quantus ``` - - This generates a wormhole key pair and shows: - - `Address`: Your wormhole address (where rewards will be sent) - - `inner_hash`: Your 32-byte preimage (use this for mining) - - **Save the preimage** - you'll need it for the `--rewards-address` parameter. + + The address is in the output like this: +```sh +... +Address: qzpjg55HuN2vLdQerpZwhsGfRn6b4pc8uh4bdEgsYbJNeu8rn +... +``` 4. **Run the node (Dirac testnet)** @@ -61,12 +52,10 @@ Minimal command - see --help for many more options --validator \ --chain dirac \ --node-key-file ~/.quantus/node_key.p2p \ - --rewards-preimage \ + --rewards-address \ --max-blocks-per-request 64 \ --sync full ``` - -**Note:** Use the `inner_hash` from step 3 as your `--rewards-preimage`. The node will derive your wormhole address and log it on startup. ### Docker Installation For users who prefer containerized deployment or have only Docker installed: @@ -103,20 +92,23 @@ docker run --rm --platform linux/amd64 \ Replace `quantus-node:v0.0.4` with your desired image (e.g., `ghcr.io/quantus-network/quantus-node:latest`). This command saves `node_key.p2p` into your local `./quantus_node_data` directory. -**Step 3: Generate Your Wormhole Address** +**Step 3: Generate and Save Your Rewards Address** +Run the following command to generate your unique rewards address: ```bash # If on Apple Silicon, you may need to add --platform linux/amd64 -docker run --rm ghcr.io/quantus-network/quantus-node:latest key quantus --scheme wormhole +docker run --rm ghcr.io/quantus-network/quantus-node:latest key quantus ``` - -This generates a wormhole key pair. Save the `inner_hash` value - this is your preimage for mining. +Replace `quantus-node:v0.0.4` with your desired image. +This command will display your secret phrase, public key, address, and seed. +**Important: Securely back up your secret phrase!** +Next, **copy the displayed `Address`. **Step 4: Run the Validator Node** +Now, run the Docker container with all the necessary parameters: ```bash # If on Apple Silicon, you may need to add --platform linux/amd64 -# Replace YOUR_PREIMAGE with the inner_hash from step 3 docker run -d \ --name quantus-node \ --restart unless-stopped \ @@ -128,7 +120,7 @@ docker run -d \ --base-path /var/lib/quantus \ --chain dirac \ --node-key-file /var/lib/quantus/node_key.p2p \ - --rewards-preimage + --rewards-address ``` *Note for Apple Silicon (M1/M2/M3) users:* As mentioned above, if you are using an `amd64` based Docker image on an ARM-based Mac, you will likely need to add the `--platform linux/amd64` flag to your `docker run` commands. @@ -198,49 +190,6 @@ docker run -d \ - Docker 20.10+ or compatible runtime - All other system requirements same as binary installation -## External Miner Setup - -For high-performance mining, you can offload the QPoW mining process to a separate service, freeing up node resources. - -### Prerequisites - -1. **Build Node:** - ```bash - # From workspace root - cargo build --release -p quantus-node - ``` - -2. **Get External Miner:** - ```bash - git clone https://github.com/Quantus-Network/quantus-miner - cd quantus-miner - cargo build --release - ``` - -### Setup with Wormhole Addresses - -1. **Generate Your Wormhole Address**: - ```bash - ./quantus-node key quantus --scheme wormhole - ``` - Save the `inner_hash` value. - -2. **Start External Miner** (in separate terminal): - ```bash - RUST_LOG=info ./target/release/quantus-miner - ``` - *(Default: `http://127.0.0.1:9833`)* - -3. **Start Node with External Miner** (in another terminal): - ```bash - # Replace with the inner_hash from step 1 - RUST_LOG=info,sc_consensus_pow=debug ./target/release/quantus-node \ - --validator \ - --chain dirac \ - --external-miner-url http://127.0.0.1:9833 \ - --rewards-preimage - ``` - ## Configuration Options ### Node Parameters @@ -248,7 +197,7 @@ For high-performance mining, you can offload the QPoW mining process to a separa | Parameter | Description | Default | |-----------|-------------|---------| | `--node-key-file` | Path to P2P identity file | Required | -| `--rewards-preimage` | Wormhole preimage (inner_hash from key generation) | Required | +| `--rewards-address` | Path to rewards address file | Required | | `--chain` | Chain specification | `dirac` | | `--port` | P2P networking port | `30333` | | `--prometheus-port` | Metrics endpoint port | `9616` | @@ -285,18 +234,14 @@ curl -H "Content-Type: application/json" \ ### Check Mining Rewards -**View Balance at Your Wormhole Address** +**View Balance** ```bash -# Replace YOUR_WORMHOLE_ADDRESS with your wormhole address from key generation +# Replace YOUR_ADDRESS with your rewards address curl -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","id":1,"method":"faucet_getAccountInfo","params":["YOUR_WORMHOLE_ADDRESS"]}' \ + -d '{"jsonrpc":"2.0","id":1,"method":"faucet_getAccountInfo","params":["YOUR_ADDRESS"]}' \ http://localhost:9944 ``` -**Find Your Wormhole Address** -- From key generation: Use the `Address` field from `./quantus-node key quantus --scheme wormhole` -- From node logs: Check startup logs for "Mining rewards will be sent to wormhole address" - ## Testnet Information - **Chain**: Dirac Testnet @@ -323,14 +268,9 @@ quantus-node purge-chain --chain dirac **Mining Not Working** 1. Check that `--validator` flag is present -2. Verify your preimage from `inner_hash` field in key generation +2. Verify rewards address file exists and contains valid address 3. Ensure node is synchronized (check logs for "Imported #XXXX") -**Wormhole Address Issues** -1. **Can't find rewards**: Check the `Address` field from your key generation -2. **Invalid preimage**: Use the exact `inner_hash` value from key generation -3. **Wrong address**: Rewards go to the wormhole address, not the preimage - **Connection Issues** 1. Check firewall settings (allow port 30333) 2. Verify internet connection @@ -362,11 +302,9 @@ curl -H "Content-Type: application/json" \ ## Mining Economics -### Wormhole Address Rewards System +### Rewards Structure -- **Automatic Wormhole Addresses**: All mining rewards go to your wormhole address -- **Privacy by Design**: Your reward address is derived from your preimage -- **Block Rewards**: Earned by successfully mining blocks +- **Block Rewards**: Earned by successfully mining blocks - **Transaction Fees**: Collected from transactions in mined blocks - **Network Incentives**: Additional rewards for network participation @@ -382,9 +320,9 @@ Mining performance depends on: ### Key Management -- **Backup Your Keys**: Securely store your wormhole key pair from key generation -- **Backup Node Keys**: Store copies of your node identity keys safely -- **Secure Storage**: Keep preimages and private keys in encrypted storage +- **Backup Your Keys**: Store copies of your node identity and rewards keys safely +- **Secure Storage**: Keep private keys in encrypted storage +- **Regular Rotation**: Consider rotating keys periodically for enhanced security ### Node Security @@ -403,8 +341,8 @@ This is testnet software for testing purposes only: ## Next Steps 1. **Join the Community**: Connect with other miners and developers -2. **Monitor Performance**: Track your mining efficiency and rewards at your wormhole address -3. **Experiment**: Try different configurations and optimizations +2. **Monitor Performance**: Track your mining efficiency and rewards +3. **Experiment**: Try different configurations and optimizations 4. **Contribute**: Help improve the network by reporting issues and feedback Happy mining! 🚀 diff --git a/README.md b/README.md index 156734c5..ccd9043b 100644 --- a/README.md +++ b/README.md @@ -57,9 +57,17 @@ bip39 wordlist. Seed must be a 64-character hex string -## Mining +--- + +### Rewards address -For complete mining setup instructions, including wormhole address requirements and external miner configuration, see [MINING.md](MINING.md). +By providing the optional `--rewards-address` parameter, the node will start sending mining and transaction rewards +after each block confirmation by the runtime. +If this address is not specified, rewards will not be minted. + +```shell +./quantus-node --chain local --validator --rewards-address +``` ## Local dev run @@ -73,6 +81,44 @@ For complete mining setup instructions, including wormhole address requirements ./target/release/quantus-node --dev ``` +## Run with External Miner + +--- + +This node supports offloading the QPoW mining process to a separate service, freeing up node resources. + +Any service that adheres to the API spec below can be used as miner by the node. We provide a sample implementation in +the 'miner' crate. + +API classes are defined in the 'resonance-miner-api' crate. + +**API Spec: +** [openapi.yaml](https://gitlab.com/resonance-network/backbone/-/blob/b37c4fcdb749ddddc747915b79149e29f537e92f/external-miner/api/openapi.yaml) + +1. **Build Node & Miner:** + ```bash + # From workspace root + cargo build --release -p quantus-node + ``` + +2. **Run External Miner:** (In a separate terminal) + ```bash + git clone https://github.com/Quantus-Network/quantus-miner + cd quantus-miner + cargo build --release + RUST_LOG=info ./target/release/quantus-miner + ``` + *(Listens on `http://127.0.0.1:9833` by default)* + +3. **Run Node:** (In another terminal) + ```bash + # From workspace root (replace ) + RUST_LOG=info,sc_consensus_pow=debug ./target/release/quantus-node \ + --dev \ + --external-miner-url http://127.0.0.1:9833 \ + --rewards-address + ``` + ## Multinode local run --- diff --git a/client/consensus/qpow/src/lib.rs b/client/consensus/qpow/src/lib.rs index 2b5c091b..4a8e36ac 100644 --- a/client/consensus/qpow/src/lib.rs +++ b/client/consensus/qpow/src/lib.rs @@ -7,7 +7,7 @@ use sc_client_api::BlockBackend; use sp_api::ProvideRuntimeApi; use sp_consensus_pow::Seal as RawSeal; use sp_consensus_qpow::QPoWApi; -use sp_runtime::{generic::BlockId, traits::Block as BlockT}; +use sp_runtime::{generic::BlockId, traits::Block as BlockT, AccountId32}; use std::{sync::Arc, time::Duration}; use crate::worker::UntilImportedOrTransaction; @@ -24,7 +24,7 @@ use sp_block_builder::BlockBuilder as BlockBuilderApi; use sp_blockchain::HeaderBackend; use sp_consensus::{Environment, Error as ConsensusError, Proposer, SelectChain, SyncOracle}; use sp_consensus_pow::POW_ENGINE_ID; - +use sp_core::ByteArray; use sp_inherents::{CreateInherentDataProviders, InherentDataProvider}; use sp_runtime::{ generic::{Digest, DigestItem}, @@ -370,7 +370,7 @@ pub fn start_mining_worker( mut env: E, sync_oracle: SO, justification_sync_link: L, - rewards_preimage: [u8; 32], + rewards_address: AccountId32, create_inherent_data_providers: CIDP, tx_notifications: TxStream, build_time: Duration, @@ -479,8 +479,8 @@ where }; let mut inherent_digest = Digest::default(); - let rewards_preimage_bytes = rewards_preimage.to_vec(); - inherent_digest.push(DigestItem::PreRuntime(POW_ENGINE_ID, rewards_preimage_bytes)); + let rewards_address_bytes = rewards_address.clone().as_slice().to_vec(); + inherent_digest.push(DigestItem::PreRuntime(POW_ENGINE_ID, rewards_address_bytes)); let proposer = match env.init(&best_header).await { Ok(x) => x, @@ -513,7 +513,7 @@ where metadata: MiningMetadata { best_hash, pre_hash: proposal.block.header().hash(), - rewards_preimage, + rewards_address: rewards_address.clone(), difficulty, }, proposal, diff --git a/client/consensus/qpow/src/worker.rs b/client/consensus/qpow/src/worker.rs index 15834b47..ca41aa95 100644 --- a/client/consensus/qpow/src/worker.rs +++ b/client/consensus/qpow/src/worker.rs @@ -34,7 +34,7 @@ use sp_consensus_pow::{Seal, POW_ENGINE_ID}; use sp_consensus_qpow::QPoWApi; use sp_runtime::{ traits::{Block as BlockT, Header as HeaderT}, - DigestItem, + AccountId32, DigestItem, }; use std::{ pin::Pin, @@ -52,8 +52,8 @@ pub struct MiningMetadata { pub best_hash: H, /// Mining pre-hash. pub pre_hash: H, - /// Rewards preimage (32 bytes) - stored in block headers, hashed to derive wormhole address. - pub rewards_preimage: [u8; 32], + /// Rewards address. + pub rewards_address: AccountId32, /// Mining target difficulty. pub difficulty: D, } diff --git a/node/Cargo.toml b/node/Cargo.toml index c76647eb..9df4bb9e 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -33,8 +33,6 @@ pallet-transaction-payment-rpc.default-features = true pallet-transaction-payment-rpc.workspace = true prometheus.workspace = true qp-dilithium-crypto = { workspace = true } -qp-poseidon.workspace = true -qp-rusty-crystals-dilithium.workspace = true qp-rusty-crystals-hdwallet.workspace = true qpow-math.workspace = true quantus-miner-api = { workspace = true } @@ -100,6 +98,7 @@ substrate-frame-rpc-system.workspace = true tokio-util.workspace = true [build-dependencies] +qp-wormhole-circuit-builder.workspace = true qp-wormhole-verifier.workspace = true substrate-build-script-utils.default-features = true substrate-build-script-utils.workspace = true diff --git a/node/build.rs b/node/build.rs index 85ea5bd4..2de7de2a 100644 --- a/node/build.rs +++ b/node/build.rs @@ -6,37 +6,35 @@ fn main() { rerun_if_git_head_changed(); - // Validate pre-generated circuit binaries - validate_circuit_binaries(); + // Circuit validation and generation + validate_and_generate_circuits(); } -fn validate_circuit_binaries() { +fn validate_and_generate_circuits() { println!("cargo:rerun-if-changed=pallets/wormhole/verifier.bin"); println!("cargo:rerun-if-changed=pallets/wormhole/common.bin"); - println!("cargo:rerun-if-changed=pallets/wormhole/aggregated_verifier.bin"); - println!("cargo:rerun-if-changed=pallets/wormhole/aggregated_common.bin"); - // Validate the pre-generated wormhole circuit binaries + // Generate circuit binaries from the zk-circuits dependency + generate_circuit_binaries(); + + // Validate generated binaries let verifier_bytes = include_bytes!("../pallets/wormhole/verifier.bin"); let common_bytes = include_bytes!("../pallets/wormhole/common.bin"); - WormholeVerifier::new_from_bytes(verifier_bytes, common_bytes).expect( - "CRITICAL ERROR: Failed to create WormholeVerifier from embedded data. \ - The verifier.bin and common.bin files must be regenerated using qp-zk-circuits \ - with a compatible qp-plonky2 version. Run: \ - cd ../qp-zk-circuits/wormhole/circuit-builder && cargo run", - ); - - // TODO: Re-enable validation once aggregated circuit binaries are regenerated - // with the new qp-plonky2 version that has updated Poseidon2 gates. - // The aggregated circuit binaries were generated with an older qp-plonky2 version - // and have incompatible gate serialization. - // - // let agg_verifier_bytes = include_bytes!("../pallets/wormhole/aggregated_verifier.bin"); - // let agg_common_bytes = include_bytes!("../pallets/wormhole/aggregated_common.bin"); - // WormholeVerifier::new_from_bytes(agg_verifier_bytes, agg_common_bytes).expect( - // "CRITICAL ERROR: Failed to create aggregated WormholeVerifier from embedded data.", - // ); - - println!("cargo:trace=✅ Wormhole circuit binaries validated successfully"); + // This will fail at build time if the binaries are invalid + WormholeVerifier::new_from_bytes(verifier_bytes, common_bytes) + .expect("CRITICAL ERROR: Failed to create WormholeVerifier from embedded data. Check verifier.bin and common.bin"); + + println!("cargo:trace=✅ Wormhole circuit binaries generated and validated successfully"); +} + +fn generate_circuit_binaries() { + println!("cargo:trace=🔧 Generating wormhole circuit binaries from zk-circuits..."); + + // Call the circuit-builder to generate binaries directly in the pallet directory + // We don't need the prover binary for the chain, only verifier and common + qp_wormhole_circuit_builder::generate_circuit_binaries("../pallets/wormhole", false) + .expect("Failed to generate circuit binaries"); + + println!("cargo:trace=✅ Circuit binaries generated successfully"); } diff --git a/node/src/benchmarking.rs b/node/src/benchmarking.rs index 2dd9a8ef..2d3d33eb 100644 --- a/node/src/benchmarking.rs +++ b/node/src/benchmarking.rs @@ -127,9 +127,6 @@ pub fn create_benchmark_extrinsic( quantus_runtime::transaction_extensions::ReversibleTransactionExtension::< runtime::Runtime, >::new(), - quantus_runtime::transaction_extensions::WormholeProofRecorderExtension::< - runtime::Runtime, - >::new(), ); let raw_payload = runtime::SignedPayload::from_raw( @@ -146,7 +143,6 @@ pub fn create_benchmark_extrinsic( (), None, (), - (), ), ); let signature = raw_payload.using_encoded(|e| sender.sign(e)); diff --git a/node/src/cli.rs b/node/src/cli.rs index b18fd9f9..f52ff1d2 100644 --- a/node/src/cli.rs +++ b/node/src/cli.rs @@ -9,9 +9,9 @@ pub struct Cli { #[clap(flatten)] pub run: RunCmd, - /// Specify a rewards preimage for the miner (32-byte hex from wormhole key generation) - #[arg(long, value_name = "REWARDS_PREIMAGE")] - pub rewards_preimage: Option, + /// Specify a rewards address for the miner + #[arg(long, value_name = "REWARDS_ADDRESS")] + pub rewards_address: Option, /// Port to listen for external miner connections (e.g., 9833). /// When set, the node will wait for miners to connect instead of mining locally. diff --git a/node/src/command.rs b/node/src/command.rs index 622c2a66..b44d75ac 100644 --- a/node/src/command.rs +++ b/node/src/command.rs @@ -179,25 +179,21 @@ impl SubstrateCli for Cli { fn load_spec(&self, id: &str) -> Result, String> { Ok(match id { - "dev" => { - Box::new(chain_spec::development_chain_spec()?) as Box - }, - "dirac_live_spec" => { - Box::new(chain_spec::dirac_chain_spec()?) as Box - }, + "dev" => + Box::new(chain_spec::development_chain_spec()?) as Box, + "dirac_live_spec" => + Box::new(chain_spec::dirac_chain_spec()?) as Box, "dirac" => Box::new(chain_spec::ChainSpec::from_json_bytes(include_bytes!( "chain-specs/dirac.json" ))?) as Box, - "heisenberg_live_spec" => { - Box::new(chain_spec::heisenberg_chain_spec()?) as Box - }, + "heisenberg_live_spec" => + Box::new(chain_spec::heisenberg_chain_spec()?) as Box, "" | "heisenberg" => Box::new(chain_spec::ChainSpec::from_json_bytes(include_bytes!( "chain-specs/heisenberg.json" ))?) as Box, - path => { + path => Box::new(chain_spec::ChainSpec::from_json_file(std::path::PathBuf::from(path))?) - as Box - }, + as Box, }) } } @@ -413,9 +409,8 @@ pub fn run() -> sc_cli::Result<()> { cmd.run(client, inherent_benchmark_data()?, Vec::new(), &ext_factory) }, - BenchmarkCmd::Machine(cmd) => { - cmd.run(&config, SUBSTRATE_REFERENCE_HARDWARE.clone()) - }, + BenchmarkCmd::Machine(cmd) => + cmd.run(&config, SUBSTRATE_REFERENCE_HARDWARE.clone()), } }) }, @@ -453,16 +448,16 @@ pub fn run() -> sc_cli::Result<()> { config.network.network_backend = NetworkBackendType::Libp2p; - let rewards_account = match cli.rewards_preimage { + let rewards_account = match cli.rewards_address { Some(address) => { let account = address.parse::().map_err(|_| { - sc_cli::Error::Input("Invalid rewards preimage format".into()) + sc_cli::Error::Input("Invalid rewards address format".into()) })?; log::info!("⛏️ Using address for rewards: {:?}", account); account }, None => { - // Automatically set rewards_preimage to Treasury when --dev is used + // Automatically set rewards_address to Treasury when --dev is used if cli.run.shared_params.is_dev() { let treasury_account = quantus_runtime::configs::TreasuryPalletId::get() @@ -475,9 +470,7 @@ pub fn run() -> sc_cli::Result<()> { treasury_account } else { // Should never happen - return Err(sc_cli::Error::Input( - "No rewards preimage provided".into(), - )); + return Err(sc_cli::Error::Input("No rewards address provided".into())); } }, }; diff --git a/node/src/service.rs b/node/src/service.rs index 3b3aed19..9215b035 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -187,8 +187,8 @@ async fn handle_external_mining( let best_hash = metadata.best_hash; loop { let (miner_id, seal) = match wait_for_mining_result(server, &job_id, || { - cancellation_token.is_cancelled() - || worker_handle.metadata().map(|m| m.best_hash != best_hash).unwrap_or(true) + cancellation_token.is_cancelled() || + worker_handle.metadata().map(|m| m.best_hash != best_hash).unwrap_or(true) }) .await { @@ -437,8 +437,6 @@ fn spawn_authority_tasks( >; // Start the mining worker (block building task) - // Convert AccountId32 to [u8; 32] for the mining worker (rewards preimage) - let rewards_preimage: [u8; 32] = rewards_address.into(); let (worker_handle, worker_task) = sc_consensus_qpow::start_mining_worker( Box::new(pow_block_import), client.clone(), @@ -446,7 +444,7 @@ fn spawn_authority_tasks( proposer, sync_service.clone(), sync_service.clone(), - rewards_preimage, + rewards_address, inherent_data_providers, tx_stream_for_worker, Duration::from_secs(10), diff --git a/pallets/balances/Cargo.toml b/pallets/balances/Cargo.toml new file mode 100644 index 00000000..686b4e18 --- /dev/null +++ b/pallets/balances/Cargo.toml @@ -0,0 +1,65 @@ +[package] +authors.workspace = true +description = "FRAME pallet to manage balances" +edition.workspace = true +homepage.workspace = true +license = "Apache-2.0" +name = "pallet-balances" +readme = "README.md" +repository.workspace = true +version = "40.0.1" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { features = ["derive", "max-encoded-len"], workspace = true } +docify = { workspace = true } +frame-benchmarking = { optional = true, workspace = true } +frame-support.workspace = true +frame-system.workspace = true +log.workspace = true +qp-poseidon = { workspace = true, features = ["serde"] } +qp-wormhole = { workspace = true } +scale-info = { features = ["derive"], workspace = true } +sp-metadata-ir = { workspace = true } +sp-runtime.workspace = true + +[dev-dependencies] +frame-support = { workspace = true, features = ["experimental"], default-features = true } +pallet-transaction-payment.features = ["std"] +pallet-transaction-payment.workspace = true +paste.workspace = true +sp-core.workspace = true +sp-io.workspace = true + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "log/std", + "pallet-transaction-payment/std", + "qp-poseidon/std", + "qp-wormhole/std", + "scale-info/std", + "sp-core/std", + "sp-io/std", + "sp-metadata-ir/std", + "sp-runtime/std", +] +# Enable support for setting the existential deposit to zero. +insecure_zero_ed = [] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/pallets/balances/README.md b/pallets/balances/README.md new file mode 100644 index 00000000..b1e9c82f --- /dev/null +++ b/pallets/balances/README.md @@ -0,0 +1,127 @@ +# Balances Module + +The Balances module provides functionality for handling accounts and balances. + +- [`Config`](https://docs.rs/pallet-balances/latest/pallet_balances/pallet/trait.Config.html) +- [`Call`](https://docs.rs/pallet-balances/latest/pallet_balances/pallet/enum.Call.html) +- [`Pallet`](https://docs.rs/pallet-balances/latest/pallet_balances/pallet/struct.Pallet.html) + +## Overview + +The Balances module provides functions for: + +- Getting and setting free balances. +- Retrieving total, reserved and unreserved balances. +- Repatriating a reserved balance to a beneficiary account that exists. +- Transferring a balance between accounts (when not reserved). +- Slashing an account balance. +- Account creation and removal. +- Managing total issuance. +- Setting and managing locks. + +### Terminology + +- **Existential Deposit:** The minimum balance required to create or keep an account open. This prevents "dust accounts" +from filling storage. When the free plus the reserved balance (i.e. the total balance) fall below this, then the account + is said to be dead; and it loses its functionality as well as any prior history and all information on it is removed + from the chain's state. No account should ever have a total balance that is strictly between 0 and the existential + deposit (exclusive). If this ever happens, it indicates either a bug in this module or an erroneous raw mutation of + storage. + +- **Total Issuance:** The total number of units in existence in a system. + +- **Reaping an account:** The act of removing an account by resetting its nonce. Happens after its total balance has +become zero (or, strictly speaking, less than the Existential Deposit). + +- **Free Balance:** The portion of a balance that is not reserved. The free balance is the only balance that matters for + most operations. + +- **Reserved Balance:** Reserved balance still belongs to the account holder, but is suspended. Reserved balance can + still be slashed, but only after all the free balance has been slashed. + +- **Imbalance:** A condition when some funds were credited or debited without equal and opposite accounting (i.e. a +difference between total issuance and account balances). Functions that result in an imbalance will return an object of +the `Imbalance` trait that can be managed within your runtime logic. (If an imbalance is simply dropped, it should +automatically maintain any book-keeping such as total issuance.) + +- **Lock:** A freeze on a specified amount of an account's free balance until a specified block number. Multiple locks +always operate over the same funds, so they "overlay" rather than "stack". + +### Implementations + +The Balances module provides implementations for the following traits. If these traits provide the functionality that +you need, then you can avoid coupling with the Balances module. + +- [`Currency`](https://docs.rs/frame-support/latest/frame_support/traits/trait.Currency.html): Functions for dealing +with a fungible assets system. +- [`ReservableCurrency`](https://docs.rs/frame-support/latest/frame_support/traits/trait.ReservableCurrency.html): +Functions for dealing with assets that can be reserved from an account. +- [`LockableCurrency`](https://docs.rs/frame-support/latest/frame_support/traits/trait.LockableCurrency.html): Functions +for dealing with accounts that allow liquidity restrictions. +- [`Imbalance`](https://docs.rs/frame-support/latest/frame_support/traits/trait.Imbalance.html): Functions for handling +imbalances between total issuance in the system and account balances. Must be used when a function creates new funds +(e.g. a reward) or destroys some funds (e.g. a system fee). +- [`IsDeadAccount`](https://docs.rs/frame-support/latest/frame_support/traits/trait.IsDeadAccount.html): Determiner to +say whether a given account is unused. + +## Interface + +### Dispatchable Functions + +- `transfer` - Transfer some liquid free balance to another account. +- `force_set_balance` - Set the balances of a given account. The origin of this call must be root. + +## Usage + +The following examples show how to use the Balances module in your custom module. + +### Examples from the FRAME + +The Contract module uses the `Currency` trait to handle gas payment, and its types inherit from `Currency`: + +```rust +use frame_support::traits::Currency; + +pub type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; +pub type NegativeImbalanceOf = <::Currency as Currency<::AccountId>>::NegativeImbalance; + +``` + +The Staking module uses the `LockableCurrency` trait to lock a stash account's funds: + +```rust +use frame_support::traits::{WithdrawReasons, LockableCurrency}; +use sp_runtime::traits::Bounded; +pub trait Config: frame_system::Config { + type Currency: LockableCurrency>; +} + +fn update_ledger( + controller: &T::AccountId, + ledger: &StakingLedger +) { + T::Currency::set_lock( + STAKING_ID, + &ledger.stash, + ledger.total, + WithdrawReasons::all() + ); + // >::insert(controller, ledger); // Commented out as we don't have access to Staking's storage here. +} +``` + +## Genesis config + +The Balances module depends on the +[`GenesisConfig`](https://docs.rs/pallet-balances/latest/pallet_balances/pallet/struct.GenesisConfig.html). + +## Assumptions + +- Total issued balanced of all accounts should be less than `Config::Balance::max_value()`. + +License: Apache-2.0 + + +## Release + +Polkadot SDK Stable 2412 diff --git a/pallets/balances/src/benchmarking.rs b/pallets/balances/src/benchmarking.rs new file mode 100644 index 00000000..06f3d24d --- /dev/null +++ b/pallets/balances/src/benchmarking.rs @@ -0,0 +1,350 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Balances pallet benchmarking. + +#![cfg(feature = "runtime-benchmarks")] + +use super::*; +use crate::Pallet as Balances; + +use frame_benchmarking::v2::*; +use frame_system::RawOrigin; +use sp_runtime::traits::Bounded; +use types::ExtraFlags; + +const SEED: u32 = 0; +// existential deposit multiplier +const ED_MULTIPLIER: u32 = 10; + +fn minimum_balance, I: 'static>() -> T::Balance { + if cfg!(feature = "insecure_zero_ed") { + 100u32.into() + } else { + T::ExistentialDeposit::get() + } +} + +#[instance_benchmarks] +mod benchmarks { + use super::*; + + // Benchmark `transfer` extrinsic with the worst possible conditions: + // * Transfer will kill the sender account. + // * Transfer will create the recipient account. + #[benchmark] + fn transfer_allow_death() { + let existential_deposit: T::Balance = minimum_balance::(); + let caller = whitelisted_caller(); + + // Give some multiple of the existential deposit + let balance = existential_deposit.saturating_mul(ED_MULTIPLIER.into()).max(1u32.into()); + let _ = as Currency<_>>::make_free_balance_be(&caller, balance); + + // Transfer `e - 1` existential deposits + 1 unit, which guarantees to create one account, + // and reap this user. + let recipient: T::AccountId = account("recipient", 0, SEED); + let recipient_lookup = T::Lookup::unlookup(recipient.clone()); + let transfer_amount = + existential_deposit.saturating_mul((ED_MULTIPLIER - 1).into()) + 1u32.into(); + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), recipient_lookup, transfer_amount); + + assert_eq!(Balances::::free_balance(&caller), Zero::zero()); + assert_eq!(Balances::::free_balance(&recipient), transfer_amount); + } + + // Benchmark `transfer` with the best possible condition: + // * Both accounts exist and will continue to exist. + #[benchmark(extra)] + fn transfer_best_case() { + let caller = whitelisted_caller(); + let recipient: T::AccountId = account("recipient", 0, SEED); + let recipient_lookup = T::Lookup::unlookup(recipient.clone()); + + // Give the sender account max funds for transfer (their account will never reasonably be + // killed). + let _ = + as Currency<_>>::make_free_balance_be(&caller, T::Balance::max_value()); + + // Give the recipient account existential deposit (thus their account already exists). + let existential_deposit: T::Balance = minimum_balance::(); + let _ = + as Currency<_>>::make_free_balance_be(&recipient, existential_deposit); + let transfer_amount = existential_deposit.saturating_mul(ED_MULTIPLIER.into()); + + #[extrinsic_call] + transfer_allow_death(RawOrigin::Signed(caller.clone()), recipient_lookup, transfer_amount); + + assert!(!Balances::::free_balance(&caller).is_zero()); + assert!(!Balances::::free_balance(&recipient).is_zero()); + } + + // Benchmark `transfer_keep_alive` with the worst possible condition: + // * The recipient account is created. + #[benchmark] + fn transfer_keep_alive() { + let caller = whitelisted_caller(); + let recipient: T::AccountId = account("recipient", 0, SEED); + let recipient_lookup = T::Lookup::unlookup(recipient.clone()); + + // Give the sender account max funds, thus a transfer will not kill account. + let _ = + as Currency<_>>::make_free_balance_be(&caller, T::Balance::max_value()); + let existential_deposit: T::Balance = minimum_balance::(); + let transfer_amount = existential_deposit.saturating_mul(ED_MULTIPLIER.into()); + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), recipient_lookup, transfer_amount); + + assert!(!Balances::::free_balance(&caller).is_zero()); + assert_eq!(Balances::::free_balance(&recipient), transfer_amount); + } + + // Benchmark `force_set_balance` coming from ROOT account. This always creates an account. + #[benchmark] + fn force_set_balance_creating() { + let user: T::AccountId = account("user", 0, SEED); + let user_lookup = T::Lookup::unlookup(user.clone()); + + // Give the user some initial balance. + let existential_deposit: T::Balance = minimum_balance::(); + let balance_amount = existential_deposit.saturating_mul(ED_MULTIPLIER.into()); + let _ = as Currency<_>>::make_free_balance_be(&user, balance_amount); + + #[extrinsic_call] + force_set_balance(RawOrigin::Root, user_lookup, balance_amount); + + assert_eq!(Balances::::free_balance(&user), balance_amount); + } + + // Benchmark `force_set_balance` coming from ROOT account. This always kills an account. + #[benchmark] + fn force_set_balance_killing() { + let user: T::AccountId = account("user", 0, SEED); + let user_lookup = T::Lookup::unlookup(user.clone()); + + // Give the user some initial balance. + let existential_deposit: T::Balance = minimum_balance::(); + let balance_amount = existential_deposit.saturating_mul(ED_MULTIPLIER.into()); + let _ = as Currency<_>>::make_free_balance_be(&user, balance_amount); + + #[extrinsic_call] + force_set_balance(RawOrigin::Root, user_lookup, Zero::zero()); + + assert!(Balances::::free_balance(&user).is_zero()); + } + + // Benchmark `force_transfer` extrinsic with the worst possible conditions: + // * Transfer will kill the sender account. + // * Transfer will create the recipient account. + #[benchmark] + fn force_transfer() { + let existential_deposit: T::Balance = minimum_balance::(); + let source: T::AccountId = account("source", 0, SEED); + let source_lookup = T::Lookup::unlookup(source.clone()); + + // Give some multiple of the existential deposit + let balance = existential_deposit.saturating_mul(ED_MULTIPLIER.into()); + let _ = as Currency<_>>::make_free_balance_be(&source, balance); + + // Transfer `e - 1` existential deposits + 1 unit, which guarantees to create one account, + // and reap this user. + let recipient: T::AccountId = account("recipient", 0, SEED); + let recipient_lookup = T::Lookup::unlookup(recipient.clone()); + let transfer_amount = + existential_deposit.saturating_mul((ED_MULTIPLIER - 1).into()) + 1u32.into(); + + #[extrinsic_call] + _(RawOrigin::Root, source_lookup, recipient_lookup, transfer_amount); + + assert_eq!(Balances::::free_balance(&source), Zero::zero()); + assert_eq!(Balances::::free_balance(&recipient), transfer_amount); + } + + // This benchmark performs the same operation as `transfer` in the worst case scenario, + // but additionally introduces many new users into the storage, increasing the the merkle + // trie and PoV size. + #[benchmark(extra)] + fn transfer_increasing_users(u: Linear<0, 1_000>) { + // 1_000 is not very much, but this upper bound can be controlled by the CLI. + let existential_deposit: T::Balance = minimum_balance::(); + let caller = whitelisted_caller(); + + // Give some multiple of the existential deposit + let balance = existential_deposit.saturating_mul(ED_MULTIPLIER.into()); + let _ = as Currency<_>>::make_free_balance_be(&caller, balance); + + // Transfer `e - 1` existential deposits + 1 unit, which guarantees to create one account, + // and reap this user. + let recipient: T::AccountId = account("recipient", 0, SEED); + let recipient_lookup = T::Lookup::unlookup(recipient.clone()); + let transfer_amount = + existential_deposit.saturating_mul((ED_MULTIPLIER - 1).into()) + 1u32.into(); + + // Create a bunch of users in storage. + for i in 0..u { + // The `account` function uses `blake2_256` to generate unique accounts, so these + // should be quite random and evenly distributed in the trie. + let new_user: T::AccountId = account("new_user", i, SEED); + let _ = as Currency<_>>::make_free_balance_be(&new_user, balance); + } + + #[extrinsic_call] + transfer_allow_death(RawOrigin::Signed(caller.clone()), recipient_lookup, transfer_amount); + + assert_eq!(Balances::::free_balance(&caller), Zero::zero()); + assert_eq!(Balances::::free_balance(&recipient), transfer_amount); + } + + // Benchmark `transfer_all` with the worst possible condition: + // * The recipient account is created + // * The sender is killed + #[benchmark] + fn transfer_all() { + let caller = whitelisted_caller(); + let recipient: T::AccountId = account("recipient", 0, SEED); + let recipient_lookup = T::Lookup::unlookup(recipient.clone()); + + // Give some multiple of the existential deposit + let existential_deposit: T::Balance = minimum_balance::(); + let balance = existential_deposit.saturating_mul(ED_MULTIPLIER.into()); + let _ = as Currency<_>>::make_free_balance_be(&caller, balance); + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), recipient_lookup, false); + + assert!(Balances::::free_balance(&caller).is_zero()); + assert_eq!(Balances::::free_balance(&recipient), balance); + } + + #[benchmark] + fn force_unreserve() -> Result<(), BenchmarkError> { + let user: T::AccountId = account("user", 0, SEED); + let user_lookup = T::Lookup::unlookup(user.clone()); + + // Give some multiple of the existential deposit + let ed = minimum_balance::(); + let balance = ed + ed; + let _ = as Currency<_>>::make_free_balance_be(&user, balance); + + // Reserve the balance + as ReservableCurrency<_>>::reserve(&user, ed)?; + assert_eq!(Balances::::reserved_balance(&user), ed); + assert_eq!(Balances::::free_balance(&user), ed); + + #[extrinsic_call] + _(RawOrigin::Root, user_lookup, balance); + + assert!(Balances::::reserved_balance(&user).is_zero()); + assert_eq!(Balances::::free_balance(&user), ed + ed); + + Ok(()) + } + + #[benchmark] + fn upgrade_accounts(u: Linear<1, 1_000>) { + let caller: T::AccountId = whitelisted_caller(); + let who = (0..u) + .map(|i| -> T::AccountId { + let user = account("old_user", i, SEED); + let account = AccountData { + free: minimum_balance::(), + reserved: minimum_balance::(), + frozen: Zero::zero(), + flags: ExtraFlags::old_logic(), + }; + frame_system::Pallet::::inc_providers(&user); + assert!(T::AccountStore::try_mutate_exists(&user, |a| -> DispatchResult { + *a = Some(account); + Ok(()) + }) + .is_ok()); + assert!(!Balances::::account(&user).flags.is_new_logic()); + assert_eq!(frame_system::Pallet::::providers(&user), 1); + assert_eq!(frame_system::Pallet::::consumers(&user), 0); + user + }) + .collect(); + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), who); + + for i in 0..u { + let user: T::AccountId = account("old_user", i, SEED); + assert!(Balances::::account(&user).flags.is_new_logic()); + assert_eq!(frame_system::Pallet::::providers(&user), 1); + assert_eq!(frame_system::Pallet::::consumers(&user), 1); + } + } + + #[benchmark] + fn force_adjust_total_issuance() { + let ti = TotalIssuance::::get(); + let delta = 123u32.into(); + + #[extrinsic_call] + _(RawOrigin::Root, AdjustmentDirection::Increase, delta); + + assert_eq!(TotalIssuance::::get(), ti + delta); + } + + /// Benchmark `burn` extrinsic with the worst possible condition - burn kills the account. + #[benchmark] + fn burn_allow_death() { + let existential_deposit = T::ExistentialDeposit::get(); + let caller = whitelisted_caller(); + + // Give some multiple of the existential deposit + let balance = existential_deposit.saturating_mul(ED_MULTIPLIER.into()); + let _ = as Currency<_>>::make_free_balance_be(&caller, balance); + + // Burn enough to kill the account. + let burn_amount = balance - existential_deposit + 1u32.into(); + + #[extrinsic_call] + burn(RawOrigin::Signed(caller.clone()), burn_amount, false); + + assert_eq!(Balances::::free_balance(&caller), Zero::zero()); + } + + // Benchmark `burn` extrinsic with the case where account is kept alive. + #[benchmark] + fn burn_keep_alive() { + let existential_deposit = T::ExistentialDeposit::get(); + let caller = whitelisted_caller(); + + // Give some multiple of the existential deposit + let balance = existential_deposit.saturating_mul(ED_MULTIPLIER.into()); + let _ = as Currency<_>>::make_free_balance_be(&caller, balance); + + // Burn minimum possible amount which should not kill the account. + let burn_amount = 1u32.into(); + + #[extrinsic_call] + burn(RawOrigin::Signed(caller.clone()), burn_amount, true); + + assert_eq!(Balances::::free_balance(&caller), balance - burn_amount); + } + + impl_benchmark_test_suite! { + Balances, + crate::tests::ExtBuilder::default().build(), + crate::tests::Test, + } +} diff --git a/pallets/balances/src/impl_currency.rs b/pallets/balances/src/impl_currency.rs new file mode 100644 index 00000000..547cca75 --- /dev/null +++ b/pallets/balances/src/impl_currency.rs @@ -0,0 +1,952 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Implementations for the `Currency` family of traits. +//! +//! Note that `WithdrawReasons` are intentionally not used for anything in this implementation and +//! are expected to be removed in the near future, once migration to `fungible::*` traits is done. + +use super::*; +use core::cmp::Ordering; +use frame_support::{ + ensure, + pallet_prelude::DispatchResult, + traits::{ + tokens::{ + fungible, Balance, BalanceStatus as Status, Fortitude::Polite, Precision::BestEffort, + }, + Currency, DefensiveSaturating, + ExistenceRequirement::{self, AllowDeath}, + Get, Imbalance, InspectLockableCurrency, LockIdentifier, LockableCurrency, + NamedReservableCurrency, ReservableCurrency, SignedImbalance, TryDrop, WithdrawReasons, + }, +}; +use frame_system::pallet_prelude::BlockNumberFor; +pub use imbalances::{NegativeImbalance, PositiveImbalance}; +use sp_runtime::traits::Bounded; + +// wrapping these imbalances in a private module is necessary to ensure absolute privacy +// of the inner member. +mod imbalances { + use super::*; + use core::mem; + use frame_support::traits::{tokens::imbalance::TryMerge, SameOrOther}; + + /// Opaque, move-only struct with private fields that serves as a token denoting that + /// funds have been created without any equal and opposite accounting. + #[must_use] + #[derive(RuntimeDebug, PartialEq, Eq)] + pub struct PositiveImbalance, I: 'static = ()>(T::Balance); + + impl, I: 'static> PositiveImbalance { + /// Create a new positive imbalance from a balance. + pub fn new(amount: T::Balance) -> Self { + PositiveImbalance(amount) + } + } + + /// Opaque, move-only struct with private fields that serves as a token denoting that + /// funds have been destroyed without any equal and opposite accounting. + #[must_use] + #[derive(RuntimeDebug, PartialEq, Eq)] + pub struct NegativeImbalance, I: 'static = ()>(T::Balance); + + impl, I: 'static> NegativeImbalance { + /// Create a new negative imbalance from a balance. + pub fn new(amount: T::Balance) -> Self { + NegativeImbalance(amount) + } + } + + impl, I: 'static> TryDrop for PositiveImbalance { + fn try_drop(self) -> result::Result<(), Self> { + self.drop_zero() + } + } + + impl, I: 'static> Default for PositiveImbalance { + fn default() -> Self { + Self::zero() + } + } + + impl, I: 'static> Imbalance for PositiveImbalance { + type Opposite = NegativeImbalance; + + fn zero() -> Self { + Self(Zero::zero()) + } + fn drop_zero(self) -> result::Result<(), Self> { + if self.0.is_zero() { + Ok(()) + } else { + Err(self) + } + } + fn split(self, amount: T::Balance) -> (Self, Self) { + let first = self.0.min(amount); + let second = self.0 - first; + + mem::forget(self); + (Self(first), Self(second)) + } + fn extract(&mut self, amount: T::Balance) -> Self { + let new = self.0.min(amount); + self.0 -= new; + Self(new) + } + fn merge(mut self, other: Self) -> Self { + self.0 = self.0.saturating_add(other.0); + mem::forget(other); + + self + } + fn subsume(&mut self, other: Self) { + self.0 = self.0.saturating_add(other.0); + mem::forget(other); + } + fn offset(self, other: Self::Opposite) -> SameOrOther { + let (a, b) = (self.0, other.0); + mem::forget((self, other)); + + match a.cmp(&b) { + Ordering::Greater => SameOrOther::Same(Self(a - b)), + Ordering::Less => SameOrOther::Other(NegativeImbalance::new(b - a)), + Ordering::Equal => SameOrOther::None, + } + } + fn peek(&self) -> T::Balance { + self.0 + } + } + + impl, I: 'static> TryMerge for PositiveImbalance { + fn try_merge(self, other: Self) -> Result { + Ok(self.merge(other)) + } + } + + impl, I: 'static> TryDrop for NegativeImbalance { + fn try_drop(self) -> result::Result<(), Self> { + self.drop_zero() + } + } + + impl, I: 'static> Default for NegativeImbalance { + fn default() -> Self { + Self::zero() + } + } + + impl, I: 'static> Imbalance for NegativeImbalance { + type Opposite = PositiveImbalance; + + fn zero() -> Self { + Self(Zero::zero()) + } + fn drop_zero(self) -> result::Result<(), Self> { + if self.0.is_zero() { + Ok(()) + } else { + Err(self) + } + } + fn split(self, amount: T::Balance) -> (Self, Self) { + let first = self.0.min(amount); + let second = self.0 - first; + + mem::forget(self); + (Self(first), Self(second)) + } + fn extract(&mut self, amount: T::Balance) -> Self { + let new = self.0.min(amount); + self.0 -= new; + Self(new) + } + fn merge(mut self, other: Self) -> Self { + self.0 = self.0.saturating_add(other.0); + mem::forget(other); + + self + } + fn subsume(&mut self, other: Self) { + self.0 = self.0.saturating_add(other.0); + mem::forget(other); + } + fn offset(self, other: Self::Opposite) -> SameOrOther { + let (a, b) = (self.0, other.0); + mem::forget((self, other)); + + match a.cmp(&b) { + Ordering::Greater => SameOrOther::Same(Self(a - b)), + Ordering::Less => SameOrOther::Other(PositiveImbalance::new(b - a)), + Ordering::Equal => SameOrOther::None, + } + } + fn peek(&self) -> T::Balance { + self.0 + } + } + + impl, I: 'static> TryMerge for NegativeImbalance { + fn try_merge(self, other: Self) -> Result { + Ok(self.merge(other)) + } + } + + impl, I: 'static> Drop for PositiveImbalance { + /// Basic drop handler will just square up the total issuance. + fn drop(&mut self) { + if !self.0.is_zero() { + >::mutate(|v| *v = v.saturating_add(self.0)); + Pallet::::deposit_event(Event::::Issued { amount: self.0 }); + } + } + } + + impl, I: 'static> Drop for NegativeImbalance { + /// Basic drop handler will just square up the total issuance. + fn drop(&mut self) { + if !self.0.is_zero() { + >::mutate(|v| *v = v.saturating_sub(self.0)); + Pallet::::deposit_event(Event::::Rescinded { amount: self.0 }); + } + } + } +} + +impl, I: 'static> Currency for Pallet +where + T::Balance: Balance, +{ + type Balance = T::Balance; + type PositiveImbalance = PositiveImbalance; + type NegativeImbalance = NegativeImbalance; + + fn total_balance(who: &T::AccountId) -> Self::Balance { + Self::account(who).total() + } + + // Check if `value` amount of free balance can be slashed from `who`. + fn can_slash(who: &T::AccountId, value: Self::Balance) -> bool { + if value.is_zero() { + return true; + } + Self::free_balance(who) >= value + } + + fn total_issuance() -> Self::Balance { + TotalIssuance::::get() + } + + fn active_issuance() -> Self::Balance { + >::active_issuance() + } + + fn deactivate(amount: Self::Balance) { + >::deactivate(amount); + } + + fn reactivate(amount: Self::Balance) { + >::reactivate(amount); + } + + fn minimum_balance() -> Self::Balance { + T::ExistentialDeposit::get() + } + + // Burn funds from the total issuance, returning a positive imbalance for the amount burned. + // Is a no-op if amount to be burned is zero. + fn burn(mut amount: Self::Balance) -> Self::PositiveImbalance { + if amount.is_zero() { + return PositiveImbalance::zero(); + } + >::mutate(|issued| { + *issued = issued.checked_sub(&amount).unwrap_or_else(|| { + amount = *issued; + Zero::zero() + }); + }); + + Pallet::::deposit_event(Event::::Rescinded { amount }); + PositiveImbalance::new(amount) + } + + // Create new funds into the total issuance, returning a negative imbalance + // for the amount issued. + // Is a no-op if amount to be issued it zero. + fn issue(mut amount: Self::Balance) -> Self::NegativeImbalance { + if amount.is_zero() { + return NegativeImbalance::zero(); + } + >::mutate(|issued| { + *issued = issued.checked_add(&amount).unwrap_or_else(|| { + amount = Self::Balance::max_value() - *issued; + Self::Balance::max_value() + }) + }); + + Pallet::::deposit_event(Event::::Issued { amount }); + NegativeImbalance::new(amount) + } + + fn free_balance(who: &T::AccountId) -> Self::Balance { + Self::account(who).free + } + + // Ensure that an account can withdraw from their free balance given any existing withdrawal + // restrictions like locks and vesting balance. + // Is a no-op if amount to be withdrawn is zero. + fn ensure_can_withdraw( + who: &T::AccountId, + amount: T::Balance, + _reasons: WithdrawReasons, + new_balance: T::Balance, + ) -> DispatchResult { + if amount.is_zero() { + return Ok(()); + } + ensure!(new_balance >= Self::account(who).frozen, Error::::LiquidityRestrictions); + Ok(()) + } + + // Transfer some free balance from `transactor` to `dest`, respecting existence requirements. + // Is a no-op if value to be transferred is zero or the `transactor` is the same as `dest`. + fn transfer( + transactor: &T::AccountId, + dest: &T::AccountId, + value: Self::Balance, + existence_requirement: ExistenceRequirement, + ) -> DispatchResult { + if value.is_zero() || transactor == dest { + return Ok(()); + } + let keep_alive = match existence_requirement { + ExistenceRequirement::KeepAlive => Preserve, + ExistenceRequirement::AllowDeath => Expendable, + }; + >::transfer(transactor, dest, value, keep_alive)?; + Ok(()) + } + + /// Slash a target account `who`, returning the negative imbalance created and any left over + /// amount that could not be slashed. + /// + /// Is a no-op if `value` to be slashed is zero or the account does not exist. + /// + /// NOTE: `slash()` prefers free balance, but assumes that reserve balance can be drawn + /// from in extreme circumstances. `can_slash()` should be used prior to `slash()` to avoid + /// having to draw from reserved funds, however we err on the side of punishment if things are + /// inconsistent or `can_slash` wasn't used appropriately. + fn slash(who: &T::AccountId, value: Self::Balance) -> (Self::NegativeImbalance, Self::Balance) { + if value.is_zero() { + return (NegativeImbalance::zero(), Zero::zero()); + } + if Self::total_balance(who).is_zero() { + return (NegativeImbalance::zero(), value); + } + + match Self::try_mutate_account_handling_dust( + who, + |account, _is_new| -> Result<(Self::NegativeImbalance, Self::Balance), DispatchError> { + // Best value is the most amount we can slash following liveness rules. + let ed = T::ExistentialDeposit::get(); + let actual = match system::Pallet::::can_dec_provider(who) { + true => value.min(account.free), + false => value.min(account.free.saturating_sub(ed)), + }; + account.free.saturating_reduce(actual); + let remaining = value.saturating_sub(actual); + Ok((NegativeImbalance::new(actual), remaining)) + }, + ) { + Ok((imbalance, remaining)) => { + Self::deposit_event(Event::Slashed { + who: who.clone(), + amount: value.saturating_sub(remaining), + }); + (imbalance, remaining) + }, + Err(_) => (Self::NegativeImbalance::zero(), value), + } + } + + /// Deposit some `value` into the free balance of an existing target account `who`. + /// + /// Is a no-op if the `value` to be deposited is zero. + fn deposit_into_existing( + who: &T::AccountId, + value: Self::Balance, + ) -> Result { + if value.is_zero() { + return Ok(PositiveImbalance::zero()); + } + + Self::try_mutate_account_handling_dust( + who, + |account, is_new| -> Result { + ensure!(!is_new, Error::::DeadAccount); + account.free = account.free.checked_add(&value).ok_or(ArithmeticError::Overflow)?; + Self::deposit_event(Event::Deposit { who: who.clone(), amount: value }); + Ok(PositiveImbalance::new(value)) + }, + ) + } + + /// Deposit some `value` into the free balance of `who`, possibly creating a new account. + /// + /// This function is a no-op if: + /// - the `value` to be deposited is zero; or + /// - the `value` to be deposited is less than the required ED and the account does not yet + /// exist; or + /// - the deposit would necessitate the account to exist and there are no provider references; + /// or + /// - `value` is so large it would cause the balance of `who` to overflow. + fn deposit_creating(who: &T::AccountId, value: Self::Balance) -> Self::PositiveImbalance { + if value.is_zero() { + return Self::PositiveImbalance::zero(); + } + + Self::try_mutate_account_handling_dust( + who, + |account, is_new| -> Result { + let ed = T::ExistentialDeposit::get(); + ensure!(value >= ed || !is_new, Error::::ExistentialDeposit); + + // defensive only: overflow should never happen, however in case it does, then this + // operation is a no-op. + account.free = match account.free.checked_add(&value) { + Some(x) => x, + None => return Ok(Self::PositiveImbalance::zero()), + }; + + Self::deposit_event(Event::Deposit { who: who.clone(), amount: value }); + Ok(PositiveImbalance::new(value)) + }, + ) + .unwrap_or_else(|_| Self::PositiveImbalance::zero()) + } + + /// Withdraw some free balance from an account, respecting existence requirements. + /// + /// Is a no-op if value to be withdrawn is zero. + fn withdraw( + who: &T::AccountId, + value: Self::Balance, + reasons: WithdrawReasons, + liveness: ExistenceRequirement, + ) -> result::Result { + if value.is_zero() { + return Ok(NegativeImbalance::zero()); + } + + Self::try_mutate_account_handling_dust( + who, + |account, _| -> Result { + let new_free_account = + account.free.checked_sub(&value).ok_or(Error::::InsufficientBalance)?; + + // bail if we need to keep the account alive and this would kill it. + let ed = T::ExistentialDeposit::get(); + let would_be_dead = new_free_account < ed; + let would_kill = would_be_dead && account.free >= ed; + ensure!(liveness == AllowDeath || !would_kill, Error::::Expendability); + + Self::ensure_can_withdraw(who, value, reasons, new_free_account)?; + + account.free = new_free_account; + + Self::deposit_event(Event::Withdraw { who: who.clone(), amount: value }); + Ok(NegativeImbalance::new(value)) + }, + ) + } + + /// Force the new free balance of a target account `who` to some new value `balance`. + fn make_free_balance_be( + who: &T::AccountId, + value: Self::Balance, + ) -> SignedImbalance { + Self::try_mutate_account_handling_dust( + who, + |account, + is_new| + -> Result, DispatchError> { + let ed = T::ExistentialDeposit::get(); + // If we're attempting to set an existing account to less than ED, then + // bypass the entire operation. It's a no-op if you follow it through, but + // since this is an instance where we might account for a negative imbalance + // (in the dust cleaner of set_account) before we account for its actual + // equal and opposite cause (returned as an Imbalance), then in the + // instance that there's no other accounts on the system at all, we might + // underflow the issuance and our arithmetic will be off. + ensure!(value >= ed || !is_new, Error::::ExistentialDeposit); + + let imbalance = if account.free <= value { + SignedImbalance::Positive(PositiveImbalance::new(value - account.free)) + } else { + SignedImbalance::Negative(NegativeImbalance::new(account.free - value)) + }; + account.free = value; + Self::deposit_event(Event::BalanceSet { who: who.clone(), free: account.free }); + Ok(imbalance) + }, + ) + .unwrap_or_else(|_| SignedImbalance::Positive(Self::PositiveImbalance::zero())) + } +} + +impl, I: 'static> ReservableCurrency for Pallet +where + T::Balance: Balance, +{ + /// Check if `who` can reserve `value` from their free balance. + /// + /// Always `true` if value to be reserved is zero. + fn can_reserve(who: &T::AccountId, value: Self::Balance) -> bool { + if value.is_zero() { + return true; + } + Self::account(who).free.checked_sub(&value).is_some_and(|new_balance| { + new_balance >= T::ExistentialDeposit::get() && + Self::ensure_can_withdraw(who, value, WithdrawReasons::RESERVE, new_balance) + .is_ok() + }) + } + + fn reserved_balance(who: &T::AccountId) -> Self::Balance { + Self::account(who).reserved + } + + /// Move `value` from the free balance from `who` to their reserved balance. + /// + /// Is a no-op if value to be reserved is zero. + fn reserve(who: &T::AccountId, value: Self::Balance) -> DispatchResult { + if value.is_zero() { + return Ok(()); + } + + Self::try_mutate_account_handling_dust(who, |account, _| -> DispatchResult { + account.free = + account.free.checked_sub(&value).ok_or(Error::::InsufficientBalance)?; + account.reserved = + account.reserved.checked_add(&value).ok_or(ArithmeticError::Overflow)?; + Self::ensure_can_withdraw(who, value, WithdrawReasons::RESERVE, account.free) + })?; + + Self::deposit_event(Event::Reserved { who: who.clone(), amount: value }); + Ok(()) + } + + /// Unreserve some funds, returning any amount that was unable to be unreserved. + /// + /// Is a no-op if the value to be unreserved is zero or the account does not exist. + /// + /// NOTE: returns amount value which wasn't successfully unreserved. + fn unreserve(who: &T::AccountId, value: Self::Balance) -> Self::Balance { + if value.is_zero() { + return Zero::zero(); + } + if Self::total_balance(who).is_zero() { + return value; + } + + let actual = match Self::mutate_account_handling_dust(who, |account| { + let actual = cmp::min(account.reserved, value); + account.reserved -= actual; + // defensive only: this can never fail since total issuance which is at least + // free+reserved fits into the same data type. + account.free = account.free.defensive_saturating_add(actual); + actual + }) { + Ok(x) => x, + Err(_) => { + // This should never happen since we don't alter the total amount in the account. + // If it ever does, then we should fail gracefully though, indicating that nothing + // could be done. + return value; + }, + }; + + Self::deposit_event(Event::Unreserved { who: who.clone(), amount: actual }); + value - actual + } + + /// Slash from reserved balance, returning the negative imbalance created, + /// and any amount that was unable to be slashed. + /// + /// Is a no-op if the value to be slashed is zero or the account does not exist. + fn slash_reserved( + who: &T::AccountId, + value: Self::Balance, + ) -> (Self::NegativeImbalance, Self::Balance) { + if value.is_zero() { + return (NegativeImbalance::zero(), Zero::zero()); + } + if Self::total_balance(who).is_zero() { + return (NegativeImbalance::zero(), value); + } + + // NOTE: `mutate_account` may fail if it attempts to reduce the balance to the point that an + // account is attempted to be illegally destroyed. + + match Self::mutate_account_handling_dust(who, |account| { + let actual = value.min(account.reserved); + account.reserved.saturating_reduce(actual); + + // underflow should never happen, but it if does, there's nothing to be done here. + (NegativeImbalance::new(actual), value.saturating_sub(actual)) + }) { + Ok((imbalance, not_slashed)) => { + Self::deposit_event(Event::Slashed { + who: who.clone(), + amount: value.saturating_sub(not_slashed), + }); + (imbalance, not_slashed) + }, + Err(_) => (Self::NegativeImbalance::zero(), value), + } + } + + /// Move the reserved balance of one account into the balance of another, according to `status`. + /// + /// Is a no-op if: + /// - the value to be moved is zero; or + /// - the `slashed` id equal to `beneficiary` and the `status` is `Reserved`. + /// + /// This is `Polite` and thus will not repatriate any funds which would lead the total balance + /// to be less than the frozen amount. Returns `Ok` with the actual amount of funds moved, + /// which may be less than `value` since the operation is done an a `BestEffort` basis. + fn repatriate_reserved( + slashed: &T::AccountId, + beneficiary: &T::AccountId, + value: Self::Balance, + status: Status, + ) -> Result { + let actual = + Self::do_transfer_reserved(slashed, beneficiary, value, BestEffort, Polite, status)?; + Ok(value.saturating_sub(actual)) + } +} + +impl, I: 'static> NamedReservableCurrency for Pallet +where + T::Balance: Balance, +{ + type ReserveIdentifier = T::ReserveIdentifier; + + fn reserved_balance_named(id: &Self::ReserveIdentifier, who: &T::AccountId) -> Self::Balance { + let reserves = Self::reserves(who); + reserves + .binary_search_by_key(id, |data| data.id) + .map(|index| reserves[index].amount) + .unwrap_or_default() + } + + /// Move `value` from the free balance from `who` to a named reserve balance. + /// + /// Is a no-op if value to be reserved is zero. + fn reserve_named( + id: &Self::ReserveIdentifier, + who: &T::AccountId, + value: Self::Balance, + ) -> DispatchResult { + if value.is_zero() { + return Ok(()); + } + + Reserves::::try_mutate(who, |reserves| -> DispatchResult { + match reserves.binary_search_by_key(id, |data| data.id) { + Ok(index) => { + reserves[index].amount = reserves[index] + .amount + .checked_add(&value) + .ok_or(ArithmeticError::Overflow)?; + }, + Err(index) => { + reserves + .try_insert(index, ReserveData { id: *id, amount: value }) + .map_err(|_| Error::::TooManyReserves)?; + }, + }; + >::reserve(who, value)?; + Ok(()) + }) + } + + /// Unreserve some funds, returning any amount that was unable to be unreserved. + /// + /// Is a no-op if the value to be unreserved is zero. + fn unreserve_named( + id: &Self::ReserveIdentifier, + who: &T::AccountId, + value: Self::Balance, + ) -> Self::Balance { + if value.is_zero() { + return Zero::zero(); + } + + Reserves::::mutate_exists(who, |maybe_reserves| -> Self::Balance { + if let Some(reserves) = maybe_reserves.as_mut() { + match reserves.binary_search_by_key(id, |data| data.id) { + Ok(index) => { + let to_change = cmp::min(reserves[index].amount, value); + + let remain = >::unreserve(who, to_change); + + // remain should always be zero but just to be defensive here. + let actual = to_change.defensive_saturating_sub(remain); + + // `actual <= to_change` and `to_change <= amount`; qed; + reserves[index].amount -= actual; + + if reserves[index].amount.is_zero() { + if reserves.len() == 1 { + // no more named reserves + *maybe_reserves = None; + } else { + // remove this named reserve + reserves.remove(index); + } + } + + value - actual + }, + Err(_) => value, + } + } else { + value + } + }) + } + + /// Slash from reserved balance, returning the negative imbalance created, + /// and any amount that was unable to be slashed. + /// + /// Is a no-op if the value to be slashed is zero. + fn slash_reserved_named( + id: &Self::ReserveIdentifier, + who: &T::AccountId, + value: Self::Balance, + ) -> (Self::NegativeImbalance, Self::Balance) { + if value.is_zero() { + return (NegativeImbalance::zero(), Zero::zero()); + } + + Reserves::::mutate(who, |reserves| -> (Self::NegativeImbalance, Self::Balance) { + match reserves.binary_search_by_key(id, |data| data.id) { + Ok(index) => { + let to_change = cmp::min(reserves[index].amount, value); + + let (imb, remain) = + >::slash_reserved(who, to_change); + + // remain should always be zero but just to be defensive here. + let actual = to_change.defensive_saturating_sub(remain); + + // `actual <= to_change` and `to_change <= amount`; qed; + reserves[index].amount -= actual; + + Self::deposit_event(Event::Slashed { who: who.clone(), amount: actual }); + (imb, value - actual) + }, + Err(_) => (NegativeImbalance::zero(), value), + } + }) + } + + /// Move the reserved balance of one account into the balance of another, according to `status`. + /// If `status` is `Reserved`, the balance will be reserved with given `id`. + /// + /// Is a no-op if: + /// - the value to be moved is zero; or + /// - the `slashed` id equal to `beneficiary` and the `status` is `Reserved`. + fn repatriate_reserved_named( + id: &Self::ReserveIdentifier, + slashed: &T::AccountId, + beneficiary: &T::AccountId, + value: Self::Balance, + status: Status, + ) -> Result { + if value.is_zero() { + return Ok(Zero::zero()); + } + + if slashed == beneficiary { + return match status { + Status::Free => Ok(Self::unreserve_named(id, slashed, value)), + Status::Reserved => + Ok(value.saturating_sub(Self::reserved_balance_named(id, slashed))), + }; + } + + Reserves::::try_mutate(slashed, |reserves| -> Result { + match reserves.binary_search_by_key(id, |data| data.id) { + Ok(index) => { + let to_change = cmp::min(reserves[index].amount, value); + + let actual = if status == Status::Reserved { + // make it the reserved under same identifier + Reserves::::try_mutate( + beneficiary, + |reserves| -> Result { + match reserves.binary_search_by_key(id, |data| data.id) { + Ok(index) => { + let remain = + >::repatriate_reserved( + slashed, + beneficiary, + to_change, + status, + )?; + + // remain should always be zero but just to be defensive + // here. + let actual = to_change.defensive_saturating_sub(remain); + + // this add can't overflow but just to be defensive. + reserves[index].amount = + reserves[index].amount.defensive_saturating_add(actual); + + Ok(actual) + }, + Err(index) => { + let remain = + >::repatriate_reserved( + slashed, + beneficiary, + to_change, + status, + )?; + + // remain should always be zero but just to be defensive + // here + let actual = to_change.defensive_saturating_sub(remain); + + reserves + .try_insert( + index, + ReserveData { id: *id, amount: actual }, + ) + .map_err(|_| Error::::TooManyReserves)?; + + Ok(actual) + }, + } + }, + )? + } else { + let remain = >::repatriate_reserved( + slashed, + beneficiary, + to_change, + status, + )?; + + // remain should always be zero but just to be defensive here + to_change.defensive_saturating_sub(remain) + }; + + // `actual <= to_change` and `to_change <= amount`; qed; + reserves[index].amount -= actual; + + Ok(value - actual) + }, + Err(_) => Ok(value), + } + }) + } +} + +impl, I: 'static> LockableCurrency for Pallet +where + T::Balance: Balance, +{ + type Moment = BlockNumberFor; + + type MaxLocks = T::MaxLocks; + + // Set or alter a lock on the balance of `who`. + fn set_lock( + id: LockIdentifier, + who: &T::AccountId, + amount: T::Balance, + reasons: WithdrawReasons, + ) { + if reasons.is_empty() || amount.is_zero() { + Self::remove_lock(id, who); + return; + } + + let mut new_lock = Some(BalanceLock { id, amount, reasons: reasons.into() }); + let mut locks = Self::locks(who) + .into_iter() + .filter_map(|l| if l.id == id { new_lock.take() } else { Some(l) }) + .collect::>(); + if let Some(lock) = new_lock { + locks.push(lock) + } + Self::update_locks(who, &locks[..]); + } + + // Extend a lock on the balance of `who`. + // Is a no-op if lock amount is zero or `reasons` `is_none()`. + fn extend_lock( + id: LockIdentifier, + who: &T::AccountId, + amount: T::Balance, + reasons: WithdrawReasons, + ) { + if amount.is_zero() || reasons.is_empty() { + return; + } + let mut new_lock = Some(BalanceLock { id, amount, reasons: reasons.into() }); + let mut locks = Self::locks(who) + .into_iter() + .filter_map(|l| { + if l.id == id { + new_lock.take().map(|nl| BalanceLock { + id: l.id, + amount: l.amount.max(nl.amount), + reasons: l.reasons | nl.reasons, + }) + } else { + Some(l) + } + }) + .collect::>(); + if let Some(lock) = new_lock { + locks.push(lock) + } + Self::update_locks(who, &locks[..]); + } + + fn remove_lock(id: LockIdentifier, who: &T::AccountId) { + let mut locks = Self::locks(who); + locks.retain(|l| l.id != id); + Self::update_locks(who, &locks[..]); + } +} + +impl, I: 'static> InspectLockableCurrency for Pallet { + fn balance_locked(id: LockIdentifier, who: &T::AccountId) -> Self::Balance { + Self::locks(who) + .into_iter() + .filter(|l| l.id == id) + .fold(Zero::zero(), |acc, l| acc + l.amount) + } +} diff --git a/pallets/balances/src/impl_fungible.rs b/pallets/balances/src/impl_fungible.rs new file mode 100644 index 00000000..e5e43e04 --- /dev/null +++ b/pallets/balances/src/impl_fungible.rs @@ -0,0 +1,385 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Implementation of `fungible` traits for Balances pallet. +use super::*; +use frame_support::traits::{ + tokens::{ + Fortitude, + Preservation::{self, Preserve, Protect}, + Provenance::{self, Minted}, + }, + AccountTouch, +}; + +impl, I: 'static> fungible::Inspect for Pallet { + type Balance = T::Balance; + + fn total_issuance() -> Self::Balance { + TotalIssuance::::get() + } + fn active_issuance() -> Self::Balance { + TotalIssuance::::get().saturating_sub(InactiveIssuance::::get()) + } + fn minimum_balance() -> Self::Balance { + T::ExistentialDeposit::get() + } + fn total_balance(who: &T::AccountId) -> Self::Balance { + Self::account(who).total() + } + fn balance(who: &T::AccountId) -> Self::Balance { + Self::account(who).free + } + fn reducible_balance( + who: &T::AccountId, + preservation: Preservation, + force: Fortitude, + ) -> Self::Balance { + let a = Self::account(who); + let mut untouchable = Zero::zero(); + if force == Polite { + // Frozen balance applies to total. Anything on hold therefore gets discounted from the + // limit given by the freezes. + untouchable = a.frozen.saturating_sub(a.reserved); + } + // If we want to keep our provider ref.. + if preservation == Preserve + // ..or we don't want the account to die and our provider ref is needed for it to live.. + || preservation == Protect && !a.free.is_zero() && + frame_system::Pallet::::providers(who) == 1 + // ..or we don't care about the account dying but our provider ref is required.. + || preservation == Expendable && !a.free.is_zero() && + !frame_system::Pallet::::can_dec_provider(who) + { + // ..then the ED needed.. + untouchable = untouchable.max(T::ExistentialDeposit::get()); + } + // Liquid balance is what is neither on hold nor frozen/required for provider. + a.free.saturating_sub(untouchable) + } + fn can_deposit( + who: &T::AccountId, + amount: Self::Balance, + provenance: Provenance, + ) -> DepositConsequence { + if amount.is_zero() { + return DepositConsequence::Success; + } + + if provenance == Minted && TotalIssuance::::get().checked_add(&amount).is_none() { + return DepositConsequence::Overflow; + } + + let account = Self::account(who); + let new_free = match account.free.checked_add(&amount) { + None => return DepositConsequence::Overflow, + Some(x) if x < T::ExistentialDeposit::get() => return DepositConsequence::BelowMinimum, + Some(x) => x, + }; + + match account.reserved.checked_add(&new_free) { + Some(_) => {}, + None => return DepositConsequence::Overflow, + }; + + // NOTE: We assume that we are a provider, so don't need to do any checks in the + // case of account creation. + + DepositConsequence::Success + } + fn can_withdraw( + who: &T::AccountId, + amount: Self::Balance, + ) -> WithdrawConsequence { + if amount.is_zero() { + return WithdrawConsequence::Success; + } + + if TotalIssuance::::get().checked_sub(&amount).is_none() { + return WithdrawConsequence::Underflow; + } + + let account = Self::account(who); + let new_free_balance = match account.free.checked_sub(&amount) { + Some(x) => x, + None => return WithdrawConsequence::BalanceLow, + }; + + let liquid = Self::reducible_balance(who, Expendable, Polite); + if amount > liquid { + return WithdrawConsequence::Frozen; + } + + // Provider restriction - total account balance cannot be reduced to zero if it cannot + // sustain the loss of a provider reference. + // NOTE: This assumes that the pallet is a provider (which is true). Is this ever changes, + // then this will need to adapt accordingly. + let ed = T::ExistentialDeposit::get(); + let success = if new_free_balance < ed { + if frame_system::Pallet::::can_dec_provider(who) { + WithdrawConsequence::ReducedToZero(new_free_balance) + } else { + return WithdrawConsequence::WouldDie; + } + } else { + WithdrawConsequence::Success + }; + + let new_total_balance = new_free_balance.saturating_add(account.reserved); + + // Eventual free funds must be no less than the frozen balance. + if new_total_balance < account.frozen { + return WithdrawConsequence::Frozen; + } + + success + } +} + +impl, I: 'static> fungible::Unbalanced for Pallet { + fn handle_dust(dust: fungible::Dust) { + T::DustRemoval::on_unbalanced(dust.into_credit()); + } + fn write_balance( + who: &T::AccountId, + amount: Self::Balance, + ) -> Result, DispatchError> { + let max_reduction = + >::reducible_balance(who, Expendable, Force); + let (result, maybe_dust) = Self::mutate_account(who, |account| -> DispatchResult { + // Make sure the reduction (if there is one) is no more than the maximum allowed. + let reduction = account.free.saturating_sub(amount); + ensure!(reduction <= max_reduction, Error::::InsufficientBalance); + + account.free = amount; + Ok(()) + })?; + result?; + Ok(maybe_dust) + } + + fn set_total_issuance(amount: Self::Balance) { + TotalIssuance::::mutate(|t| *t = amount); + } + + fn deactivate(amount: Self::Balance) { + InactiveIssuance::::mutate(|b| { + // InactiveIssuance cannot be greater than TotalIssuance. + *b = b.saturating_add(amount).min(TotalIssuance::::get()); + }); + } + + fn reactivate(amount: Self::Balance) { + InactiveIssuance::::mutate(|b| b.saturating_reduce(amount)); + } +} + +impl, I: 'static> fungible::Mutate for Pallet { + fn done_mint_into(who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Minted { who: who.clone(), amount }); + } + fn done_burn_from(who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Burned { who: who.clone(), amount }); + } + fn done_shelve(who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Suspended { who: who.clone(), amount }); + } + fn done_restore(who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Restored { who: who.clone(), amount }); + } + fn done_transfer(source: &T::AccountId, dest: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Transfer { + from: source.clone(), + to: dest.clone(), + amount, + }); + } +} + +impl, I: 'static> fungible::MutateHold for Pallet {} + +impl, I: 'static> fungible::InspectHold for Pallet { + type Reason = T::RuntimeHoldReason; + + fn total_balance_on_hold(who: &T::AccountId) -> T::Balance { + Self::account(who).reserved + } + fn reducible_total_balance_on_hold(who: &T::AccountId, force: Fortitude) -> Self::Balance { + // The total balance must never drop below the freeze requirements if we're not forcing: + let a = Self::account(who); + let unavailable = if force == Force { + Self::Balance::zero() + } else { + // The freeze lock applies to the total balance, so we can discount the free balance + // from the amount which the total reserved balance must provide to satisfy it. + a.frozen.saturating_sub(a.free) + }; + a.reserved.saturating_sub(unavailable) + } + fn balance_on_hold(reason: &Self::Reason, who: &T::AccountId) -> T::Balance { + Holds::::get(who) + .iter() + .find(|x| &x.id == reason) + .map_or_else(Zero::zero, |x| x.amount) + } + fn hold_available(reason: &Self::Reason, who: &T::AccountId) -> bool { + if frame_system::Pallet::::providers(who) == 0 { + return false; + } + let holds = Holds::::get(who); + if holds.is_full() && !holds.iter().any(|x| &x.id == reason) { + return false; + } + true + } +} + +impl, I: 'static> fungible::UnbalancedHold for Pallet { + fn set_balance_on_hold( + reason: &Self::Reason, + who: &T::AccountId, + amount: Self::Balance, + ) -> DispatchResult { + let mut new_account = Self::account(who); + let mut holds = Holds::::get(who); + let mut increase = true; + let mut delta = amount; + + if let Some(item) = holds.iter_mut().find(|x| &x.id == reason) { + delta = item.amount.max(amount) - item.amount.min(amount); + increase = amount > item.amount; + item.amount = amount; + holds.retain(|x| !x.amount.is_zero()); + } else if !amount.is_zero() { + holds + .try_push(IdAmount { id: *reason, amount }) + .map_err(|_| Error::::TooManyHolds)?; + } + + new_account.reserved = if increase { + new_account.reserved.checked_add(&delta).ok_or(ArithmeticError::Overflow)? + } else { + new_account.reserved.checked_sub(&delta).ok_or(ArithmeticError::Underflow)? + }; + + let (result, maybe_dust) = Self::try_mutate_account(who, |a, _| -> DispatchResult { + *a = new_account; + Ok(()) + })?; + debug_assert!( + maybe_dust.is_none(), + "Does not alter main balance; dust only happens when it is altered; qed" + ); + Holds::::insert(who, holds); + Ok(result) + } +} + +impl, I: 'static> fungible::InspectFreeze for Pallet { + type Id = T::FreezeIdentifier; + + fn balance_frozen(id: &Self::Id, who: &T::AccountId) -> Self::Balance { + let locks = Freezes::::get(who); + locks.into_iter().find(|l| &l.id == id).map_or(Zero::zero(), |l| l.amount) + } + + fn can_freeze(id: &Self::Id, who: &T::AccountId) -> bool { + let l = Freezes::::get(who); + !l.is_full() || l.iter().any(|x| &x.id == id) + } +} + +impl, I: 'static> fungible::MutateFreeze for Pallet { + fn set_freeze(id: &Self::Id, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { + if amount.is_zero() { + return Self::thaw(id, who); + } + let mut locks = Freezes::::get(who); + if let Some(i) = locks.iter_mut().find(|x| &x.id == id) { + i.amount = amount; + } else { + locks + .try_push(IdAmount { id: *id, amount }) + .map_err(|_| Error::::TooManyFreezes)?; + } + Self::update_freezes(who, locks.as_bounded_slice()) + } + + fn extend_freeze(id: &Self::Id, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { + if amount.is_zero() { + return Ok(()); + } + let mut locks = Freezes::::get(who); + if let Some(i) = locks.iter_mut().find(|x| &x.id == id) { + i.amount = i.amount.max(amount); + } else { + locks + .try_push(IdAmount { id: *id, amount }) + .map_err(|_| Error::::TooManyFreezes)?; + } + Self::update_freezes(who, locks.as_bounded_slice()) + } + + fn thaw(id: &Self::Id, who: &T::AccountId) -> DispatchResult { + let mut locks = Freezes::::get(who); + locks.retain(|l| &l.id != id); + Self::update_freezes(who, locks.as_bounded_slice()) + } +} + +impl, I: 'static> fungible::Balanced for Pallet { + type OnDropCredit = fungible::DecreaseIssuance; + type OnDropDebt = fungible::IncreaseIssuance; + + fn done_deposit(who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Deposit { who: who.clone(), amount }); + } + fn done_withdraw(who: &T::AccountId, amount: Self::Balance) { + Self::deposit_event(Event::::Withdraw { who: who.clone(), amount }); + } + fn done_issue(amount: Self::Balance) { + if !amount.is_zero() { + Self::deposit_event(Event::::Issued { amount }); + } + } + fn done_rescind(amount: Self::Balance) { + Self::deposit_event(Event::::Rescinded { amount }); + } +} + +impl, I: 'static> fungible::BalancedHold for Pallet {} + +impl, I: 'static> + fungible::hold::DoneSlash for Pallet +{ + fn done_slash(reason: &T::RuntimeHoldReason, who: &T::AccountId, amount: T::Balance) { + T::DoneSlashHandler::done_slash(reason, who, amount); + } +} + +impl, I: 'static> AccountTouch<(), T::AccountId> for Pallet { + type Balance = T::Balance; + fn deposit_required(_: ()) -> Self::Balance { + Self::Balance::zero() + } + fn should_touch(_: (), _: &T::AccountId) -> bool { + false + } + fn touch(_: (), _: &T::AccountId, _: &T::AccountId) -> DispatchResult { + Ok(()) + } +} diff --git a/pallets/balances/src/impl_proofs.rs b/pallets/balances/src/impl_proofs.rs new file mode 100644 index 00000000..517225f5 --- /dev/null +++ b/pallets/balances/src/impl_proofs.rs @@ -0,0 +1,28 @@ +use super::*; +use qp_wormhole::TransferProofs; + +impl, I: 'static> TransferProofs + for Pallet +{ + fn transfer_proof_exists( + count: TransferCountType, + from: &T::AccountId, + to: &T::AccountId, + value: T::Balance, + ) -> bool { + TransferProof::::get((count, from, to, value)).is_some() + } + + fn store_transfer_proof(from: &T::AccountId, to: &T::AccountId, value: T::Balance) { + Pallet::::do_store_transfer_proof(from, to, value); + } + + fn transfer_proof_key( + transfer_count: u64, + from: T::AccountId, + to: T::AccountId, + value: T::Balance, + ) -> Vec { + Pallet::::transfer_proof_storage_key(transfer_count, from, to, value) + } +} diff --git a/pallets/balances/src/lib.rs b/pallets/balances/src/lib.rs new file mode 100644 index 00000000..164515a6 --- /dev/null +++ b/pallets/balances/src/lib.rs @@ -0,0 +1,1335 @@ +// This file is part of Substrate. + +#![allow(clippy::all)] +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # Balances Pallet +//! +//! The Balances pallet provides functionality for handling accounts and balances for a single +//! token. +//! +//! It makes heavy use of concepts such as Holds and Freezes from the +//! [`frame_support::traits::fungible`] traits, therefore you should read and understand those docs +//! as a prerequisite to understanding this pallet. +//! +//! Also see the [`frame_tokens`] reference docs for higher level information regarding the +//! place of this palet in FRAME. +//! +//! ## Overview +//! +//! The Balances pallet provides functions for: +//! +//! - Getting and setting free balances. +//! - Retrieving total, reserved and unreserved balances. +//! - Repatriating a reserved balance to a beneficiary account that exists. +//! - Transferring a balance between accounts (when not reserved). +//! - Slashing an account balance. +//! - Account creation and removal. +//! - Managing total issuance. +//! - Setting and managing locks. +//! +//! ### Terminology +//! +//! - **Reaping an account:** The act of removing an account by resetting its nonce. Happens after +//! its total balance has become less than the Existential Deposit. +//! +//! ### Implementations +//! +//! The Balances pallet provides implementations for the following [`fungible`] traits. If these +//! traits provide the functionality that you need, then you should avoid tight coupling with the +//! Balances pallet. +//! +//! - [`fungible::Inspect`] +//! - [`fungible::Mutate`] +//! - [`fungible::Unbalanced`] +//! - [`fungible::Balanced`] +//! - [`fungible::BalancedHold`] +//! - [`fungible::InspectHold`] +//! - [`fungible::MutateHold`] +//! - [`fungible::InspectFreeze`] +//! - [`fungible::MutateFreeze`] +//! - [`fungible::Imbalance`] +//! +//! It also implements the following [`Currency`] related traits, however they are deprecated and +//! will eventually be removed. +//! +//! - [`Currency`]: Functions for dealing with a fungible assets system. +//! - [`ReservableCurrency`] +//! - [`NamedReservableCurrency`](frame_support::traits::NamedReservableCurrency): Functions for +//! dealing with assets that can be reserved from an account. +//! - [`LockableCurrency`](frame_support::traits::LockableCurrency): Functions for dealing with +//! accounts that allow liquidity restrictions. +//! - [`Imbalance`](frame_support::traits::Imbalance): Functions for handling imbalances between +//! total issuance in the system and account balances. Must be used when a function creates new +//! funds (e.g. a reward) or destroys some funds (e.g. a system fee). +//! +//! ## Usage +//! +//! The following examples show how to use the Balances pallet in your custom pallet. +//! +//! ### Examples from the FRAME +//! +//! The Contract pallet uses the `Currency` trait to handle gas payment, and its types inherit from +//! `Currency`: +//! +//! ``` +//! use frame_support::traits::Currency; +//! # pub trait Config: frame_system::Config { +//! # type Currency: Currency; +//! # } +//! +//! pub type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; +//! pub type NegativeImbalanceOf = <::Currency as Currency<::AccountId>>::NegativeImbalance; +//! +//! # fn main() {} +//! ``` +//! +//! The Staking pallet uses the `LockableCurrency` trait to lock a stash account's funds: +//! +//! ``` +//! use frame_support::traits::{WithdrawReasons, LockableCurrency}; +//! use sp_runtime::traits::Bounded; +//! pub trait Config: frame_system::Config { +//! type Currency: LockableCurrency>; +//! } +//! # struct StakingLedger { +//! # stash: ::AccountId, +//! # total: <::Currency as frame_support::traits::Currency<::AccountId>>::Balance, +//! # phantom: std::marker::PhantomData, +//! # } +//! # const STAKING_ID: [u8; 8] = *b"staking "; +//! +//! fn update_ledger( +//! controller: &T::AccountId, +//! ledger: &StakingLedger +//! ) { +//! T::Currency::set_lock( +//! STAKING_ID, +//! &ledger.stash, +//! ledger.total, +//! WithdrawReasons::all() +//! ); +//! // >::insert(controller, ledger); // Commented out as we don't have access to Staking's storage here. +//! } +//! # fn main() {} +//! ``` +//! +//! ## Genesis config +//! +//! The Balances pallet depends on the [`GenesisConfig`]. +//! +//! ## Assumptions +//! +//! * Total issued balanced of all accounts should be less than `Config::Balance::max_value()`. +//! * Existential Deposit is set to a value greater than zero. +//! +//! Note, you may find the Balances pallet still functions with an ED of zero when the +//! `insecure_zero_ed` cargo feature is enabled. However this is not a configuration which is +//! generally supported, nor will it be. +//! +//! [`frame_tokens`]: ../polkadot_sdk_docs/reference_docs/frame_tokens/index.html + +#![cfg_attr(not(feature = "std"), no_std)] +mod benchmarking; +mod impl_currency; +mod impl_fungible; +mod impl_proofs; +pub mod migration; +mod tests; +mod types; +pub mod weights; + +extern crate alloc; + +use alloc::vec::Vec; +use codec::{Codec, Decode, Encode, MaxEncodedLen}; +use core::{cmp, fmt::Debug, marker::PhantomData, mem, result}; +use frame_support::{ + ensure, + pallet_prelude::DispatchResult, + traits::{ + tokens::{ + fungible, Balance as BalanceT, BalanceStatus as Status, DepositConsequence, + Fortitude::{self, Force, Polite}, + IdAmount, + Preservation::{Expendable, Preserve, Protect}, + WithdrawConsequence, + }, + Currency, Defensive, Get, OnUnbalanced, ReservableCurrency, StoredMap, + }, + BoundedSlice, StorageHasher, WeakBoundedVec, +}; +use frame_system as system; +pub use impl_currency::{NegativeImbalance, PositiveImbalance}; +pub use pallet::*; +use qp_poseidon::PoseidonHasher as PoseidonCore; +use scale_info::TypeInfo; +use sp_metadata_ir::StorageHasherIR; +use sp_runtime::{ + traits::{ + AtLeast32BitUnsigned, CheckedAdd, CheckedSub, MaybeSerializeDeserialize, Saturating, + StaticLookup, Zero, + }, + ArithmeticError, DispatchError, FixedPointOperand, Perbill, RuntimeDebug, TokenError, +}; +pub use types::{ + AccountData, AdjustmentDirection, BalanceLock, DustCleaner, ExtraFlags, Reasons, ReserveData, +}; +pub use weights::WeightInfo; + +const LOG_TARGET: &str = "runtime::balances"; + +type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; + +pub struct PoseidonStorageHasher(PhantomData); + +impl StorageHasher + for PoseidonStorageHasher +{ + // We are lying here, but maybe it's ok because it's just metadata + const METADATA: StorageHasherIR = StorageHasherIR::Identity; + type Output = [u8; 32]; + + fn hash(x: &[u8]) -> Self::Output { + PoseidonCore::hash_storage::(x) + } + + fn max_len() -> usize { + 32 + } +} + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::{ + pallet_prelude::*, + traits::{fungible::Credit, tokens::Precision, VariantCount, VariantCountOf}, + }; + use frame_system::pallet_prelude::*; + + pub type CreditOf = Credit<::AccountId, Pallet>; + pub type TransferCountType = u64; + + /// Default implementations of [`DefaultConfig`], which can be used to implement [`Config`]. + pub mod config_preludes { + use super::*; + use frame_support::{derive_impl, traits::ConstU64}; + + pub struct TestDefaultConfig; + + #[derive_impl(frame_system::config_preludes::TestDefaultConfig, no_aggregated_types)] + impl frame_system::DefaultConfig for TestDefaultConfig {} + + #[frame_support::register_default_impl(TestDefaultConfig)] + impl DefaultConfig for TestDefaultConfig { + #[inject_runtime_type] + type RuntimeHoldReason = (); + #[inject_runtime_type] + type RuntimeFreezeReason = (); + + type Balance = u64; + type ExistentialDeposit = ConstU64<1>; + + type ReserveIdentifier = (); + type FreezeIdentifier = Self::RuntimeFreezeReason; + + type DustRemoval = (); + + type MaxLocks = ConstU32<100>; + type MaxReserves = ConstU32<100>; + type MaxFreezes = VariantCountOf; + + type WeightInfo = (); + type DoneSlashHandler = (); + } + } + + #[pallet::config(with_default)] + pub trait Config: frame_system::Config { + /// The overarching hold reason. + #[pallet::no_default_bounds] + type RuntimeHoldReason: Parameter + Member + MaxEncodedLen + Copy + VariantCount; + + /// The overarching freeze reason. + #[pallet::no_default_bounds] + type RuntimeFreezeReason: VariantCount; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + + /// The balance of an account. + type Balance: Parameter + + Member + + AtLeast32BitUnsigned + + Codec + + Default + + Copy + + MaybeSerializeDeserialize + + Debug + + MaxEncodedLen + + TypeInfo + + FixedPointOperand + + BalanceT; + + /// Handler for the unbalanced reduction when removing a dust account. + #[pallet::no_default_bounds] + type DustRemoval: OnUnbalanced>; + + /// The minimum amount required to keep an account open. MUST BE GREATER THAN ZERO! + /// + /// If you *really* need it to be zero, you can enable the feature `insecure_zero_ed` for + /// this pallet. However, you do so at your own risk: this will open up a major DoS vector. + /// In case you have multiple sources of provider references, you may also get unexpected + /// behaviour if you set this to zero. + /// + /// Bottom line: Do yourself a favour and make it at least one! + #[pallet::constant] + #[pallet::no_default_bounds] + type ExistentialDeposit: Get; + + /// The means of storing the balances of an account. + #[pallet::no_default] + type AccountStore: StoredMap>; + + /// The ID type for reserves. + /// + /// Use of reserves is deprecated in favour of holds. See `https://github.com/paritytech/substrate/pull/12951/` + type ReserveIdentifier: Parameter + Member + MaxEncodedLen + Ord + Copy; + + /// The ID type for freezes. + type FreezeIdentifier: Parameter + Member + MaxEncodedLen + Copy; + + /// The maximum number of locks that should exist on an account. + /// Not strictly enforced, but used for weight estimation. + /// + /// Use of locks is deprecated in favour of freezes. See `https://github.com/paritytech/substrate/pull/12951/` + #[pallet::constant] + type MaxLocks: Get; + + /// The maximum number of named reserves that can exist on an account. + /// + /// Use of reserves is deprecated in favour of holds. See `https://github.com/paritytech/substrate/pull/12951/` + #[pallet::constant] + type MaxReserves: Get; + + /// The maximum number of individual freeze locks that can exist on an account at any time. + #[pallet::constant] + type MaxFreezes: Get; + + /// Allows callbacks to other pallets so they can update their bookkeeping when a slash + /// occurs. + type DoneSlashHandler: fungible::hold::DoneSlash< + Self::RuntimeHoldReason, + Self::AccountId, + Self::Balance, + >; + + /// Account ID used as the "from" account when creating transfer proofs for minted tokens + /// (e.g., genesis balances, mining rewards). This should be a well-known address that + /// represents "minted from nothing". + #[pallet::constant] + #[pallet::no_default] + type MintingAccount: Get; + } + + /// The in-code storage version. + const STORAGE_VERSION: frame_support::traits::StorageVersion = + frame_support::traits::StorageVersion::new(1); + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(PhantomData<(T, I)>); + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event, I: 'static = ()> { + /// An account was created with some free balance. + Endowed { account: T::AccountId, free_balance: T::Balance }, + /// An account was removed whose balance was non-zero but below ExistentialDeposit, + /// resulting in an outright loss. + DustLost { account: T::AccountId, amount: T::Balance }, + /// Transfer succeeded. + Transfer { from: T::AccountId, to: T::AccountId, amount: T::Balance }, + /// A balance was set by root. + BalanceSet { who: T::AccountId, free: T::Balance }, + /// Some balance was reserved (moved from free to reserved). + Reserved { who: T::AccountId, amount: T::Balance }, + /// Some balance was unreserved (moved from reserved to free). + Unreserved { who: T::AccountId, amount: T::Balance }, + /// Some balance was moved from the reserve of the first account to the second account. + /// Final argument indicates the destination balance type. + ReserveRepatriated { + from: T::AccountId, + to: T::AccountId, + amount: T::Balance, + destination_status: Status, + }, + /// Some amount was deposited (e.g. for transaction fees). + Deposit { who: T::AccountId, amount: T::Balance }, + /// Some amount was withdrawn from the account (e.g. for transaction fees). + Withdraw { who: T::AccountId, amount: T::Balance }, + /// Some amount was removed from the account (e.g. for misbehavior). + Slashed { who: T::AccountId, amount: T::Balance }, + /// Some amount was minted into an account. + Minted { who: T::AccountId, amount: T::Balance }, + /// Some amount was burned from an account. + Burned { who: T::AccountId, amount: T::Balance }, + /// Some amount was suspended from an account (it can be restored later). + Suspended { who: T::AccountId, amount: T::Balance }, + /// Some amount was restored into an account. + Restored { who: T::AccountId, amount: T::Balance }, + /// An account was upgraded. + Upgraded { who: T::AccountId }, + /// Total issuance was increased by `amount`, creating a credit to be balanced. + Issued { amount: T::Balance }, + /// Total issuance was decreased by `amount`, creating a debt to be balanced. + Rescinded { amount: T::Balance }, + /// Some balance was locked. + Locked { who: T::AccountId, amount: T::Balance }, + /// Some balance was unlocked. + Unlocked { who: T::AccountId, amount: T::Balance }, + /// Some balance was frozen. + Frozen { who: T::AccountId, amount: T::Balance }, + /// Some balance was thawed. + Thawed { who: T::AccountId, amount: T::Balance }, + /// The `TotalIssuance` was forcefully changed. + TotalIssuanceForced { old: T::Balance, new: T::Balance }, + } + + #[pallet::error] + pub enum Error { + /// Vesting balance too high to send value. + VestingBalance, + /// Account liquidity restrictions prevent withdrawal. + LiquidityRestrictions, + /// Balance too low to send value. + InsufficientBalance, + /// Value too low to create account due to existential deposit. + ExistentialDeposit, + /// Transfer/payment would kill account. + Expendability, + /// A vesting schedule already exists for this account. + ExistingVestingSchedule, + /// Beneficiary account must pre-exist. + DeadAccount, + /// Number of named reserves exceed `MaxReserves`. + TooManyReserves, + /// Number of holds exceed `VariantCountOf`. + TooManyHolds, + /// Number of freezes exceed `MaxFreezes`. + TooManyFreezes, + /// The issuance cannot be modified since it is already deactivated. + IssuanceDeactivated, + /// The delta cannot be zero. + DeltaZero, + } + + /// The total units issued in the system. + #[pallet::storage] + #[pallet::whitelist_storage] + pub type TotalIssuance, I: 'static = ()> = StorageValue<_, T::Balance, ValueQuery>; + + /// The total units of outstanding deactivated balance in the system. + #[pallet::storage] + #[pallet::whitelist_storage] + pub type InactiveIssuance, I: 'static = ()> = + StorageValue<_, T::Balance, ValueQuery>; + + /// The Balances pallet example of storing the balance of an account. + /// + /// # Example + /// + /// ```nocompile + /// impl pallet_balances::Config for Runtime { + /// type AccountStore = StorageMapShim, frame_system::Provider, AccountId, Self::AccountData> + /// } + /// ``` + /// + /// You can also store the balance of an account in the `System` pallet. + /// + /// # Example + /// + /// ```nocompile + /// impl pallet_balances::Config for Runtime { + /// type AccountStore = System + /// } + /// ``` + /// + /// But this comes with tradeoffs, storing account balances in the system pallet stores + /// `frame_system` data alongside the account data contrary to storing account balances in the + /// `Balances` pallet, which uses a `StorageMap` to store balances data only. + /// NOTE: This is only used in the case that this pallet is used to store balances. + #[pallet::storage] + pub type Account, I: 'static = ()> = + StorageMap<_, Blake2_128Concat, T::AccountId, AccountData, ValueQuery>; + + /// Any liquidity locks on some account balances. + /// NOTE: Should only be accessed when setting, changing and freeing a lock. + /// + /// Use of locks is deprecated in favour of freezes. See `https://github.com/paritytech/substrate/pull/12951/` + #[pallet::storage] + pub type Locks, I: 'static = ()> = StorageMap< + _, + Blake2_128Concat, + T::AccountId, + WeakBoundedVec, T::MaxLocks>, + ValueQuery, + >; + + /// Named reserves on some account balances. + /// + /// Use of reserves is deprecated in favour of holds. See `https://github.com/paritytech/substrate/pull/12951/` + #[pallet::storage] + pub type Reserves, I: 'static = ()> = StorageMap< + _, + Blake2_128Concat, + T::AccountId, + BoundedVec, T::MaxReserves>, + ValueQuery, + >; + + /// Holds on account balances. + #[pallet::storage] + pub type Holds, I: 'static = ()> = StorageMap< + _, + Blake2_128Concat, + T::AccountId, + BoundedVec< + IdAmount, + VariantCountOf, + >, + ValueQuery, + >; + + /// Freeze locks on account balances. + #[pallet::storage] + pub type Freezes, I: 'static = ()> = StorageMap< + _, + Blake2_128Concat, + T::AccountId, + BoundedVec, T::MaxFreezes>, + ValueQuery, + >; + + /// Transfer proofs for a wormhole transfers + #[pallet::storage] + #[pallet::getter(fn transfer_proof)] + pub type TransferProof, I: 'static = ()> = StorageMap< + _, + PoseidonStorageHasher, + (u64, T::AccountId, T::AccountId, T::Balance), // (tx_count, from, to, amount) + (), + OptionQuery, // Returns None if not present + >; + + #[pallet::storage] + #[pallet::getter(fn transfer_count)] + pub type TransferCount, I: 'static = ()> = + StorageValue<_, TransferCountType, ValueQuery>; + + #[pallet::genesis_config] + pub struct GenesisConfig, I: 'static = ()> { + pub balances: Vec<(T::AccountId, T::Balance)>, + } + + impl, I: 'static> Default for GenesisConfig { + fn default() -> Self { + Self { balances: Default::default() } + } + } + + #[pallet::genesis_build] + impl, I: 'static> BuildGenesisConfig for GenesisConfig { + fn build(&self) { + let total = self.balances.iter().fold(Zero::zero(), |acc: T::Balance, &(_, n)| acc + n); + + >::put(total); + + for (_, balance) in &self.balances { + assert!( + *balance >= >::ExistentialDeposit::get(), + "the balance of any account should always be at least the existential deposit.", + ) + } + + // ensure no duplicates exist. + let endowed_accounts = self + .balances + .iter() + .map(|(x, _)| x) + .cloned() + .collect::>(); + + assert!( + endowed_accounts.len() == self.balances.len(), + "duplicate balances in genesis." + ); + + let mint_account = T::MintingAccount::get(); + for &(ref who, free) in self.balances.iter() { + frame_system::Pallet::::inc_providers(who); + assert!(T::AccountStore::insert(who, AccountData { free, ..Default::default() }) + .is_ok()); + // Create transfer proof for genesis balance (from minting account) + Pallet::::do_store_transfer_proof(&mint_account, who, free); + } + } + } + + #[pallet::hooks] + impl, I: 'static> Hooks> for Pallet { + fn integrity_test() { + #[cfg(not(feature = "insecure_zero_ed"))] + assert!( + !>::ExistentialDeposit::get().is_zero(), + "The existential deposit must be greater than zero!" + ); + + assert!( + T::MaxFreezes::get() >= ::VARIANT_COUNT, + "MaxFreezes should be greater than or equal to the number of freeze reasons: {} < {}", + T::MaxFreezes::get(), ::VARIANT_COUNT, + ); + } + + #[cfg(feature = "try-runtime")] + fn try_state(_n: BlockNumberFor) -> Result<(), sp_runtime::TryRuntimeError> { + Holds::::iter_keys().try_for_each(|k| { + if Holds::::decode_len(k).unwrap_or(0) > + T::RuntimeHoldReason::VARIANT_COUNT as usize + { + Err("Found `Hold` with too many elements") + } else { + Ok(()) + } + })?; + + Freezes::::iter_keys().try_for_each(|k| { + if Freezes::::decode_len(k).unwrap_or(0) > T::MaxFreezes::get() as usize { + Err("Found `Freeze` with too many elements") + } else { + Ok(()) + } + })?; + + Ok(()) + } + } + + #[pallet::call(weight(>::WeightInfo))] + impl, I: 'static> Pallet { + /// Transfer some liquid free balance to another account. + /// + /// `transfer_allow_death` will set the `FreeBalance` of the sender and receiver. + /// If the sender's account is below the existential deposit as a result + /// of the transfer, the account will be reaped. + /// + /// The dispatch origin for this call must be `Signed` by the transactor. + #[pallet::call_index(0)] + pub fn transfer_allow_death( + origin: OriginFor, + dest: AccountIdLookupOf, + #[pallet::compact] value: T::Balance, + ) -> DispatchResult { + let source = ensure_signed(origin)?; + let dest = T::Lookup::lookup(dest)?; + >::transfer(&source, &dest, value, Expendable)?; + Self::do_store_transfer_proof(&source, &dest, value); + Ok(()) + } + + /// Exactly as `transfer_allow_death`, except the origin must be root and the source account + /// may be specified. + #[pallet::call_index(2)] + pub fn force_transfer( + origin: OriginFor, + source: AccountIdLookupOf, + dest: AccountIdLookupOf, + #[pallet::compact] value: T::Balance, + ) -> DispatchResult { + ensure_root(origin)?; + let source = T::Lookup::lookup(source)?; + let dest = T::Lookup::lookup(dest)?; + >::transfer(&source, &dest, value, Expendable)?; + Self::do_store_transfer_proof(&source, &dest, value); + Ok(()) + } + + /// Same as the [`transfer_allow_death`] call, but with a check that the transfer will not + /// kill the origin account. + /// + /// 99% of the time you want [`transfer_allow_death`] instead. + /// + /// [`transfer_allow_death`]: struct.Pallet.html#method.transfer + #[pallet::call_index(3)] + pub fn transfer_keep_alive( + origin: OriginFor, + dest: AccountIdLookupOf, + #[pallet::compact] value: T::Balance, + ) -> DispatchResult { + let source = ensure_signed(origin)?; + let dest = T::Lookup::lookup(dest)?; + >::transfer(&source, &dest, value, Preserve)?; + Self::do_store_transfer_proof(&source, &dest, value); + Ok(()) + } + + /// Transfer the entire transferable balance from the caller account. + /// + /// NOTE: This function only attempts to transfer _transferable_ balances. This means that + /// any locked, reserved, or existential deposits (when `keep_alive` is `true`), will not be + /// transferred by this function. To ensure that this function results in a killed account, + /// you might need to prepare the account by removing any reference counters, storage + /// deposits, etc... + /// + /// The dispatch origin of this call must be Signed. + /// + /// - `dest`: The recipient of the transfer. + /// - `keep_alive`: A boolean to determine if the `transfer_all` operation should send all + /// of the funds the account has, causing the sender account to be killed (false), or + /// transfer everything except at least the existential deposit, which will guarantee to + /// keep the sender account alive (true). + #[pallet::call_index(4)] + pub fn transfer_all( + origin: OriginFor, + dest: AccountIdLookupOf, + keep_alive: bool, + ) -> DispatchResult { + let transactor = ensure_signed(origin)?; + let keep_alive = if keep_alive { Preserve } else { Expendable }; + let reducible_balance = >::reducible_balance( + &transactor, + keep_alive, + Fortitude::Polite, + ); + let dest = T::Lookup::lookup(dest)?; + >::transfer( + &transactor, + &dest, + reducible_balance, + keep_alive, + )?; + Self::do_store_transfer_proof(&transactor, &dest, reducible_balance); + Ok(()) + } + + /// Unreserve some balance from a user by force. + /// + /// Can only be called by ROOT. + #[pallet::call_index(5)] + pub fn force_unreserve( + origin: OriginFor, + who: AccountIdLookupOf, + amount: T::Balance, + ) -> DispatchResult { + ensure_root(origin)?; + let who = T::Lookup::lookup(who)?; + let _leftover = >::unreserve(&who, amount); + Ok(()) + } + + /// Upgrade a specified account. + /// + /// - `origin`: Must be `Signed`. + /// - `who`: The account to be upgraded. + /// + /// This will waive the transaction fee if at least all but 10% of the accounts needed to + /// be upgraded. (We let some not have to be upgraded just in order to allow for the + /// possibility of churn). + #[pallet::call_index(6)] + #[pallet::weight(T::WeightInfo::upgrade_accounts(who.len() as u32))] + pub fn upgrade_accounts( + origin: OriginFor, + who: Vec, + ) -> DispatchResultWithPostInfo { + ensure_signed(origin)?; + if who.is_empty() { + return Ok(Pays::Yes.into()); + } + let mut upgrade_count = 0; + for i in &who { + let upgraded = Self::ensure_upgraded(i); + if upgraded { + upgrade_count.saturating_inc(); + } + } + let proportion_upgraded = Perbill::from_rational(upgrade_count, who.len() as u32); + if proportion_upgraded >= Perbill::from_percent(90) { + Ok(Pays::No.into()) + } else { + Ok(Pays::Yes.into()) + } + } + + /// Set the regular balance of a given account. + /// + /// The dispatch origin for this call is `root`. + #[pallet::call_index(8)] + #[pallet::weight( + T::WeightInfo::force_set_balance_creating() // Creates a new account. + .max(T::WeightInfo::force_set_balance_killing()) // Kills an existing account. + )] + pub fn force_set_balance( + origin: OriginFor, + who: AccountIdLookupOf, + #[pallet::compact] new_free: T::Balance, + ) -> DispatchResult { + ensure_root(origin)?; + let who = T::Lookup::lookup(who)?; + let existential_deposit = Self::ed(); + + let wipeout = new_free < existential_deposit; + let new_free = if wipeout { Zero::zero() } else { new_free }; + + // First we try to modify the account's balance to the forced balance. + let old_free = Self::mutate_account_handling_dust(&who, |account| { + let old_free = account.free; + account.free = new_free; + old_free + })?; + + // This will adjust the total issuance, which was not done by the `mutate_account` + // above. + match new_free.cmp(&old_free) { + cmp::Ordering::Greater => { + mem::drop(PositiveImbalance::::new(new_free - old_free)); + }, + cmp::Ordering::Less => { + mem::drop(NegativeImbalance::::new(old_free - new_free)); + }, + cmp::Ordering::Equal => {}, + } + + Self::deposit_event(Event::BalanceSet { who, free: new_free }); + Ok(()) + } + + /// Adjust the total issuance in a saturating way. + /// + /// Can only be called by root and always needs a positive `delta`. + /// + /// # Example + #[doc = docify::embed!("./src/tests/dispatchable_tests.rs", force_adjust_total_issuance_example)] + #[pallet::call_index(9)] + #[pallet::weight(T::WeightInfo::force_adjust_total_issuance())] + pub fn force_adjust_total_issuance( + origin: OriginFor, + direction: AdjustmentDirection, + #[pallet::compact] delta: T::Balance, + ) -> DispatchResult { + ensure_root(origin)?; + + ensure!(delta > Zero::zero(), Error::::DeltaZero); + + let old = TotalIssuance::::get(); + let new = match direction { + AdjustmentDirection::Increase => old.saturating_add(delta), + AdjustmentDirection::Decrease => old.saturating_sub(delta), + }; + + ensure!(InactiveIssuance::::get() <= new, Error::::IssuanceDeactivated); + TotalIssuance::::set(new); + + Self::deposit_event(Event::::TotalIssuanceForced { old, new }); + + Ok(()) + } + + /// Burn the specified liquid free balance from the origin account. + /// + /// If the origin's account ends up below the existential deposit as a result + /// of the burn and `keep_alive` is false, the account will be reaped. + /// + /// Unlike sending funds to a _burn_ address, which merely makes the funds inaccessible, + /// this `burn` operation will reduce total issuance by the amount _burned_. + #[pallet::call_index(10)] + #[pallet::weight(if *keep_alive {T::WeightInfo::burn_allow_death() } else {T::WeightInfo::burn_keep_alive()})] + pub fn burn( + origin: OriginFor, + #[pallet::compact] value: T::Balance, + keep_alive: bool, + ) -> DispatchResult { + let source = ensure_signed(origin)?; + let preservation = if keep_alive { Preserve } else { Expendable }; + >::burn_from( + &source, + value, + preservation, + Precision::Exact, + Polite, + )?; + Ok(()) + } + } + + impl, I: 'static> Pallet { + pub(crate) fn do_store_transfer_proof( + from: &T::AccountId, + to: &T::AccountId, + value: T::Balance, + ) { + if from != to { + let current_count = Self::transfer_count(); + TransferCount::::put(current_count.saturating_add(One::one())); + + TransferProof::::insert((current_count, from.clone(), to.clone(), value), ()); + } + } + + pub(crate) fn transfer_proof_storage_key( + transfer_count: u64, + from: T::AccountId, + to: T::AccountId, + amount: T::Balance, + ) -> Vec { + let key = (transfer_count, from, to, amount); + TransferProof::::hashed_key_for(&key) + } + } + + impl, I: 'static> Pallet { + /// Public function to get the total issuance. + pub fn total_issuance() -> T::Balance { + TotalIssuance::::get() + } + + /// Public function to get the inactive issuance. + pub fn inactive_issuance() -> T::Balance { + InactiveIssuance::::get() + } + + /// Public function to access the Locks storage. + pub fn locks(who: &T::AccountId) -> WeakBoundedVec, T::MaxLocks> { + Locks::::get(who) + } + + /// Public function to access the reserves storage. + pub fn reserves( + who: &T::AccountId, + ) -> BoundedVec, T::MaxReserves> { + Reserves::::get(who) + } + + fn ed() -> T::Balance { + T::ExistentialDeposit::get() + } + /// Ensure the account `who` is using the new logic. + /// + /// Returns `true` if the account did get upgraded, `false` if it didn't need upgrading. + pub fn ensure_upgraded(who: &T::AccountId) -> bool { + let mut a = T::AccountStore::get(who); + if a.flags.is_new_logic() { + return false; + } + a.flags.set_new_logic(); + if !a.reserved.is_zero() && a.frozen.is_zero() { + if system::Pallet::::providers(who) == 0 { + // Gah!! We have no provider refs :( + // This shouldn't practically happen, but we need a failsafe anyway: let's give + // them enough for an ED. + log::warn!( + target: LOG_TARGET, + "account with a non-zero reserve balance has no provider refs, account_id: '{:?}'.", + who + ); + a.free = a.free.max(Self::ed()); + system::Pallet::::inc_providers(who); + } + let _ = system::Pallet::::inc_consumers_without_limit(who).defensive(); + } + // Should never fail - we're only setting a bit. + let _ = T::AccountStore::try_mutate_exists(who, |account| -> DispatchResult { + *account = Some(a); + Ok(()) + }); + Self::deposit_event(Event::Upgraded { who: who.clone() }); + true + } + + /// Get the free balance of an account. + pub fn free_balance(who: impl core::borrow::Borrow) -> T::Balance { + Self::account(who.borrow()).free + } + + /// Get the balance of an account that can be used for transfers, reservations, or any other + /// non-locking, non-transaction-fee activity. Will be at most `free_balance`. + pub fn usable_balance(who: impl core::borrow::Borrow) -> T::Balance { + >::reducible_balance(who.borrow(), Expendable, Polite) + } + + /// Get the balance of an account that can be used for paying transaction fees (not tipping, + /// or any other kind of fees, though). Will be at most `free_balance`. + /// + /// This requires that the account stays alive. + pub fn usable_balance_for_fees(who: impl core::borrow::Borrow) -> T::Balance { + >::reducible_balance(who.borrow(), Protect, Polite) + } + + /// Get the reserved balance of an account. + pub fn reserved_balance(who: impl core::borrow::Borrow) -> T::Balance { + Self::account(who.borrow()).reserved + } + + /// Get both the free and reserved balances of an account. + pub(crate) fn account(who: &T::AccountId) -> AccountData { + T::AccountStore::get(who) + } + + /// Mutate an account to some new value, or delete it entirely with `None`. Will enforce + /// `ExistentialDeposit` law, annulling the account as needed. + /// + /// It returns the result from the closure. Any dust is handled through the low-level + /// `fungible::Unbalanced` trap-door for legacy dust management. + /// + /// NOTE: Doesn't do any preparatory work for creating a new account, so should only be used + /// when it is known that the account already exists. + /// + /// NOTE: LOW-LEVEL: This will not attempt to maintain total issuance. It is expected that + /// the caller will do this. + pub(crate) fn mutate_account_handling_dust( + who: &T::AccountId, + f: impl FnOnce(&mut AccountData) -> R, + ) -> Result { + let (r, maybe_dust) = Self::mutate_account(who, f)?; + if let Some(dust) = maybe_dust { + >::handle_raw_dust(dust); + } + Ok(r) + } + + /// Mutate an account to some new value, or delete it entirely with `None`. Will enforce + /// `ExistentialDeposit` law, annulling the account as needed. + /// + /// It returns the result from the closure. Any dust is handled through the low-level + /// `fungible::Unbalanced` trap-door for legacy dust management. + /// + /// NOTE: Doesn't do any preparatory work for creating a new account, so should only be used + /// when it is known that the account already exists. + /// + /// NOTE: LOW-LEVEL: This will not attempt to maintain total issuance. It is expected that + /// the caller will do this. + pub(crate) fn try_mutate_account_handling_dust>( + who: &T::AccountId, + f: impl FnOnce(&mut AccountData, bool) -> Result, + ) -> Result { + let (r, maybe_dust) = Self::try_mutate_account(who, f)?; + if let Some(dust) = maybe_dust { + >::handle_raw_dust(dust); + } + Ok(r) + } + + /// Mutate an account to some new value, or delete it entirely with `None`. Will enforce + /// `ExistentialDeposit` law, annulling the account as needed. + /// + /// It returns both the result from the closure, and an optional amount of dust + /// which should be handled once it is known that all nested mutates that could affect + /// storage items what the dust handler touches have completed. + /// + /// NOTE: Doesn't do any preparatory work for creating a new account, so should only be used + /// when it is known that the account already exists. + /// + /// NOTE: LOW-LEVEL: This will not attempt to maintain total issuance. It is expected that + /// the caller will do this. + pub(crate) fn mutate_account( + who: &T::AccountId, + f: impl FnOnce(&mut AccountData) -> R, + ) -> Result<(R, Option), DispatchError> { + Self::try_mutate_account(who, |a, _| -> Result { Ok(f(a)) }) + } + + /// Returns `true` when `who` has some providers or `insecure_zero_ed` feature is disabled. + /// Returns `false` otherwise. + #[cfg(not(feature = "insecure_zero_ed"))] + fn have_providers_or_no_zero_ed(_: &T::AccountId) -> bool { + true + } + + /// Returns `true` when `who` has some providers or `insecure_zero_ed` feature is disabled. + /// Returns `false` otherwise. + #[cfg(feature = "insecure_zero_ed")] + fn have_providers_or_no_zero_ed(who: &T::AccountId) -> bool { + frame_system::Pallet::::providers(who) > 0 + } + + /// Mutate an account to some new value, or delete it entirely with `None`. Will enforce + /// `ExistentialDeposit` law, annulling the account as needed. This will do nothing if the + /// result of `f` is an `Err`. + /// + /// It returns both the result from the closure, and an optional amount of dust + /// which should be handled once it is known that all nested mutates that could affect + /// storage items what the dust handler touches have completed. + /// + /// NOTE: Doesn't do any preparatory work for creating a new account, so should only be used + /// when it is known that the account already exists. + /// + /// NOTE: LOW-LEVEL: This will not attempt to maintain total issuance. It is expected that + /// the caller will do this. + pub(crate) fn try_mutate_account>( + who: &T::AccountId, + f: impl FnOnce(&mut AccountData, bool) -> Result, + ) -> Result<(R, Option), E> { + Self::ensure_upgraded(who); + let result = T::AccountStore::try_mutate_exists(who, |maybe_account| { + let is_new = maybe_account.is_none(); + let mut account = maybe_account.take().unwrap_or_default(); + let did_provide = + account.free >= Self::ed() && Self::have_providers_or_no_zero_ed(who); + let did_consume = + !is_new && (!account.reserved.is_zero() || !account.frozen.is_zero()); + + let result = f(&mut account, is_new)?; + + let does_provide = account.free >= Self::ed(); + let does_consume = !account.reserved.is_zero() || !account.frozen.is_zero(); + + if !did_provide && does_provide { + frame_system::Pallet::::inc_providers(who); + } + if did_consume && !does_consume { + frame_system::Pallet::::dec_consumers(who); + } + if !did_consume && does_consume { + frame_system::Pallet::::inc_consumers(who)?; + } + if does_consume && frame_system::Pallet::::consumers(who) == 0 { + // NOTE: This is a failsafe and should not happen for normal accounts. A normal + // account should have gotten a consumer ref in `!did_consume && does_consume` + // at some point. + log::error!(target: LOG_TARGET, "Defensively bumping a consumer ref."); + frame_system::Pallet::::inc_consumers(who)?; + } + if did_provide && !does_provide { + // This could reap the account so must go last. + frame_system::Pallet::::dec_providers(who).inspect_err(|_| { + // best-effort revert consumer change. + if did_consume && !does_consume { + let _ = frame_system::Pallet::::inc_consumers(who).defensive(); + } + if !did_consume && does_consume { + frame_system::Pallet::::dec_consumers(who); + } + })?; + } + + let maybe_endowed = if is_new { Some(account.free) } else { None }; + + // Handle any steps needed after mutating an account. + // + // This includes DustRemoval unbalancing, in the case than the `new` account's total + // balance is non-zero but below ED. + // + // Updates `maybe_account` to `Some` iff the account has sufficient balance. + // Evaluates `maybe_dust`, which is `Some` containing the dust to be dropped, iff + // some dust should be dropped. + // + // We should never be dropping if reserved is non-zero. Reserved being non-zero + // should imply that we have a consumer ref, so this is economically safe. + let ed = Self::ed(); + let maybe_dust = if account.free < ed && account.reserved.is_zero() { + if account.free.is_zero() { + None + } else { + Some(account.free) + } + } else { + assert!( + account.free.is_zero() || account.free >= ed || !account.reserved.is_zero() + ); + *maybe_account = Some(account); + None + }; + Ok((maybe_endowed, maybe_dust, result)) + }); + result.map(|(maybe_endowed, maybe_dust, result)| { + if let Some(endowed) = maybe_endowed { + Self::deposit_event(Event::Endowed { + account: who.clone(), + free_balance: endowed, + }); + } + if let Some(amount) = maybe_dust { + Pallet::::deposit_event(Event::DustLost { account: who.clone(), amount }); + } + (result, maybe_dust) + }) + } + + /// Update the account entry for `who`, given the locks. + pub(crate) fn update_locks(who: &T::AccountId, locks: &[BalanceLock]) { + let bounded_locks = WeakBoundedVec::<_, T::MaxLocks>::force_from( + locks.to_vec(), + Some("Balances Update Locks"), + ); + + if locks.len() as u32 > T::MaxLocks::get() { + log::warn!( + target: LOG_TARGET, + "Warning: A user has more currency locks than expected. \ + A runtime configuration adjustment may be needed." + ); + } + let freezes = Freezes::::get(who); + let mut prev_frozen = Zero::zero(); + let mut after_frozen = Zero::zero(); + // No way this can fail since we do not alter the existential balances. + // TODO: Revisit this assumption. + let res = Self::mutate_account(who, |b| { + prev_frozen = b.frozen; + b.frozen = Zero::zero(); + for l in locks.iter() { + b.frozen = b.frozen.max(l.amount); + } + for l in freezes.iter() { + b.frozen = b.frozen.max(l.amount); + } + after_frozen = b.frozen; + }); + debug_assert!(res.is_ok()); + if let Ok((_, maybe_dust)) = res { + debug_assert!(maybe_dust.is_none(), "Not altering main balance; qed"); + } + + match locks.is_empty() { + true => Locks::::remove(who), + false => Locks::::insert(who, bounded_locks), + } + + match prev_frozen.cmp(&after_frozen) { + cmp::Ordering::Greater => { + let amount = prev_frozen.saturating_sub(after_frozen); + Self::deposit_event(Event::Unlocked { who: who.clone(), amount }); + }, + cmp::Ordering::Less => { + let amount = after_frozen.saturating_sub(prev_frozen); + Self::deposit_event(Event::Locked { who: who.clone(), amount }); + }, + cmp::Ordering::Equal => {}, + } + } + + /// Update the account entry for `who`, given the locks. + pub(crate) fn update_freezes( + who: &T::AccountId, + freezes: BoundedSlice, T::MaxFreezes>, + ) -> DispatchResult { + let mut prev_frozen = Zero::zero(); + let mut after_frozen = Zero::zero(); + let (_, maybe_dust) = Self::mutate_account(who, |b| { + prev_frozen = b.frozen; + b.frozen = Zero::zero(); + for l in Locks::::get(who).iter() { + b.frozen = b.frozen.max(l.amount); + } + for l in freezes.iter() { + b.frozen = b.frozen.max(l.amount); + } + after_frozen = b.frozen; + })?; + debug_assert!(maybe_dust.is_none(), "Not altering main balance; qed"); + if freezes.is_empty() { + Freezes::::remove(who); + } else { + Freezes::::insert(who, freezes); + } + match prev_frozen.cmp(&after_frozen) { + cmp::Ordering::Greater => { + let amount = prev_frozen.saturating_sub(after_frozen); + Self::deposit_event(Event::Thawed { who: who.clone(), amount }); + }, + cmp::Ordering::Less => { + let amount = after_frozen.saturating_sub(prev_frozen); + Self::deposit_event(Event::Frozen { who: who.clone(), amount }); + }, + cmp::Ordering::Equal => {}, + } + Ok(()) + } + + /// Move the reserved balance of one account into the balance of another, according to + /// `status`. This will respect freezes/locks only if `fortitude` is `Polite`. + /// + /// Is a no-op if the value to be moved is zero. + /// + /// NOTE: returns actual amount of transferred value in `Ok` case. + pub(crate) fn do_transfer_reserved( + slashed: &T::AccountId, + beneficiary: &T::AccountId, + value: T::Balance, + precision: Precision, + fortitude: Fortitude, + status: Status, + ) -> Result { + if value.is_zero() { + return Ok(Zero::zero()); + } + + let max = >::reducible_total_balance_on_hold( + slashed, fortitude, + ); + let actual = match precision { + Precision::BestEffort => value.min(max), + Precision::Exact => value, + }; + ensure!(actual <= max, TokenError::FundsUnavailable); + if slashed == beneficiary { + return match status { + Status::Free => Ok(actual.saturating_sub(Self::unreserve(slashed, actual))), + Status::Reserved => Ok(actual), + }; + } + + let ((_, maybe_dust_1), maybe_dust_2) = Self::try_mutate_account( + beneficiary, + |to_account, is_new| -> Result<((), Option), DispatchError> { + ensure!(!is_new, Error::::DeadAccount); + Self::try_mutate_account(slashed, |from_account, _| -> DispatchResult { + match status { + Status::Free => + to_account.free = to_account + .free + .checked_add(&actual) + .ok_or(ArithmeticError::Overflow)?, + Status::Reserved => + to_account.reserved = to_account + .reserved + .checked_add(&actual) + .ok_or(ArithmeticError::Overflow)?, + } + from_account.reserved.saturating_reduce(actual); + Ok(()) + }) + }, + )?; + + if let Some(dust) = maybe_dust_1 { + >::handle_raw_dust(dust); + } + if let Some(dust) = maybe_dust_2 { + >::handle_raw_dust(dust); + } + + Self::deposit_event(Event::ReserveRepatriated { + from: slashed.clone(), + to: beneficiary.clone(), + amount: actual, + destination_status: status, + }); + Ok(actual) + } + } +} diff --git a/pallets/balances/src/migration.rs b/pallets/balances/src/migration.rs new file mode 100644 index 00000000..ac2f7e2f --- /dev/null +++ b/pallets/balances/src/migration.rs @@ -0,0 +1,103 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +use super::*; +use frame_support::{ + pallet_prelude::*, + traits::{OnRuntimeUpgrade, PalletInfoAccess}, + weights::Weight, +}; + +fn migrate_v0_to_v1, I: 'static>(accounts: &[T::AccountId]) -> Weight { + let on_chain_version = Pallet::::on_chain_storage_version(); + + if on_chain_version == 0 { + let total = accounts + .iter() + .map(Pallet::::total_balance) + .fold(T::Balance::zero(), |a, e| a.saturating_add(e)); + Pallet::::deactivate(total); + + // Remove the old `StorageVersion` type. + frame_support::storage::unhashed::kill(&frame_support::storage::storage_prefix( + Pallet::::name().as_bytes(), + "StorageVersion".as_bytes(), + )); + + // Set storage version to `1`. + StorageVersion::new(1).put::>(); + + log::info!(target: LOG_TARGET, "Storage to version 1"); + T::DbWeight::get().reads_writes(2 + accounts.len() as u64, 3) + } else { + log::info!( + target: LOG_TARGET, + "Migration did not execute. This probably should be removed" + ); + T::DbWeight::get().reads(1) + } +} + +// NOTE: This must be used alongside the account whose balance is expected to be inactive. +// Generally this will be used for the XCM teleport checking account. +pub struct MigrateToTrackInactive(PhantomData<(T, A, I)>); +impl, A: Get, I: 'static> OnRuntimeUpgrade + for MigrateToTrackInactive +{ + fn on_runtime_upgrade() -> Weight { + migrate_v0_to_v1::(&[A::get()]) + } +} + +// NOTE: This must be used alongside the accounts whose balance is expected to be inactive. +// Generally this will be used for the XCM teleport checking accounts. +pub struct MigrateManyToTrackInactive(PhantomData<(T, A, I)>); +impl, A: Get>, I: 'static> OnRuntimeUpgrade + for MigrateManyToTrackInactive +{ + fn on_runtime_upgrade() -> Weight { + migrate_v0_to_v1::(&A::get()) + } +} + +pub struct ResetInactive(PhantomData<(T, I)>); +impl, I: 'static> OnRuntimeUpgrade for ResetInactive { + fn on_runtime_upgrade() -> Weight { + let on_chain_version = Pallet::::on_chain_storage_version(); + + if on_chain_version == 1 { + // Remove the old `StorageVersion` type. + frame_support::storage::unhashed::kill(&frame_support::storage::storage_prefix( + Pallet::::name().as_bytes(), + "StorageVersion".as_bytes(), + )); + + InactiveIssuance::::kill(); + + // Set storage version to `0`. + StorageVersion::new(0).put::>(); + + log::info!(target: LOG_TARGET, "Storage to version 0"); + T::DbWeight::get().reads_writes(1, 3) + } else { + log::info!( + target: LOG_TARGET, + "Migration did not execute. This probably should be removed" + ); + T::DbWeight::get().reads(1) + } + } +} diff --git a/pallets/balances/src/tests/currency_tests.rs b/pallets/balances/src/tests/currency_tests.rs new file mode 100644 index 00000000..cf22405a --- /dev/null +++ b/pallets/balances/src/tests/currency_tests.rs @@ -0,0 +1,1643 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Tests regarding the functionality of the `Currency` trait set implementations. + +use super::*; +use crate::{Event, NegativeImbalance}; +use frame_support::{ + traits::{ + BalanceStatus::{Free, Reserved}, + Currency, + ExistenceRequirement::{self, AllowDeath, KeepAlive}, + Hooks, InspectLockableCurrency, LockIdentifier, LockableCurrency, NamedReservableCurrency, + ReservableCurrency, WithdrawReasons, + }, + StorageNoopGuard, +}; +use frame_system::Event as SysEvent; +use sp_runtime::traits::DispatchTransaction; + +const ID_1: LockIdentifier = *b"1 "; +const ID_2: LockIdentifier = *b"2 "; + +pub const CALL: &::RuntimeCall = + &RuntimeCall::Balances(crate::Call::transfer_allow_death { + dest: sp_core::crypto::AccountId32::new([0u8; 32]), + value: 0, + }); + +#[test] +fn ed_should_work() { + ExtBuilder::default().existential_deposit(1).build_and_execute_with(|| { + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), account_id(1), 1000)); + assert_noop!( + >::transfer(&account_id(1), &account_id(10), 1000, KeepAlive), + TokenError::NotExpendable + ); + assert_ok!(>::transfer( + &account_id(1), + &account_id(10), + 1000, + AllowDeath + )); + }); +} + +#[test] +fn set_lock_with_amount_zero_removes_lock() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + Balances::set_lock(ID_1, &account_id(1), Balance::MAX, WithdrawReasons::all()); + Balances::set_lock(ID_1, &account_id(1), 0, WithdrawReasons::all()); + assert_ok!(>::transfer( + &account_id(1), + &account_id(2), + 1, + AllowDeath + )); + }); +} + +#[test] +fn set_lock_with_withdraw_reasons_empty_removes_lock() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + Balances::set_lock(ID_1, &account_id(1), Balance::MAX, WithdrawReasons::all()); + Balances::set_lock(ID_1, &account_id(1), Balance::MAX, WithdrawReasons::empty()); + assert_ok!(>::transfer( + &account_id(1), + &account_id(2), + 1, + AllowDeath + )); + }); +} + +#[test] +fn basic_locking_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + assert_eq!(Balances::free_balance(account_id(1)), 10); + Balances::set_lock(ID_1, &account_id(1), 9, WithdrawReasons::all()); + assert_noop!( + >::transfer(&account_id(1), &account_id(2), 5, AllowDeath), + TokenError::Frozen + ); + }); +} + +#[test] +fn inspect_lock_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + Balances::set_lock(ID_1, &account_id(1), 10, WithdrawReasons::all()); + Balances::set_lock(ID_2, &account_id(1), 10, WithdrawReasons::all()); + Balances::set_lock(ID_1, &account_id(2), 20, WithdrawReasons::all()); + + assert_eq!( + >::balance_locked(ID_1, &account_id(1)), + 10 + ); + assert_eq!( + >::balance_locked(ID_2, &account_id(1)), + 10 + ); + assert_eq!( + >::balance_locked(ID_1, &account_id(2)), + 20 + ); + assert_eq!( + >::balance_locked(ID_2, &account_id(2)), + 0 + ); + assert_eq!( + >::balance_locked(ID_1, &account_id(3)), + 0 + ); + }) +} + +#[test] +fn account_should_be_reaped() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + assert_eq!(Balances::free_balance(account_id(1)), 10); + assert_ok!(>::transfer( + &account_id(1), + &account_id(2), + 10, + AllowDeath + )); + assert_eq!(System::providers(&account_id(1)), 0); + assert_eq!(System::consumers(&account_id(1)), 0); + // Check that the account is dead. + assert!(!frame_system::Account::::contains_key(account_id(1))); + }); +} + +#[test] +fn reap_failed_due_to_provider_and_consumer() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + // SCENARIO: only one provider and there are remaining consumers. + assert_ok!(System::inc_consumers(&account_id(1))); + assert!(!System::can_dec_provider(&account_id(1))); + assert_noop!( + >::transfer(&account_id(1), &account_id(2), 10, AllowDeath), + TokenError::Frozen + ); + assert!(System::account_exists(&account_id(1))); + assert_eq!(Balances::free_balance(account_id(1)), 10); + + // SCENARIO: more than one provider, but will not kill account due to other provider. + assert_eq!(System::inc_providers(&account_id(1)), frame_system::IncRefStatus::Existed); + assert_eq!(System::providers(&account_id(1)), 2); + assert!(System::can_dec_provider(&account_id(1))); + assert_ok!(>::transfer( + &account_id(1), + &account_id(2), + 10, + AllowDeath + )); + assert_eq!(System::providers(&account_id(1)), 1); + assert!(System::account_exists(&account_id(1))); + assert_eq!(Balances::free_balance(account_id(1)), 0); + }); +} + +#[test] +fn partial_locking_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + Balances::set_lock(ID_1, &account_id(1), 5, WithdrawReasons::all()); + assert_ok!(>::transfer( + &account_id(1), + &account_id(2), + 1, + AllowDeath + )); + }); +} + +#[test] +fn lock_removal_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + Balances::set_lock(ID_1, &account_id(1), Balance::MAX, WithdrawReasons::all()); + assert_eq!(System::consumers(&account_id(1)), 1); + Balances::remove_lock(ID_1, &account_id(1)); + assert_eq!(System::consumers(&account_id(1)), 0); + assert_ok!(>::transfer( + &account_id(1), + &account_id(2), + 1, + AllowDeath + )); + }); +} + +#[test] +fn lock_replacement_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + Balances::set_lock(ID_1, &account_id(1), Balance::MAX, WithdrawReasons::all()); + assert_eq!(System::consumers(&account_id(1)), 1); + Balances::set_lock(ID_1, &account_id(1), 5, WithdrawReasons::all()); + assert_eq!(System::consumers(&account_id(1)), 1); + assert_ok!(>::transfer( + &account_id(1), + &account_id(2), + 1, + AllowDeath + )); + }); +} + +#[test] +fn double_locking_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + Balances::set_lock(ID_1, &account_id(1), 5, WithdrawReasons::all()); + assert_eq!(System::consumers(&account_id(1)), 1); + Balances::set_lock(ID_2, &account_id(1), 5, WithdrawReasons::all()); + assert_eq!(System::consumers(&account_id(1)), 1); + assert_ok!(>::transfer( + &account_id(1), + &account_id(2), + 1, + AllowDeath + )); + }); +} + +#[test] +fn combination_locking_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + assert_eq!(System::consumers(&account_id(1)), 0); + Balances::set_lock(ID_1, &account_id(1), Balance::MAX, WithdrawReasons::empty()); + assert_eq!(System::consumers(&account_id(1)), 0); + Balances::set_lock(ID_2, &account_id(1), 0, WithdrawReasons::all()); + assert_eq!(System::consumers(&account_id(1)), 0); + assert_ok!(>::transfer( + &account_id(1), + &account_id(2), + 1, + AllowDeath + )); + }); +} + +#[test] +fn lock_value_extension_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + Balances::set_lock(ID_1, &account_id(1), 5, WithdrawReasons::all()); + assert_eq!(System::consumers(&account_id(1)), 1); + assert_noop!( + >::transfer(&account_id(1), &account_id(2), 6, AllowDeath), + TokenError::Frozen + ); + Balances::extend_lock(ID_1, &account_id(1), 2, WithdrawReasons::all()); + assert_eq!(System::consumers(&account_id(1)), 1); + assert_noop!( + >::transfer(&account_id(1), &account_id(2), 6, AllowDeath), + TokenError::Frozen + ); + Balances::extend_lock(ID_1, &account_id(1), 8, WithdrawReasons::all()); + assert_eq!(System::consumers(&account_id(1)), 1); + assert_noop!( + >::transfer(&account_id(1), &account_id(2), 3, AllowDeath), + TokenError::Frozen + ); + }); +} + +#[test] +fn lock_should_work_reserve() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + pallet_transaction_payment::NextFeeMultiplier::::put( + Multiplier::saturating_from_integer(1), + ); + Balances::set_lock(ID_1, &account_id(1), 10, WithdrawReasons::RESERVE); + assert_noop!( + >::transfer(&account_id(1), &account_id(2), 1, AllowDeath), + TokenError::Frozen + ); + assert_noop!( + Balances::reserve(&account_id(1), 1), + Error::::LiquidityRestrictions, + ); + assert!(ChargeTransactionPayment::::validate_and_prepare( + ChargeTransactionPayment::from(1), + Some(account_id(1)).into(), + CALL, + &crate::tests::info_from_weight(Weight::from_parts(1, 0)), + 1, + 0, + ) + .is_err()); + assert!(ChargeTransactionPayment::::validate_and_prepare( + ChargeTransactionPayment::from(0), + Some(account_id(1)).into(), + CALL, + &crate::tests::info_from_weight(Weight::from_parts(1, 0)), + 1, + 0, + ) + .is_err()); + }); +} + +#[test] +fn lock_should_work_tx_fee() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + Balances::set_lock(ID_1, &account_id(1), 10, WithdrawReasons::TRANSACTION_PAYMENT); + assert_noop!( + >::transfer(&account_id(1), &account_id(2), 1, AllowDeath), + TokenError::Frozen + ); + assert_noop!( + Balances::reserve(&account_id(1), 1), + Error::::LiquidityRestrictions, + ); + assert!(ChargeTransactionPayment::::validate_and_prepare( + ChargeTransactionPayment::from(1), + Some(account_id(1)).into(), + CALL, + &crate::tests::info_from_weight(Weight::from_parts(1, 0)), + 1, + 0, + ) + .is_err()); + assert!(ChargeTransactionPayment::::validate_and_prepare( + ChargeTransactionPayment::from(0), + Some(account_id(1)).into(), + CALL, + &crate::tests::info_from_weight(Weight::from_parts(1, 0)), + 1, + 0, + ) + .is_err()); + }); +} + +#[test] +fn lock_block_number_extension_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + Balances::set_lock(ID_1, &account_id(1), 10, WithdrawReasons::all()); + assert_noop!( + >::transfer(&account_id(1), &account_id(2), 6, AllowDeath), + TokenError::Frozen + ); + Balances::extend_lock(ID_1, &account_id(1), 10, WithdrawReasons::all()); + assert_noop!( + >::transfer(&account_id(1), &account_id(2), 6, AllowDeath), + TokenError::Frozen + ); + System::set_block_number(2); + Balances::extend_lock(ID_1, &account_id(1), 10, WithdrawReasons::all()); + assert_noop!( + >::transfer(&account_id(1), &account_id(2), 3, AllowDeath), + TokenError::Frozen + ); + }); +} + +#[test] +fn lock_reasons_extension_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + Balances::set_lock(ID_1, &account_id(1), 10, WithdrawReasons::TRANSFER); + assert_noop!( + >::transfer(&account_id(1), &account_id(2), 6, AllowDeath), + TokenError::Frozen + ); + Balances::extend_lock(ID_1, &account_id(1), 10, WithdrawReasons::empty()); + assert_noop!( + >::transfer(&account_id(1), &account_id(2), 6, AllowDeath), + TokenError::Frozen + ); + Balances::extend_lock(ID_1, &account_id(1), 10, WithdrawReasons::RESERVE); + assert_noop!( + >::transfer(&account_id(1), &account_id(2), 6, AllowDeath), + TokenError::Frozen + ); + }); +} + +#[test] +fn reserved_balance_should_prevent_reclaim_count() { + ExtBuilder::default() + .existential_deposit(256) + .monied(true) + .build_and_execute_with(|| { + System::inc_account_nonce(account_id(2)); + assert_eq!(Balances::total_balance(&account_id(2)), 256 * 20); + assert_eq!(System::providers(&account_id(2)), 1); + System::inc_providers(&account_id(2)); + assert_eq!(System::providers(&account_id(2)), 2); + + assert_ok!(Balances::reserve(&account_id(2), 256 * 19 + 1)); // account 2 becomes mostly reserved + assert_eq!(System::providers(&account_id(2)), 1); + assert_eq!(Balances::free_balance(account_id(2)), 255); // "free" account would be deleted. + assert_eq!(Balances::total_balance(&account_id(2)), 256 * 20); // reserve still exists. + assert_eq!(System::account_nonce(account_id(2)), 1); + + // account 4 tries to take index 1 for account 5. + assert_ok!(Balances::transfer_allow_death( + Some(account_id(4)).into(), + account_id(5), + 256 + 0x69 + )); + assert_eq!(Balances::total_balance(&account_id(5)), 256 + 0x69); + + assert!(Balances::slash_reserved(&account_id(2), 256 * 19 + 1).1.is_zero()); // account 2 gets slashed + + // "reserve" account reduced to 255 (below ED) so account no longer consuming + assert_ok!(System::dec_providers(&account_id(2))); + assert_eq!(System::providers(&account_id(2)), 0); + // account deleted + assert_eq!(System::account_nonce(account_id(2)), 0); // nonce zero + assert_eq!(Balances::total_balance(&account_id(2)), 0); + + // account 4 tries to take index 1 again for account 6. + assert_ok!(Balances::transfer_allow_death( + Some(account_id(4)).into(), + account_id(6), + 256 + 0x69 + )); + assert_eq!(Balances::total_balance(&account_id(6)), 256 + 0x69); + }); +} + +#[test] +fn reward_should_work() { + ExtBuilder::default().monied(true).build_and_execute_with(|| { + assert_eq!(Balances::total_balance(&account_id(1)), 10); + assert_ok!(Balances::deposit_into_existing(&account_id(1), 10).map(drop)); + assert_eq!( + events(), + [ + RuntimeEvent::Balances(crate::Event::Deposit { who: account_id(1), amount: 10 }), + RuntimeEvent::Balances(crate::Event::Issued { amount: 10 }), + ] + ); + assert_eq!(Balances::total_balance(&account_id(1)), 20); + assert_eq!(pallet_balances::TotalIssuance::::get(), 120); + }); +} + +#[test] +fn balance_works() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&account_id(1), 42); + System::assert_has_event(RuntimeEvent::Balances(crate::Event::Deposit { + who: account_id(1), + amount: 42, + })); + assert_eq!(Balances::free_balance(account_id(1)), 42); + assert_eq!(Balances::reserved_balance(account_id(1)), 0); + assert_eq!(Balances::total_balance(&account_id(1)), 42); + assert_eq!(Balances::free_balance(account_id(2)), 0); + assert_eq!(Balances::reserved_balance(account_id(2)), 0); + assert_eq!(Balances::total_balance(&account_id(2)), 0); + }); +} + +#[test] +fn reserving_balance_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&account_id(1), 111); + + assert_eq!(Balances::total_balance(&account_id(1)), 111); + assert_eq!(Balances::free_balance(account_id(1)), 111); + assert_eq!(Balances::reserved_balance(account_id(1)), 0); + + assert_ok!(Balances::reserve(&account_id(1), 69)); + + assert_eq!(Balances::total_balance(&account_id(1)), 111); + assert_eq!(Balances::free_balance(account_id(1)), 42); + assert_eq!(Balances::reserved_balance(account_id(1)), 69); + }); +} + +#[test] +fn deducting_balance_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&account_id(1), 111); + assert_ok!(Balances::reserve(&account_id(1), 69)); + assert_eq!(Balances::free_balance(account_id(1)), 42); + }); +} + +#[test] +fn refunding_balance_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&account_id(1), 42); + System::set_block_number(2); + Balances::unreserve(&account_id(1), 69); + assert_eq!(Balances::free_balance(account_id(1)), 42); + assert_eq!(Balances::reserved_balance(account_id(1)), 0); + }); +} + +#[test] +fn slashing_balance_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&account_id(1), 111); + assert_ok!(Balances::reserve(&account_id(1), 69)); + assert_eq!(Balances::slash_reserved(&account_id(1), 69).1, 0); + assert_eq!(Balances::free_balance(account_id(1)), 42); + assert_eq!(Balances::reserved_balance(account_id(1)), 0); + assert_eq!(pallet_balances::TotalIssuance::::get(), 42); + }); +} + +#[test] +fn withdrawing_balance_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&account_id(2), 111); + let _ = Balances::withdraw( + &account_id(2), + 11, + WithdrawReasons::TRANSFER, + ExistenceRequirement::KeepAlive, + ); + System::assert_has_event(RuntimeEvent::Balances(crate::Event::Withdraw { + who: account_id(2), + amount: 11, + })); + assert_eq!(Balances::free_balance(account_id(2)), 100); + assert_eq!(pallet_balances::TotalIssuance::::get(), 100); + }); +} + +#[test] +fn withdrawing_balance_should_fail_when_not_expendable() { + ExtBuilder::default().build_and_execute_with(|| { + ExistentialDeposit::set(10); + let _ = Balances::deposit_creating(&account_id(2), 20); + assert_ok!(Balances::reserve(&account_id(2), 5)); + assert_noop!( + Balances::withdraw( + &account_id(2), + 6, + WithdrawReasons::TRANSFER, + ExistenceRequirement::KeepAlive + ), + Error::::Expendability, + ); + assert_ok!(Balances::withdraw( + &account_id(2), + 5, + WithdrawReasons::TRANSFER, + ExistenceRequirement::KeepAlive + ),); + }); +} + +#[test] +fn slashing_incomplete_balance_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&account_id(1), 42); + assert_ok!(Balances::reserve(&account_id(1), 21)); + assert_eq!(Balances::slash_reserved(&account_id(1), 69).1, 48); + assert_eq!(Balances::free_balance(account_id(1)), 21); + assert_eq!(Balances::reserved_balance(account_id(1)), 0); + assert_eq!(pallet_balances::TotalIssuance::::get(), 21); + }); +} + +#[test] +fn unreserving_balance_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&account_id(1), 111); + assert_ok!(Balances::reserve(&account_id(1), 110)); + Balances::unreserve(&account_id(1), 41); + assert_eq!(Balances::reserved_balance(account_id(1)), 69); + assert_eq!(Balances::free_balance(account_id(1)), 42); + }); +} + +#[test] +fn slashing_reserved_balance_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&account_id(1), 112); + assert_ok!(Balances::reserve(&account_id(1), 111)); + assert_eq!(Balances::slash_reserved(&account_id(1), 42).1, 0); + assert_eq!(Balances::reserved_balance(account_id(1)), 69); + assert_eq!(Balances::free_balance(account_id(1)), 1); + assert_eq!(pallet_balances::TotalIssuance::::get(), 70); + }); +} + +#[test] +fn slashing_incomplete_reserved_balance_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&account_id(1), 111); + assert_ok!(Balances::reserve(&account_id(1), 42)); + assert_eq!(Balances::slash_reserved(&account_id(1), 69).1, 27); + assert_eq!(Balances::free_balance(account_id(1)), 69); + assert_eq!(Balances::reserved_balance(account_id(1)), 0); + assert_eq!(pallet_balances::TotalIssuance::::get(), 69); + }); +} + +#[test] +fn repatriating_reserved_balance_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&account_id(1), 111); + let _ = Balances::deposit_creating(&account_id(2), 1); + assert_ok!(Balances::reserve(&account_id(1), 110)); + assert_ok!(Balances::repatriate_reserved(&account_id(1), &account_id(2), 41, Free), 0); + System::assert_last_event(RuntimeEvent::Balances(crate::Event::ReserveRepatriated { + from: account_id(1), + to: account_id(2), + amount: 41, + destination_status: Free, + })); + assert_eq!(Balances::reserved_balance(account_id(1)), 69); + assert_eq!(Balances::free_balance(account_id(1)), 1); + assert_eq!(Balances::reserved_balance(account_id(2)), 0); + assert_eq!(Balances::free_balance(account_id(2)), 42); + }); +} + +#[test] +fn transferring_reserved_balance_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&account_id(1), 111); + let _ = Balances::deposit_creating(&account_id(2), 1); + assert_ok!(Balances::reserve(&account_id(1), 110)); + assert_ok!(Balances::repatriate_reserved(&account_id(1), &account_id(2), 41, Reserved), 0); + assert_eq!(Balances::reserved_balance(account_id(1)), 69); + assert_eq!(Balances::free_balance(account_id(1)), 1); + assert_eq!(Balances::reserved_balance(account_id(2)), 41); + assert_eq!(Balances::free_balance(account_id(2)), 1); + }); +} + +#[test] +fn transferring_reserved_balance_to_yourself_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&account_id(1), 110); + assert_ok!(Balances::reserve(&account_id(1), 50)); + assert_ok!(Balances::repatriate_reserved(&account_id(1), &account_id(1), 50, Free), 0); + assert_eq!(Balances::free_balance(account_id(1)), 110); + assert_eq!(Balances::reserved_balance(account_id(1)), 0); + + assert_ok!(Balances::reserve(&account_id(1), 50)); + assert_ok!(Balances::repatriate_reserved(&account_id(1), &account_id(1), 60, Free), 10); + assert_eq!(Balances::free_balance(account_id(1)), 110); + assert_eq!(Balances::reserved_balance(account_id(1)), 0); + }); +} + +#[test] +fn transferring_reserved_balance_to_nonexistent_should_fail() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&account_id(1), 111); + assert_ok!(Balances::reserve(&account_id(1), 110)); + assert_noop!( + Balances::repatriate_reserved(&account_id(1), &account_id(2), 42, Free), + Error::::DeadAccount + ); + }); +} + +#[test] +fn transferring_incomplete_reserved_balance_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&account_id(1), 110); + let _ = Balances::deposit_creating(&account_id(2), 1); + assert_ok!(Balances::reserve(&account_id(1), 41)); + assert_ok!(Balances::repatriate_reserved(&account_id(1), &account_id(2), 69, Free), 28); + assert_eq!(Balances::reserved_balance(account_id(1)), 0); + assert_eq!(Balances::free_balance(account_id(1)), 69); + assert_eq!(Balances::reserved_balance(account_id(2)), 0); + assert_eq!(Balances::free_balance(account_id(2)), 42); + }); +} + +#[test] +fn transferring_too_high_value_should_not_panic() { + ExtBuilder::default().build_and_execute_with(|| { + Balances::make_free_balance_be(&account_id(1), Balance::MAX); + Balances::make_free_balance_be(&account_id(2), 1); + + assert_err!( + >::transfer( + &account_id(1), + &account_id(2), + Balance::MAX, + AllowDeath + ), + ArithmeticError::Overflow, + ); + + assert_eq!(Balances::free_balance(account_id(1)), Balance::MAX); + assert_eq!(Balances::free_balance(account_id(2)), 1); + }); +} + +#[test] +fn account_create_on_free_too_low_with_other() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + let _ = Balances::deposit_creating(&account_id(1), 100); + assert_eq!(pallet_balances::TotalIssuance::::get(), 100); + + // No-op. + let _ = Balances::deposit_creating(&account_id(2), 50); + assert_eq!(Balances::free_balance(account_id(2)), 0); + assert_eq!(pallet_balances::TotalIssuance::::get(), 100); + }) +} + +#[test] +fn account_create_on_free_too_low() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + // No-op. + let _ = Balances::deposit_creating(&account_id(2), 50); + assert_eq!(Balances::free_balance(account_id(2)), 0); + assert_eq!(pallet_balances::TotalIssuance::::get(), 0); + }) +} + +#[test] +fn account_removal_on_free_too_low() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + assert_eq!(pallet_balances::TotalIssuance::::get(), 0); + + // Setup two accounts with free balance above the existential threshold. + let _ = Balances::deposit_creating(&account_id(1), 110); + let _ = Balances::deposit_creating(&account_id(2), 110); + + assert_eq!(Balances::free_balance(account_id(1)), 110); + assert_eq!(Balances::free_balance(account_id(2)), 110); + assert_eq!(pallet_balances::TotalIssuance::::get(), 220); + + // Transfer funds from account 1 of such amount that after this transfer + // the balance of account 1 will be below the existential threshold. + // This should lead to the removal of all balance of this account. + assert_ok!(Balances::transfer_allow_death(Some(account_id(1)).into(), account_id(2), 20)); + + // Verify free balance removal of account 1. + assert_eq!(Balances::free_balance(account_id(1)), 0); + assert_eq!(Balances::free_balance(account_id(2)), 130); + + // Verify that TotalIssuance tracks balance removal when free balance is too low. + assert_eq!(pallet_balances::TotalIssuance::::get(), 130); + }); +} + +#[test] +fn burn_must_work() { + ExtBuilder::default().monied(true).build_and_execute_with(|| { + let init_total_issuance = pallet_balances::TotalIssuance::::get(); + let imbalance = >::burn(10); + assert_eq!(pallet_balances::TotalIssuance::::get(), init_total_issuance - 10); + drop(imbalance); + assert_eq!(pallet_balances::TotalIssuance::::get(), init_total_issuance); + }); +} + +#[test] +#[should_panic = "the balance of any account should always be at least the existential deposit."] +fn cannot_set_genesis_value_below_ed() { + EXISTENTIAL_DEPOSIT.with(|v| *v.borrow_mut() = 11); + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + crate::GenesisConfig:: { balances: vec![(account_id(1), 10)] } + .assimilate_storage(&mut t) + .unwrap(); +} + +#[test] +#[should_panic = "duplicate balances in genesis."] +fn cannot_set_genesis_value_twice() { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + crate::GenesisConfig:: { + balances: vec![(account_id(1), 10), (account_id(2), 20), (account_id(1), 15)], + } + .assimilate_storage(&mut t) + .unwrap(); +} + +#[test] +fn existential_deposit_respected_when_reserving() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + // Set balance to free and reserved at the existential deposit + assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), account_id(1), 101)); + // Check balance + assert_eq!(Balances::free_balance(account_id(1)), 101); + assert_eq!(Balances::reserved_balance(account_id(1)), 0); + + // Reserve some free balance + assert_ok!(Balances::reserve(&account_id(1), 1)); + // Check balance, the account should be ok. + assert_eq!(Balances::free_balance(account_id(1)), 100); + assert_eq!(Balances::reserved_balance(account_id(1)), 1); + + // Cannot reserve any more of the free balance. + assert_noop!(Balances::reserve(&account_id(1), 1), DispatchError::ConsumerRemaining); + }); +} + +#[test] +fn slash_fails_when_account_needed() { + ExtBuilder::default().existential_deposit(50).build_and_execute_with(|| { + // Set balance to free and reserved at the existential deposit + assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), account_id(1), 52)); + assert_ok!(Balances::reserve(&account_id(1), 1)); + // Check balance + assert_eq!(Balances::free_balance(account_id(1)), 51); + assert_eq!(Balances::reserved_balance(account_id(1)), 1); + + // Slash a small amount + let res = Balances::slash(&account_id(1), 1); + assert_eq!(res, (NegativeImbalance::new(1), 0)); + + // The account should be dead. + assert_eq!(Balances::free_balance(account_id(1)), 50); + assert_eq!(Balances::reserved_balance(account_id(1)), 1); + + // Slashing again doesn't work since we require the ED + let res = Balances::slash(&account_id(1), 1); + assert_eq!(res, (NegativeImbalance::new(0), 1)); + + // The account should be dead. + assert_eq!(Balances::free_balance(account_id(1)), 50); + assert_eq!(Balances::reserved_balance(account_id(1)), 1); + }); +} + +#[test] +fn account_deleted_when_just_dust() { + ExtBuilder::default().existential_deposit(50).build_and_execute_with(|| { + // Set balance to free and reserved at the existential deposit + assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), account_id(1), 50)); + // Check balance + assert_eq!(Balances::free_balance(account_id(1)), 50); + + // Slash a small amount + let res = Balances::slash(&account_id(1), 1); + assert_eq!(res, (NegativeImbalance::new(1), 0)); + + // The account should be dead. + assert_eq!(Balances::free_balance(account_id(1)), 0); + }); +} + +#[test] +fn emit_events_with_reserve_and_unreserve() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&account_id(1), 100); + + System::set_block_number(2); + assert_ok!(Balances::reserve(&account_id(1), 10)); + + System::assert_last_event(RuntimeEvent::Balances(crate::Event::Reserved { + who: account_id(1), + amount: 10, + })); + + System::set_block_number(3); + assert!(Balances::unreserve(&account_id(1), 5).is_zero()); + + System::assert_last_event(RuntimeEvent::Balances(crate::Event::Unreserved { + who: account_id(1), + amount: 5, + })); + + System::set_block_number(4); + assert_eq!(Balances::unreserve(&account_id(1), 6), 1); + + // should only unreserve 5 + System::assert_last_event(RuntimeEvent::Balances(crate::Event::Unreserved { + who: account_id(1), + amount: 5, + })); + }); +} + +#[test] +fn emit_events_with_changing_locks() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&account_id(1), 100); + System::reset_events(); + + // Locks = [] --> [10] + Balances::set_lock(*b"LOCK_000", &account_id(1), 10, WithdrawReasons::TRANSFER); + assert_eq!( + events(), + [RuntimeEvent::Balances(crate::Event::Locked { who: account_id(1), amount: 10 })] + ); + + // Locks = [10] --> [15] + Balances::set_lock(*b"LOCK_000", &account_id(1), 15, WithdrawReasons::TRANSFER); + assert_eq!( + events(), + [RuntimeEvent::Balances(crate::Event::Locked { who: account_id(1), amount: 5 })] + ); + + // Locks = [15] --> [15, 20] + Balances::set_lock(*b"LOCK_001", &account_id(1), 20, WithdrawReasons::TRANSACTION_PAYMENT); + assert_eq!( + events(), + [RuntimeEvent::Balances(crate::Event::Locked { who: account_id(1), amount: 5 })] + ); + + // Locks = [15, 20] --> [17, 20] + Balances::set_lock(*b"LOCK_000", &account_id(1), 17, WithdrawReasons::TRANSACTION_PAYMENT); + for event in events() { + match event { + RuntimeEvent::Balances(crate::Event::Locked { .. }) => { + assert!(false, "unexpected lock event") + }, + RuntimeEvent::Balances(crate::Event::Unlocked { .. }) => { + assert!(false, "unexpected unlock event") + }, + _ => continue, + } + } + + // Locks = [17, 20] --> [17, 15] + Balances::set_lock(*b"LOCK_001", &account_id(1), 15, WithdrawReasons::TRANSFER); + assert_eq!( + events(), + [RuntimeEvent::Balances(crate::Event::Unlocked { who: account_id(1), amount: 3 })] + ); + + // Locks = [17, 15] --> [15] + Balances::remove_lock(*b"LOCK_000", &account_id(1)); + assert_eq!( + events(), + [RuntimeEvent::Balances(crate::Event::Unlocked { who: account_id(1), amount: 2 })] + ); + + // Locks = [15] --> [] + Balances::remove_lock(*b"LOCK_001", &account_id(1)); + assert_eq!( + events(), + [RuntimeEvent::Balances(crate::Event::Unlocked { who: account_id(1), amount: 15 })] + ); + }); +} + +#[test] +fn emit_events_with_existential_deposit() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), account_id(1), 100)); + + assert_eq!( + events(), + [ + RuntimeEvent::System(system::Event::NewAccount { account: account_id(1) }), + RuntimeEvent::Balances(crate::Event::Endowed { + account: account_id(1), + free_balance: 100 + }), + RuntimeEvent::Balances(crate::Event::Issued { amount: 100 }), + RuntimeEvent::Balances(crate::Event::BalanceSet { who: account_id(1), free: 100 }), + ] + ); + + let res = Balances::slash(&account_id(1), 1); + assert_eq!(res, (NegativeImbalance::new(1), 0)); + + assert_eq!( + events(), + [ + RuntimeEvent::System(system::Event::KilledAccount { account: account_id(1) }), + RuntimeEvent::Balances(crate::Event::DustLost { + account: account_id(1), + amount: 99 + }), + RuntimeEvent::Balances(crate::Event::Slashed { who: account_id(1), amount: 1 }), + RuntimeEvent::Balances(crate::Event::Rescinded { amount: 1 }), + ] + ); + }); +} + +#[test] +fn emit_events_with_no_existential_deposit_suicide() { + ExtBuilder::default().existential_deposit(1).build_and_execute_with(|| { + Balances::make_free_balance_be(&account_id(1), 100); + + assert_eq!( + events(), + [ + RuntimeEvent::Balances(crate::Event::BalanceSet { who: account_id(1), free: 100 }), + RuntimeEvent::System(system::Event::NewAccount { account: account_id(1) }), + RuntimeEvent::Balances(crate::Event::Endowed { + account: account_id(1), + free_balance: 100 + }), + RuntimeEvent::Balances(crate::Event::Issued { amount: 100 }), + ] + ); + + let res = Balances::slash(&account_id(1), 100); + assert_eq!(res, (NegativeImbalance::new(100), 0)); + + assert_eq!( + events(), + [ + RuntimeEvent::System(system::Event::KilledAccount { account: account_id(1) }), + RuntimeEvent::Balances(crate::Event::Slashed { who: account_id(1), amount: 100 }), + RuntimeEvent::Balances(crate::Event::Rescinded { amount: 100 }), + ] + ); + }); +} + +#[test] +fn slash_over_works() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + // SCENARIO: Over-slash will kill account, and report missing slash amount. + Balances::make_free_balance_be(&account_id(1), 1_000); + // Slashed full free_balance, and reports 300 not slashed + assert_eq!(Balances::slash(&account_id(1), 1_300), (NegativeImbalance::new(1000), 300)); + // Account is dead + assert!(!System::account_exists(&account_id(1))); + }); +} + +#[test] +fn slash_full_works() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + Balances::make_free_balance_be(&account_id(1), 1_000); + // Slashed completed in full + assert_eq!(Balances::slash(&account_id(1), 1_000), (NegativeImbalance::new(1000), 0)); + // Account is still alive + assert!(!System::account_exists(&account_id(1))); + System::assert_has_event(RuntimeEvent::Balances(crate::Event::Slashed { + who: account_id(1), + amount: 1000, + })); + }); +} + +#[test] +fn slash_partial_works() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + Balances::make_free_balance_be(&account_id(1), 1_000); + // Slashed completed in full + assert_eq!(Balances::slash(&account_id(1), 900), (NegativeImbalance::new(900), 0)); + // Account is still alive + assert!(System::account_exists(&account_id(1))); + System::assert_has_event(RuntimeEvent::Balances(crate::Event::Slashed { + who: account_id(1), + amount: 900, + })); + }); +} + +#[test] +fn slash_dusting_works() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + Balances::make_free_balance_be(&account_id(1), 1_000); + // Slashed completed in full + assert_eq!(Balances::slash(&account_id(1), 950), (NegativeImbalance::new(950), 0)); + assert!(!System::account_exists(&account_id(1))); + System::assert_has_event(RuntimeEvent::Balances(crate::Event::Slashed { + who: account_id(1), + amount: 950, + })); + }); +} + +#[test] +fn slash_does_not_take_from_reserve() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + Balances::make_free_balance_be(&account_id(1), 1_000); + assert_ok!(Balances::reserve(&account_id(1), 100)); + // Slashed completed in full + assert_eq!(Balances::slash(&account_id(1), 900), (NegativeImbalance::new(800), 100)); + assert_eq!(Balances::reserved_balance(account_id(1)), 100); + System::assert_has_event(RuntimeEvent::Balances(crate::Event::Slashed { + who: account_id(1), + amount: 800, + })); + }); +} + +#[test] +fn slash_consumed_slash_full_works() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + Balances::make_free_balance_be(&account_id(1), 1_000); + assert_ok!(System::inc_consumers(&account_id(1))); // <-- Reference counter added here is enough for all tests + // Slashed completed in full + assert_eq!(Balances::slash(&account_id(1), 900), (NegativeImbalance::new(900), 0)); + // Account is still alive + assert!(System::account_exists(&account_id(1))); + }); +} + +#[test] +fn slash_consumed_slash_over_works() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + Balances::make_free_balance_be(&account_id(1), 1_000); + assert_ok!(System::inc_consumers(&account_id(1))); // <-- Reference counter added here is enough for all tests + // Slashed completed in full + assert_eq!(Balances::slash(&account_id(1), 1_000), (NegativeImbalance::new(900), 100)); + // Account is still alive + assert!(System::account_exists(&account_id(1))); + }); +} + +#[test] +fn slash_consumed_slash_partial_works() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + Balances::make_free_balance_be(&account_id(1), 1_000); + assert_ok!(System::inc_consumers(&account_id(1))); // <-- Reference counter added here is enough for all tests + // Slashed completed in full + assert_eq!(Balances::slash(&account_id(1), 800), (NegativeImbalance::new(800), 0)); + // Account is still alive + assert!(System::account_exists(&account_id(1))); + }); +} + +#[test] +fn slash_on_non_existent_works() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + // Slash on non-existent account is okay. + assert_eq!(Balances::slash(&account_id(123), 1_300), (NegativeImbalance::new(0), 1300)); + }); +} + +#[test] +fn slash_reserved_slash_partial_works() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + Balances::make_free_balance_be(&account_id(1), 1_000); + assert_ok!(Balances::reserve(&account_id(1), 900)); + // Slashed completed in full + assert_eq!(Balances::slash_reserved(&account_id(1), 800), (NegativeImbalance::new(800), 0)); + assert_eq!(System::consumers(&account_id(1)), 1); + assert_eq!(Balances::reserved_balance(account_id(1)), 100); + assert_eq!(Balances::free_balance(account_id(1)), 100); + }); +} + +#[test] +fn slash_reserved_slash_everything_works() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + Balances::make_free_balance_be(&account_id(1), 1_000); + assert_ok!(Balances::reserve(&account_id(1), 900)); + assert_eq!(System::consumers(&account_id(1)), 1); + // Slashed completed in full + assert_eq!(Balances::slash_reserved(&account_id(1), 900), (NegativeImbalance::new(900), 0)); + assert_eq!(System::consumers(&account_id(1)), 0); + // Account is still alive + assert!(System::account_exists(&account_id(1))); + }); +} + +#[test] +fn slash_reserved_overslash_does_not_touch_free_balance() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + // SCENARIO: Over-slash doesn't touch free balance. + Balances::make_free_balance_be(&account_id(1), 1_000); + assert_ok!(Balances::reserve(&account_id(1), 800)); + // Slashed done + assert_eq!( + Balances::slash_reserved(&account_id(1), 900), + (NegativeImbalance::new(800), 100) + ); + assert_eq!(Balances::free_balance(account_id(1)), 200); + }); +} + +#[test] +fn slash_reserved_on_non_existent_works() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + // Slash on non-existent account is okay. + assert_eq!( + Balances::slash_reserved(&account_id(123), 1_300), + (NegativeImbalance::new(0), 1300) + ); + }); +} + +#[test] +fn operations_on_dead_account_should_not_change_state() { + // These functions all use `mutate_account` which may introduce a storage change when + // the account never existed to begin with, and shouldn't exist in the end. + ExtBuilder::default().existential_deposit(1).build_and_execute_with(|| { + assert!(!frame_system::Account::::contains_key(account_id(137))); + + // Unreserve + assert_storage_noop!(assert_eq!(Balances::unreserve(&account_id(137), 42), 42)); + // Reserve + assert_noop!( + Balances::reserve(&account_id(137), 42), + Error::::InsufficientBalance + ); + // Slash Reserve + assert_storage_noop!(assert_eq!(Balances::slash_reserved(&account_id(137), 42).1, 42)); + // Repatriate Reserve + assert_noop!( + Balances::repatriate_reserved(&account_id(137), &account_id(138), 42, Free), + Error::::DeadAccount + ); + // Slash + assert_storage_noop!(assert_eq!(Balances::slash(&account_id(137), 42).1, 42)); + }); +} + +#[test] +#[should_panic = "The existential deposit must be greater than zero!"] +fn zero_ed_is_prohibited() { + // These functions all use `mutate_account` which may introduce a storage change when + // the account never existed to begin with, and shouldn't exist in the end. + ExtBuilder::default().existential_deposit(0).build_and_execute_with(|| { + Balances::integrity_test(); + }); +} + +#[test] +fn named_reserve_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&account_id(1), 111); + + let id_1 = TestId::Foo; + let id_2 = TestId::Bar; + let id_3 = TestId::Baz; + + // reserve + + assert_noop!( + Balances::reserve_named(&id_1, &account_id(1), 112), + Error::::InsufficientBalance + ); + + assert_ok!(Balances::reserve_named(&id_1, &account_id(1), 12)); + + assert_eq!(Balances::reserved_balance(account_id(1)), 12); + assert_eq!(Balances::reserved_balance_named(&id_1, &account_id(1)), 12); + assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(1)), 0); + + assert_ok!(Balances::reserve_named(&id_1, &account_id(1), 2)); + + assert_eq!(Balances::reserved_balance(account_id(1)), 14); + assert_eq!(Balances::reserved_balance_named(&id_1, &account_id(1)), 14); + assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(1)), 0); + + assert_ok!(Balances::reserve_named(&id_2, &account_id(1), 23)); + + assert_eq!(Balances::reserved_balance(account_id(1)), 37); + assert_eq!(Balances::reserved_balance_named(&id_1, &account_id(1)), 14); + assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(1)), 23); + + assert_ok!(Balances::reserve(&account_id(1), 34)); + + assert_eq!(Balances::reserved_balance(account_id(1)), 71); + assert_eq!(Balances::reserved_balance_named(&id_1, &account_id(1)), 14); + assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(1)), 23); + + assert_eq!(Balances::total_balance(&account_id(1)), 111); + assert_eq!(Balances::free_balance(account_id(1)), 40); + + assert_noop!( + Balances::reserve_named(&id_3, &account_id(1), 2), + Error::::TooManyReserves + ); + + // unreserve + + assert_eq!(Balances::unreserve_named(&id_1, &account_id(1), 10), 0); + + assert_eq!(Balances::reserved_balance(account_id(1)), 61); + assert_eq!(Balances::reserved_balance_named(&id_1, &account_id(1)), 4); + assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(1)), 23); + + assert_eq!(Balances::unreserve_named(&id_1, &account_id(1), 5), 1); + + assert_eq!(Balances::reserved_balance(account_id(1)), 57); + assert_eq!(Balances::reserved_balance_named(&id_1, &account_id(1)), 0); + assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(1)), 23); + + assert_eq!(Balances::unreserve_named(&id_2, &account_id(1), 3), 0); + + assert_eq!(Balances::reserved_balance(account_id(1)), 54); + assert_eq!(Balances::reserved_balance_named(&id_1, &account_id(1)), 0); + assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(1)), 20); + + assert_eq!(Balances::total_balance(&account_id(1)), 111); + assert_eq!(Balances::free_balance(account_id(1)), 57); + + // slash_reserved_named + + assert_ok!(Balances::reserve_named(&id_1, &account_id(1), 10)); + + assert_eq!(Balances::slash_reserved_named(&id_1, &account_id(1), 25).1, 15); + + assert_eq!(Balances::reserved_balance(account_id(1)), 54); + assert_eq!(Balances::reserved_balance_named(&id_1, &account_id(1)), 0); + assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(1)), 20); + assert_eq!(Balances::total_balance(&account_id(1)), 101); + + assert_eq!(Balances::slash_reserved_named(&id_2, &account_id(1), 5).1, 0); + + assert_eq!(Balances::reserved_balance(account_id(1)), 49); + assert_eq!(Balances::reserved_balance_named(&id_1, &account_id(1)), 0); + assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(1)), 15); + assert_eq!(Balances::total_balance(&account_id(1)), 96); + + // repatriate_reserved_named + + let _ = Balances::deposit_creating(&account_id(2), 100); + + assert_eq!( + Balances::repatriate_reserved_named( + &id_2, + &account_id(1), + &account_id(2), + 10, + Reserved + ) + .unwrap(), + 0 + ); + + assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(1)), 5); + assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(2)), 10); + assert_eq!(Balances::reserved_balance(account_id(2)), 10); + + assert_eq!( + Balances::repatriate_reserved_named( + &id_2, + &account_id(2), + &account_id(1), + 11, + Reserved + ) + .unwrap(), + 1 + ); + + assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(1)), 15); + assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(2)), 0); + assert_eq!(Balances::reserved_balance(account_id(2)), 0); + + assert_eq!( + Balances::repatriate_reserved_named(&id_2, &account_id(1), &account_id(2), 10, Free) + .unwrap(), + 0 + ); + assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(1)), 5); + assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(2)), 0); + assert_eq!(Balances::free_balance(account_id(2)), 110); + + // repatriate_reserved_named to self + + assert_eq!( + Balances::repatriate_reserved_named( + &id_2, + &account_id(1), + &account_id(1), + 10, + Reserved + ) + .unwrap(), + 5 + ); + assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(1)), 5); + + assert_eq!(Balances::free_balance(account_id(1)), 47); + + assert_eq!( + Balances::repatriate_reserved_named(&id_2, &account_id(1), &account_id(1), 15, Free) + .unwrap(), + 10 + ); + assert_eq!(Balances::reserved_balance_named(&id_2, &account_id(1)), 0); + + assert_eq!(Balances::free_balance(account_id(1)), 52); + }); +} + +#[test] +fn reserve_must_succeed_if_can_reserve_does() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&account_id(1), 1); + let _ = Balances::deposit_creating(&account_id(2), 2); + assert!( + Balances::can_reserve(&account_id(1), 1) == + Balances::reserve(&account_id(1), 1).is_ok() + ); + assert!( + Balances::can_reserve(&account_id(2), 1) == + Balances::reserve(&account_id(2), 1).is_ok() + ); + }); +} + +#[test] +fn reserved_named_to_yourself_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&account_id(1), 110); + + let id = TestId::Foo; + + assert_ok!(Balances::reserve_named(&id, &account_id(1), 50)); + assert_ok!( + Balances::repatriate_reserved_named(&id, &account_id(1), &account_id(1), 50, Free), + 0 + ); + assert_eq!(Balances::free_balance(account_id(1)), 110); + assert_eq!(Balances::reserved_balance_named(&id, &account_id(1)), 0); + + assert_ok!(Balances::reserve_named(&id, &account_id(1), 50)); + assert_ok!( + Balances::repatriate_reserved_named(&id, &account_id(1), &account_id(1), 60, Free), + 10 + ); + assert_eq!(Balances::free_balance(account_id(1)), 110); + assert_eq!(Balances::reserved_balance_named(&id, &account_id(1)), 0); + }); +} + +#[test] +fn ensure_reserved_named_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&account_id(1), 111); + + let id = TestId::Foo; + + assert_ok!(Balances::ensure_reserved_named(&id, &account_id(1), 15)); + assert_eq!(Balances::reserved_balance_named(&id, &account_id(1)), 15); + + assert_ok!(Balances::ensure_reserved_named(&id, &account_id(1), 10)); + assert_eq!(Balances::reserved_balance_named(&id, &account_id(1)), 10); + + assert_ok!(Balances::ensure_reserved_named(&id, &account_id(1), 20)); + assert_eq!(Balances::reserved_balance_named(&id, &account_id(1)), 20); + }); +} + +#[test] +fn unreserve_all_named_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&account_id(1), 111); + + let id = TestId::Foo; + + assert_ok!(Balances::reserve_named(&id, &account_id(1), 15)); + + assert_eq!(Balances::unreserve_all_named(&id, &account_id(1)), 15); + assert_eq!(Balances::reserved_balance_named(&id, &account_id(1)), 0); + assert_eq!(Balances::free_balance(account_id(1)), 111); + + assert_eq!(Balances::unreserve_all_named(&id, &account_id(1)), 0); + }); +} + +#[test] +fn slash_all_reserved_named_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&account_id(1), 111); + + let id = TestId::Foo; + + assert_ok!(Balances::reserve_named(&id, &account_id(1), 15)); + + assert_eq!(Balances::slash_all_reserved_named(&id, &account_id(1)).peek(), 15); + assert_eq!(Balances::reserved_balance_named(&id, &account_id(1)), 0); + assert_eq!(Balances::free_balance(account_id(1)), 96); + + assert_eq!(Balances::slash_all_reserved_named(&id, &account_id(1)).peek(), 0); + }); +} + +#[test] +fn repatriate_all_reserved_named_should_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::deposit_creating(&account_id(1), 111); + let _ = Balances::deposit_creating(&account_id(2), 10); + let _ = Balances::deposit_creating(&account_id(3), 10); + + let id = TestId::Foo; + + assert_ok!(Balances::reserve_named(&id, &account_id(1), 15)); + + assert_ok!(Balances::repatriate_all_reserved_named( + &id, + &account_id(1), + &account_id(2), + Reserved + )); + assert_eq!(Balances::reserved_balance_named(&id, &account_id(1)), 0); + assert_eq!(Balances::reserved_balance_named(&id, &account_id(2)), 15); + + assert_ok!(Balances::repatriate_all_reserved_named( + &id, + &account_id(2), + &account_id(3), + Free + )); + assert_eq!(Balances::reserved_balance_named(&id, &account_id(2)), 0); + assert_eq!(Balances::free_balance(account_id(3)), 25); + }); +} + +#[test] +fn freezing_and_locking_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + // Consumer is shared between freezing and locking. + assert_eq!(System::consumers(&account_id(1)), 0); + assert_ok!(>::set_freeze( + &TestId::Foo, + &account_id(1), + 4 + )); + assert_eq!(System::consumers(&account_id(1)), 1); + Balances::set_lock(ID_1, &account_id(1), 5, WithdrawReasons::all()); + assert_eq!(System::consumers(&account_id(1)), 1); + + // Frozen and locked balances update correctly. + assert_eq!(Balances::account(&account_id(1)).frozen, 5); + assert_ok!(>::set_freeze( + &TestId::Foo, + &account_id(1), + 6 + )); + assert_eq!(Balances::account(&account_id(1)).frozen, 6); + assert_ok!(>::set_freeze( + &TestId::Foo, + &account_id(1), + 4 + )); + assert_eq!(Balances::account(&account_id(1)).frozen, 5); + Balances::set_lock(ID_1, &account_id(1), 3, WithdrawReasons::all()); + assert_eq!(Balances::account(&account_id(1)).frozen, 4); + Balances::set_lock(ID_1, &account_id(1), 5, WithdrawReasons::all()); + assert_eq!(Balances::account(&account_id(1)).frozen, 5); + + // Locks update correctly. + Balances::remove_lock(ID_1, &account_id(1)); + assert_eq!(Balances::account(&account_id(1)).frozen, 4); + assert_ok!(>::thaw(&TestId::Foo, &account_id(1))); + assert_eq!(Balances::account(&account_id(1)).frozen, 0); + assert_eq!(System::consumers(&account_id(1)), 0); + }); +} + +#[test] +fn self_transfer_noop() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + assert_eq!(pallet_balances::TotalIssuance::::get(), 0); + let _ = Balances::deposit_creating(&account_id(1), 100); + + // The account is set up properly: + assert_eq!( + events(), + [ + Event::Deposit { who: account_id(1), amount: 100 }.into(), + SysEvent::NewAccount { account: account_id(1) }.into(), + Event::Endowed { account: account_id(1), free_balance: 100 }.into(), + Event::Issued { amount: 100 }.into(), + ] + ); + assert_eq!(Balances::free_balance(account_id(1)), 100); + assert_eq!(pallet_balances::TotalIssuance::::get(), 100); + + // Transfers to self are No-OPs: + let _g = StorageNoopGuard::new(); + for i in 0..200 { + let r = Balances::transfer_allow_death(Some(account_id(1)).into(), account_id(1), i); + + if i <= 100 { + assert_ok!(r); + } else { + assert!(r.is_err()); + } + + assert!(events().is_empty()); + assert_eq!( + Balances::free_balance(account_id(1)), + 100, + "Balance unchanged by self transfer" + ); + assert_eq!( + pallet_balances::TotalIssuance::::get(), + 100, + "TI unchanged by self transfers" + ); + } + }); +} diff --git a/pallets/balances/src/tests/dispatchable_tests.rs b/pallets/balances/src/tests/dispatchable_tests.rs new file mode 100644 index 00000000..94991c96 --- /dev/null +++ b/pallets/balances/src/tests/dispatchable_tests.rs @@ -0,0 +1,410 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Tests regarding the functionality of the dispatchables/extrinsics. + +use super::*; +use crate::{ + AdjustmentDirection::{Decrease as Dec, Increase as Inc}, + Event, +}; +use frame_support::traits::{fungible::Unbalanced, tokens::Preservation::Expendable}; +use fungible::{hold::Mutate as HoldMutate, Inspect, Mutate}; + +/// Alice account ID for more readable tests. +fn alice() -> AccountId { + account_id(1) +} + +#[test] +fn default_indexing_on_new_accounts_should_not_work2() { + ExtBuilder::default() + .existential_deposit(10) + .monied(true) + .build_and_execute_with(|| { + // account 5 should not exist + // ext_deposit is 10, value is 9, not satisfies for ext_deposit + assert_noop!( + Balances::transfer_allow_death(Some(account_id(1)).into(), account_id(5), 9), + TokenError::BelowMinimum, + ); + assert_eq!(Balances::free_balance(account_id(1)), 100); + }); +} + +#[test] +fn dust_account_removal_should_work() { + ExtBuilder::default() + .existential_deposit(100) + .monied(true) + .build_and_execute_with(|| { + System::inc_account_nonce(account_id(2)); + assert_eq!(System::account_nonce(account_id(2)), 1); + assert_eq!(Balances::total_balance(&account_id(2)), 2000); + // index 1 (account 2) becomes zombie + assert_ok!(Balances::transfer_allow_death( + Some(account_id(2)).into(), + account_id(5), + 1901 + )); + assert_eq!(Balances::total_balance(&account_id(2)), 0); + assert_eq!(Balances::total_balance(&account_id(5)), 1901); + assert_eq!(System::account_nonce(account_id(2)), 0); + }); +} + +#[test] +fn balance_transfer_works() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::mint_into(&account_id(1), 111); + assert_ok!(Balances::transfer_allow_death(Some(account_id(1)).into(), account_id(2), 69)); + assert_eq!(Balances::total_balance(&account_id(1)), 42); + assert_eq!(Balances::total_balance(&account_id(2)), 69); + }); +} + +#[test] +fn force_transfer_works() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::mint_into(&account_id(1), 111); + assert_noop!( + Balances::force_transfer(Some(account_id(2)).into(), account_id(1), account_id(2), 69), + BadOrigin, + ); + assert_ok!(Balances::force_transfer( + RawOrigin::Root.into(), + account_id(1), + account_id(2), + 69 + )); + assert_eq!(Balances::total_balance(&account_id(1)), 42); + assert_eq!(Balances::total_balance(&account_id(2)), 69); + }); +} + +#[test] +fn balance_transfer_when_on_hold_should_not_work() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::mint_into(&account_id(1), 111); + assert_ok!(Balances::hold(&TestId::Foo, &account_id(1), 69)); + assert_noop!( + Balances::transfer_allow_death(Some(account_id(1)).into(), account_id(2), 69), + TokenError::FundsUnavailable, + ); + }); +} + +#[test] +fn transfer_keep_alive_works() { + ExtBuilder::default().existential_deposit(1).build_and_execute_with(|| { + let _ = Balances::mint_into(&account_id(1), 100); + assert_noop!( + Balances::transfer_keep_alive(Some(account_id(1)).into(), account_id(2), 100), + TokenError::NotExpendable + ); + assert_eq!(Balances::total_balance(&account_id(1)), 100); + assert_eq!(Balances::total_balance(&account_id(2)), 0); + }); +} + +#[test] +fn transfer_keep_alive_all_free_succeed() { + ExtBuilder::default().existential_deposit(100).build_and_execute_with(|| { + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), account_id(1), 300)); + assert_ok!(Balances::hold(&TestId::Foo, &account_id(1), 100)); + assert_ok!(Balances::transfer_keep_alive(Some(account_id(1)).into(), account_id(2), 100)); + assert_eq!(Balances::total_balance(&account_id(1)), 200); + assert_eq!(Balances::total_balance(&account_id(2)), 100); + }); +} + +#[test] +fn transfer_all_works_1() { + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + // setup + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), account_id(1), 200)); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), account_id(2), 0)); + // transfer all and allow death + assert_ok!(Balances::transfer_all(Some(account_id(1)).into(), account_id(2), false)); + assert_eq!(Balances::total_balance(&account_id(1)), 0); + assert_eq!(Balances::total_balance(&account_id(2)), 200); + }); +} + +#[test] +fn transfer_all_works_2() { + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + // setup + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), account_id(1), 200)); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), account_id(2), 0)); + // transfer all and keep alive + assert_ok!(Balances::transfer_all(Some(account_id(1)).into(), account_id(2), true)); + assert_eq!(Balances::total_balance(&account_id(1)), 100); + assert_eq!(Balances::total_balance(&account_id(2)), 100); + }); +} + +#[test] +fn transfer_all_works_3() { + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + // setup + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), account_id(1), 210)); + assert_ok!(Balances::hold(&TestId::Foo, &account_id(1), 10)); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), account_id(2), 0)); + // transfer all and allow death w/ reserved + assert_ok!(Balances::transfer_all(Some(account_id(1)).into(), account_id(2), false)); + assert_eq!(Balances::total_balance(&account_id(1)), 110); + assert_eq!(Balances::total_balance(&account_id(2)), 100); + }); +} + +#[test] +fn transfer_all_works_4() { + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + // setup + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), account_id(1), 210)); + assert_ok!(Balances::hold(&TestId::Foo, &account_id(1), 10)); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), account_id(2), 0)); + // transfer all and keep alive w/ reserved + assert_ok!(Balances::transfer_all(Some(account_id(1)).into(), account_id(2), true)); + assert_eq!(Balances::total_balance(&account_id(1)), 110); + assert_eq!(Balances::total_balance(&account_id(2)), 100); + }); +} + +#[test] +fn set_balance_handles_killing_account() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::mint_into(&account_id(1), 111); + assert_ok!(frame_system::Pallet::::inc_consumers(&account_id(1))); + assert_noop!( + Balances::force_set_balance(RuntimeOrigin::root(), account_id(1), 0), + DispatchError::ConsumerRemaining, + ); + }); +} + +#[test] +fn set_balance_handles_total_issuance() { + ExtBuilder::default().build_and_execute_with(|| { + let old_total_issuance = pallet_balances::TotalIssuance::::get(); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), account_id(137), 69)); + assert_eq!(pallet_balances::TotalIssuance::::get(), old_total_issuance + 69); + assert_eq!(Balances::total_balance(&account_id(137)), 69); + assert_eq!(Balances::free_balance(account_id(137)), 69); + }); +} + +#[test] +fn upgrade_accounts_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + System::inc_providers(&account_id(7)); + assert_ok!(::AccountStore::try_mutate_exists( + &account_id(7), + |a| -> DispatchResult { + *a = Some(AccountData { + free: 5, + reserved: 5, + frozen: Zero::zero(), + flags: crate::types::ExtraFlags::old_logic(), + }); + Ok(()) + } + )); + assert!(!Balances::account(&account_id(7)).flags.is_new_logic()); + assert_eq!(System::providers(&account_id(7)), 1); + assert_eq!(System::consumers(&account_id(7)), 0); + assert_ok!(Balances::upgrade_accounts(Some(account_id(1)).into(), vec![account_id(7)])); + assert!(Balances::account(&account_id(7)).flags.is_new_logic()); + assert_eq!(System::providers(&account_id(7)), 1); + assert_eq!(System::consumers(&account_id(7)), 1); + + >::unreserve( + &account_id(7), + 5, + ); + assert_ok!(>::transfer( + &account_id(7), + &account_id(1), + 10, + Expendable + )); + assert_eq!(Balances::total_balance(&account_id(7)), 0); + assert_eq!(System::providers(&account_id(7)), 0); + assert_eq!(System::consumers(&account_id(7)), 0); + }); +} + +#[test] +#[docify::export] +fn force_adjust_total_issuance_example() { + ExtBuilder::default().build_and_execute_with(|| { + // First we set the TotalIssuance to 64 by giving Alice a balance of 64. + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), alice(), 64)); + let old_ti = pallet_balances::TotalIssuance::::get(); + assert_eq!(old_ti, 64, "TI should be 64"); + + // Now test the increase: + assert_ok!(Balances::force_adjust_total_issuance(RawOrigin::Root.into(), Inc, 32)); + let new_ti = pallet_balances::TotalIssuance::::get(); + assert_eq!(old_ti + 32, new_ti, "Should increase by 32"); + + // If Alice tries to call it, it errors: + assert_noop!( + Balances::force_adjust_total_issuance(RawOrigin::Signed(alice()).into(), Inc, 69), + BadOrigin, + ); + }); +} + +#[test] +fn force_adjust_total_issuance_works() { + ExtBuilder::default().build_and_execute_with(|| { + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), account_id(137), 64)); + let ti = pallet_balances::TotalIssuance::::get(); + + // Increase works: + assert_ok!(Balances::force_adjust_total_issuance(RawOrigin::Root.into(), Inc, 32)); + assert_eq!(pallet_balances::TotalIssuance::::get(), ti + 32); + System::assert_last_event(RuntimeEvent::Balances(Event::TotalIssuanceForced { + old: 64, + new: 96, + })); + + // Decrease works: + assert_ok!(Balances::force_adjust_total_issuance(RawOrigin::Root.into(), Dec, 64)); + assert_eq!(pallet_balances::TotalIssuance::::get(), ti - 32); + System::assert_last_event(RuntimeEvent::Balances(Event::TotalIssuanceForced { + old: 96, + new: 32, + })); + }); +} + +#[test] +fn force_adjust_total_issuance_saturates() { + ExtBuilder::default().build_and_execute_with(|| { + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), account_id(137), 64)); + let ti = pallet_balances::TotalIssuance::::get(); + let max = ::Balance::max_value(); + assert_eq!(ti, 64); + + // Increment saturates: + assert_ok!(Balances::force_adjust_total_issuance(RawOrigin::Root.into(), Inc, max)); + assert_ok!(Balances::force_adjust_total_issuance(RawOrigin::Root.into(), Inc, 123)); + assert_eq!(pallet_balances::TotalIssuance::::get(), max); + + // Decrement saturates: + assert_ok!(Balances::force_adjust_total_issuance(RawOrigin::Root.into(), Dec, max)); + assert_ok!(Balances::force_adjust_total_issuance(RawOrigin::Root.into(), Dec, 123)); + assert_eq!(pallet_balances::TotalIssuance::::get(), 0); + }); +} + +#[test] +fn force_adjust_total_issuance_rejects_zero_delta() { + ExtBuilder::default().build_and_execute_with(|| { + assert_noop!( + Balances::force_adjust_total_issuance(RawOrigin::Root.into(), Inc, 0), + Error::::DeltaZero, + ); + assert_noop!( + Balances::force_adjust_total_issuance(RawOrigin::Root.into(), Dec, 0), + Error::::DeltaZero, + ); + }); +} + +#[test] +fn force_adjust_total_issuance_rejects_more_than_inactive() { + ExtBuilder::default().build_and_execute_with(|| { + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), account_id(137), 64)); + Balances::deactivate(16u32.into()); + + assert_eq!(pallet_balances::TotalIssuance::::get(), 64); + assert_eq!(Balances::active_issuance(), 48); + + // Works with up to 48: + assert_ok!(Balances::force_adjust_total_issuance(RawOrigin::Root.into(), Dec, 40),); + assert_ok!(Balances::force_adjust_total_issuance(RawOrigin::Root.into(), Dec, 8),); + assert_eq!(pallet_balances::TotalIssuance::::get(), 16); + assert_eq!(Balances::active_issuance(), 0); + // Errors with more than 48: + assert_noop!( + Balances::force_adjust_total_issuance(RawOrigin::Root.into(), Dec, 1), + Error::::IssuanceDeactivated, + ); + // Increasing again increases the inactive issuance: + assert_ok!(Balances::force_adjust_total_issuance(RawOrigin::Root.into(), Inc, 10),); + assert_eq!(pallet_balances::TotalIssuance::::get(), 26); + assert_eq!(Balances::active_issuance(), 10); + }); +} + +#[test] +fn burn_works() { + ExtBuilder::default().build().execute_with(|| { + // Prepare account with initial balance + let (account, init_balance) = (account_id(1), 37); + assert_ok!(Balances::force_set_balance( + RuntimeOrigin::root(), + account.clone(), + init_balance + )); + let init_issuance = pallet_balances::TotalIssuance::::get(); + let (keep_alive, allow_death) = (true, false); + + // 1. Cannot burn more than what's available + assert_noop!( + Balances::burn(Some(account.clone()).into(), init_balance + 1, allow_death), + TokenError::FundsUnavailable, + ); + + // 2. Burn some funds, without reaping the account + let burn_amount_1 = 1; + assert_ok!(Balances::burn(Some(account.clone()).into(), burn_amount_1, allow_death)); + System::assert_last_event(RuntimeEvent::Balances(Event::Burned { + who: account.clone(), + amount: burn_amount_1, + })); + assert_eq!(pallet_balances::TotalIssuance::::get(), init_issuance - burn_amount_1); + assert_eq!(Balances::total_balance(&account), init_balance - burn_amount_1); + + // 3. Cannot burn funds below existential deposit if `keep_alive` is `true` + let burn_amount_2 = + init_balance - burn_amount_1 - ::ExistentialDeposit::get() + 1; + assert_noop!( + Balances::burn(Some(account.clone()).into(), init_balance + 1, keep_alive), + TokenError::FundsUnavailable, + ); + + // 4. Burn some more funds, this time reaping the account + assert_ok!(Balances::burn(Some(account.clone()).into(), burn_amount_2, allow_death)); + System::assert_last_event(RuntimeEvent::Balances(Event::Burned { + who: account.clone(), + amount: burn_amount_2, + })); + assert_eq!( + pallet_balances::TotalIssuance::::get(), + init_issuance - burn_amount_1 - burn_amount_2 + ); + assert!(Balances::total_balance(&account).is_zero()); + }); +} diff --git a/pallets/balances/src/tests/fungible_conformance_tests.rs b/pallets/balances/src/tests/fungible_conformance_tests.rs new file mode 100644 index 00000000..5c0c19a5 --- /dev/null +++ b/pallets/balances/src/tests/fungible_conformance_tests.rs @@ -0,0 +1,141 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::*; +use frame_support::traits::fungible::{conformance_tests, Inspect, Mutate}; +use paste::paste; + +macro_rules! generate_tests { + // Handle a conformance test that requires special testing with and without a dust trap. + (dust_trap_variation, $base_path:path, $scope:expr, $trait:ident, $ext_deposit:expr, $($test_name:ident),*) => { + $( + paste! { + #[test] + fn [<$trait _ $scope _ $test_name _existential_deposit_ $ext_deposit _dust_trap_on >]() { + // Some random trap account. + let trap_account = ::AccountId::from(65174286u64); + let builder = ExtBuilder::default().existential_deposit($ext_deposit).dust_trap(trap_account); + builder.build_and_execute_with(|| { + Balances::set_balance(&trap_account, Balances::minimum_balance()); + $base_path::$scope::$trait::$test_name::< + Balances, + ::AccountId, + >(Some(trap_account)); + }); + } + + #[test] + fn [< $trait _ $scope _ $test_name _existential_deposit_ $ext_deposit _dust_trap_off >]() { + let builder = ExtBuilder::default().existential_deposit($ext_deposit); + builder.build_and_execute_with(|| { + $base_path::$scope::$trait::$test_name::< + Balances, + ::AccountId, + >(None); + }); + } + } + )* + }; + // Regular conformance test + ($base_path:path, $scope:expr, $trait:ident, $ext_deposit:expr, $($test_name:ident),*) => { + $( + paste! { + #[test] + fn [< $trait _ $scope _ $test_name _existential_deposit_ $ext_deposit>]() { + let builder = ExtBuilder::default().existential_deposit($ext_deposit); + builder.build_and_execute_with(|| { + $base_path::$scope::$trait::$test_name::< + Balances, + ::AccountId, + >(); + }); + } + } + )* + }; + ($base_path:path, $ext_deposit:expr) => { + // regular::mutate + generate_tests!( + dust_trap_variation, + $base_path, + regular, + mutate, + $ext_deposit, + transfer_expendable_dust + ); + generate_tests!( + $base_path, + regular, + mutate, + $ext_deposit, + mint_into_success, + mint_into_overflow, + mint_into_below_minimum, + burn_from_exact_success, + burn_from_best_effort_success, + burn_from_exact_insufficient_funds, + restore_success, + restore_overflow, + restore_below_minimum, + shelve_success, + shelve_insufficient_funds, + transfer_success, + transfer_expendable_all, + transfer_protect_preserve, + set_balance_mint_success, + set_balance_burn_success, + can_deposit_success, + can_deposit_below_minimum, + can_deposit_overflow, + can_withdraw_success, + can_withdraw_reduced_to_zero, + can_withdraw_balance_low, + reducible_balance_expendable, + reducible_balance_protect_preserve + ); + // regular::unbalanced + generate_tests!( + $base_path, + regular, + unbalanced, + $ext_deposit, + write_balance, + decrease_balance_expendable, + decrease_balance_preserve, + increase_balance, + set_total_issuance, + deactivate_and_reactivate + ); + // regular::balanced + generate_tests!( + $base_path, + regular, + balanced, + $ext_deposit, + issue_and_resolve_credit, + rescind_and_settle_debt, + deposit, + withdraw, + pair + ); + }; +} + +generate_tests!(conformance_tests, 1); +generate_tests!(conformance_tests, 5); +generate_tests!(conformance_tests, 1000); diff --git a/pallets/balances/src/tests/fungible_tests.rs b/pallets/balances/src/tests/fungible_tests.rs new file mode 100644 index 00000000..4840258e --- /dev/null +++ b/pallets/balances/src/tests/fungible_tests.rs @@ -0,0 +1,883 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Tests regarding the functionality of the `fungible` trait set implementations. + +use super::*; +use frame_support::traits::{ + tokens::{ + Fortitude::{Force, Polite}, + Precision::{BestEffort, Exact}, + Preservation::{Expendable, Preserve, Protect}, + Restriction::Free, + }, + Consideration, Footprint, LinearStoragePrice, MaybeConsideration, +}; +use fungible::{ + FreezeConsideration, HoldConsideration, Inspect, InspectFreeze, InspectHold, + LoneFreezeConsideration, LoneHoldConsideration, Mutate, MutateFreeze, MutateHold, Unbalanced, +}; +use sp_core::ConstU128; + +#[test] +fn inspect_trait_reducible_balance_basic_works() { + ExtBuilder::default().existential_deposit(10).build_and_execute_with(|| { + Balances::set_balance(&account_id(1), 100); + assert_eq!(Balances::reducible_balance(&account_id(1), Expendable, Polite), 100); + assert_eq!(Balances::reducible_balance(&account_id(1), Protect, Polite), 90); + assert_eq!(Balances::reducible_balance(&account_id(1), Preserve, Polite), 90); + assert_eq!(Balances::reducible_balance(&account_id(1), Expendable, Force), 100); + assert_eq!(Balances::reducible_balance(&account_id(1), Protect, Force), 90); + assert_eq!(Balances::reducible_balance(&account_id(1), Preserve, Force), 90); + }); +} + +#[test] +fn inspect_trait_reducible_balance_other_provide_works() { + ExtBuilder::default().existential_deposit(10).build_and_execute_with(|| { + Balances::set_balance(&account_id(1), 100); + System::inc_providers(&account_id(1)); + assert_eq!(Balances::reducible_balance(&account_id(1), Expendable, Polite), 100); + assert_eq!(Balances::reducible_balance(&account_id(1), Protect, Polite), 100); + assert_eq!(Balances::reducible_balance(&account_id(1), Preserve, Polite), 90); + assert_eq!(Balances::reducible_balance(&account_id(1), Expendable, Force), 100); + assert_eq!(Balances::reducible_balance(&account_id(1), Protect, Force), 100); + assert_eq!(Balances::reducible_balance(&account_id(1), Preserve, Force), 90); + }); +} + +#[test] +fn inspect_trait_reducible_balance_frozen_works() { + ExtBuilder::default().existential_deposit(10).build_and_execute_with(|| { + Balances::set_balance(&account_id(1), 100); + assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), 50)); + assert_eq!(Balances::reducible_balance(&account_id(1), Expendable, Polite), 50); + assert_eq!(Balances::reducible_balance(&account_id(1), Protect, Polite), 50); + assert_eq!(Balances::reducible_balance(&account_id(1), Preserve, Polite), 50); + assert_eq!(Balances::reducible_balance(&account_id(1), Expendable, Force), 90); + assert_eq!(Balances::reducible_balance(&account_id(1), Protect, Force), 90); + assert_eq!(Balances::reducible_balance(&account_id(1), Preserve, Force), 90); + }); +} + +#[test] +fn unbalanced_trait_set_balance_works() { + ExtBuilder::default().build_and_execute_with(|| { + assert_eq!(>::balance(&account_id(137)), 0); + assert_ok!(Balances::write_balance(&account_id(137), 100)); + assert_eq!(>::balance(&account_id(137)), 100); + + assert_ok!(>::hold(&TestId::Foo, &account_id(137), 60)); + assert_eq!(>::balance(&account_id(137)), 40); + assert_eq!( + >::total_balance_on_hold(&account_id(137)), + 60 + ); + assert_eq!( + >::balance_on_hold(&TestId::Foo, &account_id(137)), + 60 + ); + + assert_noop!( + Balances::write_balance(&account_id(137), 0), + Error::::InsufficientBalance + ); + + assert_ok!(Balances::write_balance(&account_id(137), 1)); + assert_eq!(>::balance(&account_id(137)), 1); + assert_eq!( + >::balance_on_hold(&TestId::Foo, &account_id(137)), + 60 + ); + + assert_ok!(>::release( + &TestId::Foo, + &account_id(137), + 60, + Exact + )); + assert_eq!( + >::balance_on_hold(&TestId::Foo, &account_id(137)), + 0 + ); + assert_eq!( + >::total_balance_on_hold(&account_id(137)), + 0 + ); + }); +} + +#[test] +fn unbalanced_trait_set_total_issuance_works() { + ExtBuilder::default().build_and_execute_with(|| { + assert_eq!(>::total_issuance(), 0); + Balances::set_total_issuance(100); + assert_eq!(>::total_issuance(), 100); + }); +} + +#[test] +fn unbalanced_trait_decrease_balance_simple_works() { + ExtBuilder::default().build_and_execute_with(|| { + // An Account that starts at 100 + assert_ok!(Balances::write_balance(&account_id(137), 100)); + assert_eq!(>::balance(&account_id(137)), 100); + // and reserves 50 + assert_ok!(>::hold(&TestId::Foo, &account_id(137), 50)); + assert_eq!(>::balance(&account_id(137)), 50); + // and is decreased by 20 + assert_ok!(Balances::decrease_balance(&account_id(137), 20, Exact, Expendable, Polite)); + assert_eq!(>::balance(&account_id(137)), 30); + }); +} + +#[test] +fn unbalanced_trait_decrease_balance_works_1() { + ExtBuilder::default().build_and_execute_with(|| { + assert_ok!(Balances::write_balance(&account_id(137), 100)); + assert_eq!(>::balance(&account_id(137)), 100); + + assert_noop!( + Balances::decrease_balance(&account_id(137), 101, Exact, Expendable, Polite), + TokenError::FundsUnavailable + ); + assert_eq!( + Balances::decrease_balance(&account_id(137), 100, Exact, Expendable, Polite), + Ok(100) + ); + assert_eq!(>::balance(&account_id(137)), 0); + }); +} + +#[test] +fn unbalanced_trait_decrease_balance_works_2() { + ExtBuilder::default().build_and_execute_with(|| { + // free: 40, reserved: 60 + assert_ok!(Balances::write_balance(&account_id(137), 100)); + assert_ok!(Balances::hold(&TestId::Foo, &account_id(137), 60)); + assert_eq!(>::balance(&account_id(137)), 40); + assert_eq!(Balances::total_balance_on_hold(&account_id(137)), 60); + assert_noop!( + Balances::decrease_balance(&account_id(137), 40, Exact, Expendable, Polite), + TokenError::FundsUnavailable + ); + assert_eq!( + Balances::decrease_balance(&account_id(137), 39, Exact, Expendable, Polite), + Ok(39) + ); + assert_eq!(>::balance(&account_id(137)), 1); + assert_eq!(Balances::total_balance_on_hold(&account_id(137)), 60); + }); +} + +#[test] +fn unbalanced_trait_decrease_balance_at_most_works_1() { + ExtBuilder::default().build_and_execute_with(|| { + assert_ok!(Balances::write_balance(&account_id(137), 100)); + assert_eq!(>::balance(&account_id(137)), 100); + + assert_eq!( + Balances::decrease_balance(&account_id(137), 101, BestEffort, Expendable, Polite), + Ok(100) + ); + assert_eq!(>::balance(&account_id(137)), 0); + }); +} + +#[test] +fn unbalanced_trait_decrease_balance_at_most_works_2() { + ExtBuilder::default().build_and_execute_with(|| { + assert_ok!(Balances::write_balance(&account_id(137), 99)); + assert_eq!( + Balances::decrease_balance(&account_id(137), 99, BestEffort, Expendable, Polite), + Ok(99) + ); + assert_eq!(>::balance(&account_id(137)), 0); + }); +} + +#[test] +fn unbalanced_trait_decrease_balance_at_most_works_3() { + ExtBuilder::default().build_and_execute_with(|| { + // free: 40, reserved: 60 + assert_ok!(Balances::write_balance(&account_id(137), 100)); + assert_ok!(Balances::hold(&TestId::Foo, &account_id(137), 60)); + assert_eq!(Balances::free_balance(account_id(137)), 40); + assert_eq!(Balances::total_balance_on_hold(&account_id(137)), 60); + assert_eq!( + Balances::decrease_balance(&account_id(137), 0, BestEffort, Expendable, Polite), + Ok(0) + ); + assert_eq!(Balances::free_balance(account_id(137)), 40); + assert_eq!(Balances::total_balance_on_hold(&account_id(137)), 60); + assert_eq!( + Balances::decrease_balance(&account_id(137), 10, BestEffort, Expendable, Polite), + Ok(10) + ); + assert_eq!(Balances::free_balance(account_id(137)), 30); + assert_eq!( + Balances::decrease_balance(&account_id(137), 200, BestEffort, Expendable, Polite), + Ok(29) + ); + assert_eq!(>::balance(&account_id(137)), 1); + assert_eq!(Balances::free_balance(account_id(137)), 1); + assert_eq!(Balances::total_balance_on_hold(&account_id(137)), 60); + }); +} + +#[test] +fn unbalanced_trait_increase_balance_works() { + ExtBuilder::default().build_and_execute_with(|| { + assert_noop!( + Balances::increase_balance(&account_id(137), 0, Exact), + TokenError::BelowMinimum + ); + assert_eq!(Balances::increase_balance(&account_id(137), 1, Exact), Ok(1)); + assert_noop!( + Balances::increase_balance(&account_id(137), Balance::MAX, Exact), + ArithmeticError::Overflow + ); + }); +} + +#[test] +fn unbalanced_trait_increase_balance_at_most_works() { + ExtBuilder::default().build_and_execute_with(|| { + assert_eq!(Balances::increase_balance(&account_id(137), 0, BestEffort), Ok(0)); + assert_eq!(Balances::increase_balance(&account_id(137), 1, BestEffort), Ok(1)); + assert_eq!( + Balances::increase_balance(&account_id(137), Balance::MAX, BestEffort), + Ok(Balance::MAX - 1) + ); + }); +} + +#[test] +fn freezing_and_holds_should_overlap() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), 10)); + assert_ok!(Balances::hold(&TestId::Foo, &account_id(1), 9)); + assert_eq!(Balances::account(&account_id(1)).free, 1); + assert_eq!(System::consumers(&account_id(1)), 1); + assert_eq!(Balances::account(&account_id(1)).free, 1); + assert_eq!(Balances::account(&account_id(1)).frozen, 10); + assert_eq!(Balances::account(&account_id(1)).reserved, 9); + assert_eq!(Balances::total_balance_on_hold(&account_id(1)), 9); + }); +} + +#[test] +fn frozen_hold_balance_cannot_be_moved_without_force() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), 10)); + assert_ok!(Balances::hold(&TestId::Foo, &account_id(1), 9)); + assert_eq!(Balances::reducible_total_balance_on_hold(&account_id(1), Force), 9); + assert_eq!(Balances::reducible_total_balance_on_hold(&account_id(1), Polite), 0); + let e = TokenError::Frozen; + assert_noop!( + Balances::transfer_on_hold( + &TestId::Foo, + &account_id(1), + &account_id(2), + 1, + Exact, + Free, + Polite + ), + e + ); + assert_ok!(Balances::transfer_on_hold( + &TestId::Foo, + &account_id(1), + &account_id(2), + 1, + Exact, + Free, + Force + )); + }); +} + +#[test] +fn frozen_hold_balance_best_effort_transfer_works() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), 5)); + assert_ok!(Balances::hold(&TestId::Foo, &account_id(1), 9)); + assert_eq!(Balances::reducible_total_balance_on_hold(&account_id(1), Force), 9); + assert_eq!(Balances::reducible_total_balance_on_hold(&account_id(1), Polite), 5); + assert_ok!(Balances::transfer_on_hold( + &TestId::Foo, + &account_id(1), + &account_id(2), + 10, + BestEffort, + Free, + Polite + )); + assert_eq!(Balances::total_balance(&account_id(1)), 5); + assert_eq!(Balances::total_balance(&account_id(2)), 25); + }); +} + +#[test] +fn partial_freezing_should_work() { + ExtBuilder::default() + .existential_deposit(10) + .monied(true) + .build_and_execute_with(|| { + assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), 5)); + assert_eq!(System::consumers(&account_id(1)), 1); + assert_ok!(>::transfer( + &account_id(1), + &account_id(2), + 5, + Expendable + )); + // After transferring 5, balance is 95. With 10 frozen, can transfer up to 85 more + // (95-10=85) + assert_ok!(>::transfer( + &account_id(1), + &account_id(2), + 85, + Expendable + )); + // Now balance is 10. Transferring 1 more would leave 9, which is < 10 frozen, so should + // fail + assert_noop!( + >::transfer( + &account_id(1), + &account_id(2), + 1, + Expendable + ), + TokenError::Frozen + ); + }); +} + +#[test] +fn thaw_should_work() { + ExtBuilder::default() + .existential_deposit(10) + .monied(true) + .build_and_execute_with(|| { + assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), Balance::MAX)); + assert_ok!(Balances::thaw(&TestId::Foo, &account_id(1))); + assert_eq!(System::consumers(&account_id(1)), 0); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &account_id(1)), 0); + assert_eq!(Balances::account(&account_id(1)).frozen, 0); + assert_ok!(>::transfer( + &account_id(1), + &account_id(2), + 10, + Expendable + )); + }); +} + +#[test] +fn set_freeze_zero_should_work() { + ExtBuilder::default() + .existential_deposit(10) + .monied(true) + .build_and_execute_with(|| { + assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), Balance::MAX)); + assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), 0)); + assert_eq!(System::consumers(&account_id(1)), 0); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &account_id(1)), 0); + assert_eq!(Balances::account(&account_id(1)).frozen, 0); + assert_ok!(>::transfer( + &account_id(1), + &account_id(2), + 10, + Expendable + )); + }); +} + +#[test] +fn set_freeze_should_work() { + ExtBuilder::default() + .existential_deposit(10) + .monied(true) + .build_and_execute_with(|| { + assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), Balance::MAX)); + assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), 5)); + assert_ok!(>::transfer( + &account_id(1), + &account_id(2), + 5, + Expendable + )); + // After transferring 5, balance is 95. With 10 frozen, can transfer up to 85 more + assert_ok!(>::transfer( + &account_id(1), + &account_id(2), + 85, + Expendable + )); + // Now balance is 10. Transferring 1 more would leave 9, which is < 10 frozen, so should + // fail + assert_noop!( + >::transfer( + &account_id(1), + &account_id(2), + 1, + Expendable + ), + TokenError::Frozen + ); + }); +} + +#[test] +fn extend_freeze_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), 5)); + assert_ok!(Balances::extend_freeze(&TestId::Foo, &account_id(1), 10)); + assert_eq!(Balances::account(&account_id(1)).frozen, 10); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &account_id(1)), 10); + assert_noop!( + >::transfer( + &account_id(1), + &account_id(2), + 1, + Expendable + ), + TokenError::Frozen + ); + }); +} + +#[test] +fn debug_freeze_behavior() { + ExtBuilder::default() + .existential_deposit(10) + .monied(true) + .build_and_execute_with(|| { + println!("=== Debug freeze behavior ==="); + println!("Initial balance: {}", Balances::free_balance(account_id(1))); + + // Set a freeze + assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), 10)); + println!("After freeze(10):"); + println!(" Free balance: {}", Balances::free_balance(account_id(1))); + println!(" Frozen amount: {}", Balances::balance_frozen(&TestId::Foo, &account_id(1))); + println!(" Account frozen: {}", Balances::account(&account_id(1)).frozen); + + // Try transfers of different amounts + for amount in [1, 5, 10, 89, 90, 91] { + let balance_before = Balances::free_balance(account_id(1)); + let result = >::transfer( + &account_id(1), + &account_id(2), + amount, + Expendable, + ); + let balance_after = Balances::free_balance(account_id(1)); + + println!( + "Transfer {} units: {:?} (balance: {} -> {})", + amount, result, balance_before, balance_after + ); + + // Restore balance for next test + if result.is_ok() { + let _ = >::transfer( + &account_id(2), + &account_id(1), + amount, + Expendable, + ); + } + } + }); +} + +#[test] +fn double_freezing_should_work() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), 5)); + assert_ok!(Balances::set_freeze(&TestId::Bar, &account_id(1), 5)); + assert_eq!(System::consumers(&account_id(1)), 1); + assert_ok!(>::transfer( + &account_id(1), + &account_id(2), + 5, + Expendable + )); + assert_noop!( + >::transfer( + &account_id(1), + &account_id(2), + 1, + Expendable + ), + TokenError::Frozen + ); + }); +} + +#[test] +fn can_hold_entire_balance_when_second_provider() { + ExtBuilder::default() + .existential_deposit(1) + .monied(false) + .build_and_execute_with(|| { + >::set_balance(&account_id(1), 100); + assert_noop!( + Balances::hold(&TestId::Foo, &account_id(1), 100), + TokenError::FundsUnavailable + ); + System::inc_providers(&account_id(1)); + assert_eq!(System::providers(&account_id(1)), 2); + assert_ok!(Balances::hold(&TestId::Foo, &account_id(1), 100)); + assert_eq!(System::providers(&account_id(1)), 1); + assert_noop!(System::dec_providers(&account_id(1)), DispatchError::ConsumerRemaining); + }); +} + +#[test] +fn unholding_frees_hold_slot() { + ExtBuilder::default() + .existential_deposit(1) + .monied(false) + .build_and_execute_with(|| { + >::set_balance(&account_id(1), 100); + assert_ok!(Balances::hold(&TestId::Foo, &account_id(1), 10)); + assert_ok!(Balances::hold(&TestId::Bar, &account_id(1), 10)); + assert_ok!(Balances::release(&TestId::Foo, &account_id(1), 10, Exact)); + assert_ok!(Balances::hold(&TestId::Baz, &account_id(1), 10)); + }); +} + +#[test] +fn sufficients_work_properly_with_reference_counting() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + // Only run PoC when the system pallet is enabled, since the underlying bug is in the + // system pallet it won't work with BalancesAccountStore + if UseSystem::get() { + // Start with a balance of 100 + >::set_balance(&account_id(1), 100); + // Emulate a sufficient, in reality this could be reached by transferring a + // sufficient asset to the account + System::inc_sufficients(&account_id(1)); + // Spend the same balance multiple times + assert_ok!(>::transfer( + &account_id(1), + &account_id(137), + 100, + Expendable + )); + assert_eq!(Balances::free_balance(account_id(1)), 0); + assert_noop!( + >::transfer( + &account_id(1), + &account_id(137), + 100, + Expendable + ), + TokenError::FundsUnavailable + ); + } + }); +} + +#[test] +fn emit_events_with_changing_freezes() { + ExtBuilder::default().build_and_execute_with(|| { + let _ = Balances::set_balance(&account_id(1), 100); + System::reset_events(); + + // Freeze = [] --> [10] + assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), 10)); + assert_eq!( + events(), + [RuntimeEvent::Balances(crate::Event::Frozen { who: account_id(1), amount: 10 })] + ); + + // Freeze = [10] --> [15] + assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), 15)); + assert_eq!( + events(), + [RuntimeEvent::Balances(crate::Event::Frozen { who: account_id(1), amount: 5 })] + ); + + // Freeze = [15] --> [15, 20] + assert_ok!(Balances::set_freeze(&TestId::Bar, &account_id(1), 20)); + assert_eq!( + events(), + [RuntimeEvent::Balances(crate::Event::Frozen { who: account_id(1), amount: 5 })] + ); + + // Freeze = [15, 20] --> [17, 20] + assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), 17)); + for event in events() { + match event { + RuntimeEvent::Balances(crate::Event::Frozen { .. }) => { + assert!(false, "unexpected freeze event") + }, + RuntimeEvent::Balances(crate::Event::Thawed { .. }) => { + assert!(false, "unexpected thaw event") + }, + _ => continue, + } + } + + // Freeze = [17, 20] --> [17, 15] + assert_ok!(Balances::set_freeze(&TestId::Bar, &account_id(1), 15)); + assert_eq!( + events(), + [RuntimeEvent::Balances(crate::Event::Thawed { who: account_id(1), amount: 3 })] + ); + + // Freeze = [17, 15] --> [15] + assert_ok!(Balances::thaw(&TestId::Foo, &account_id(1))); + assert_eq!( + events(), + [RuntimeEvent::Balances(crate::Event::Thawed { who: account_id(1), amount: 2 })] + ); + + // Freeze = [15] --> [] + assert_ok!(Balances::thaw(&TestId::Bar, &account_id(1))); + assert_eq!( + events(), + [RuntimeEvent::Balances(crate::Event::Thawed { who: account_id(1), amount: 15 })] + ); + }); +} + +#[test] +fn withdraw_precision_exact_works() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + assert_ok!(Balances::set_freeze(&TestId::Foo, &account_id(1), 10)); + assert_eq!(Balances::account(&account_id(1)).free, 10); + assert_eq!(Balances::account(&account_id(1)).frozen, 10); + + // `BestEffort` will not reduce anything + assert_ok!(>::withdraw( + &account_id(1), + 5, + BestEffort, + Preserve, + Polite + )); + + assert_eq!(Balances::account(&account_id(1)).free, 10); + assert_eq!(Balances::account(&account_id(1)).frozen, 10); + + assert_noop!( + >::withdraw( + &account_id(1), + 5, + Exact, + Preserve, + Polite + ), + TokenError::FundsUnavailable + ); + }); +} + +#[test] +fn freeze_consideration_works() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + type Consideration = FreezeConsideration< + AccountId, + Balances, + FooReason, + LinearStoragePrice, ConstU128<1>, Balance>, + Footprint, + >; + + let who = account_id(4); + // freeze amount taken somewhere outside of our (Consideration) scope. + let extend_freeze = 15; + + let ticket = Consideration::new(&who, Footprint::from_parts(0, 0)).unwrap(); + assert!(ticket.is_none()); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 0); + + let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap(); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 10); + + let ticket = ticket.update(&who, Footprint::from_parts(4, 1)).unwrap(); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 4); + + assert_ok!(Balances::increase_frozen(&TestId::Foo, &who, extend_freeze)); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 4 + extend_freeze); + + let ticket = ticket.update(&who, Footprint::from_parts(8, 1)).unwrap(); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 8 + extend_freeze); + + let ticket = ticket.update(&who, Footprint::from_parts(0, 0)).unwrap(); + assert!(ticket.is_none()); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), extend_freeze); + + let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap(); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 10 + extend_freeze); + + ticket.drop(&who).unwrap(); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), extend_freeze); + }); +} + +#[test] +fn hold_consideration_works() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + type Consideration = HoldConsideration< + AccountId, + Balances, + FooReason, + LinearStoragePrice, ConstU128<1>, Balance>, + Footprint, + >; + + let who = account_id(4); + // hold amount taken somewhere outside of our (Consideration) scope. + let extend_hold = 15; + + let ticket = Consideration::new(&who, Footprint::from_parts(0, 0)).unwrap(); + assert!(ticket.is_none()); + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 0); + + let ticket = ticket.update(&who, Footprint::from_parts(10, 1)).unwrap(); + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 10); + + let ticket = ticket.update(&who, Footprint::from_parts(4, 1)).unwrap(); + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 4); + + assert_ok!(Balances::hold(&TestId::Foo, &who, extend_hold)); + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 4 + extend_hold); + + let ticket = ticket.update(&who, Footprint::from_parts(8, 1)).unwrap(); + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 8 + extend_hold); + + let ticket = ticket.update(&who, Footprint::from_parts(0, 0)).unwrap(); + assert!(ticket.is_none()); + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), extend_hold); + + let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap(); + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 10 + extend_hold); + + ticket.drop(&who).unwrap(); + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), extend_hold); + }); +} + +#[test] +fn lone_freeze_consideration_works() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + type Consideration = LoneFreezeConsideration< + AccountId, + Balances, + FooReason, + LinearStoragePrice, ConstU128<1>, Balance>, + Footprint, + >; + + let who = account_id(4); + let zero_ticket = Consideration::new(&who, Footprint::from_parts(0, 0)).unwrap(); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 0); + + let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap(); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 10); + + assert_ok!(Balances::increase_frozen(&TestId::Foo, &who, 5)); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 15); + + let ticket = ticket.update(&who, Footprint::from_parts(4, 1)).unwrap(); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 4); + + assert_eq!(ticket.update(&who, Footprint::from_parts(0, 0)).unwrap(), zero_ticket); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 0); + + let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap(); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 10); + + ticket.drop(&who).unwrap(); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 0); + }); +} + +#[test] +fn lone_hold_consideration_works() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + type Consideration = LoneHoldConsideration< + AccountId, + Balances, + FooReason, + LinearStoragePrice, ConstU128<1>, Balance>, + Footprint, + >; + + let who = account_id(4); + let zero_ticket = Consideration::new(&who, Footprint::from_parts(0, 0)).unwrap(); + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 0); + + let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap(); + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 10); + + assert_ok!(Balances::hold(&TestId::Foo, &who, 5)); + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 15); + + let ticket = ticket.update(&who, Footprint::from_parts(4, 1)).unwrap(); + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 4); + + assert_eq!(ticket.update(&who, Footprint::from_parts(0, 0)).unwrap(), zero_ticket); + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 0); + + let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap(); + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 10); + + ticket.drop(&who).unwrap(); + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 0); + }); +} diff --git a/pallets/balances/src/tests/general_tests.rs b/pallets/balances/src/tests/general_tests.rs new file mode 100644 index 00000000..a56f9f98 --- /dev/null +++ b/pallets/balances/src/tests/general_tests.rs @@ -0,0 +1,143 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg(test)] + +use crate::{ + system::AccountInfo, + tests::{account_id, ensure_ti_valid, Balances, ExtBuilder, System, Test, TestId, UseSystem}, + AccountData, ExtraFlags, TotalIssuance, +}; +use frame_support::{ + assert_noop, assert_ok, hypothetically, + traits::{ + fungible::{Mutate, MutateHold}, + tokens::Precision, + }, +}; +use sp_runtime::DispatchError; + +/// There are some accounts that have one consumer ref too few. These accounts are at risk of losing +/// their held (reserved) balance. They do not just lose it - it is also not accounted for in the +/// Total Issuance. Here we test the case that the account does not reap in such a case, but gets +/// one consumer ref for its reserved balance. +#[test] +fn regression_historic_acc_does_not_evaporate_reserve() { + ExtBuilder::default().build_and_execute_with(|| { + UseSystem::set(true); + let (alice, bob) = (account_id(0), account_id(1)); + // Alice is in a bad state with consumer == 0 && reserved > 0: + Balances::set_balance(&alice, 100); + TotalIssuance::::put(100); + ensure_ti_valid(); + + assert_ok!(Balances::hold(&TestId::Foo, &alice, 10)); + // This is the issue of the account: + System::dec_consumers(&alice); + + assert_eq!( + System::account(&alice), + AccountInfo { + data: AccountData { + free: 90, + reserved: 10, + frozen: 0, + flags: ExtraFlags(1u128 << 127), + }, + nonce: 0, + consumers: 0, // should be 1 on a good acc + providers: 1, + sufficients: 0, + } + ); + + ensure_ti_valid(); + + // Reaping the account is prevented by the new logic: + assert_noop!( + Balances::transfer_allow_death(Some(alice.clone()).into(), bob.clone(), 90), + DispatchError::ConsumerRemaining + ); + assert_noop!( + Balances::transfer_all(Some(alice.clone()).into(), bob.clone(), false), + DispatchError::ConsumerRemaining + ); + + // normal transfers still work: + hypothetically!({ + assert_ok!(Balances::transfer_keep_alive(Some(alice.clone()).into(), bob.clone(), 40)); + // Alice got back her consumer ref: + assert_eq!(System::consumers(&alice), 1); + ensure_ti_valid(); + }); + hypothetically!({ + assert_ok!(Balances::transfer_all(Some(alice.clone()).into(), bob.clone(), true)); + // Alice got back her consumer ref: + assert_eq!(System::consumers(&alice), 1); + ensure_ti_valid(); + }); + + // un-reserving all does not add a consumer ref: + hypothetically!({ + assert_ok!(Balances::release(&TestId::Foo, &alice, 10, Precision::Exact)); + assert_eq!(System::consumers(&alice), 0); + assert_ok!(Balances::transfer_keep_alive(Some(alice.clone()).into(), bob.clone(), 40)); + assert_eq!(System::consumers(&alice), 0); + ensure_ti_valid(); + }); + // un-reserving some does add a consumer ref: + hypothetically!({ + assert_ok!(Balances::release(&TestId::Foo, &alice, 5, Precision::Exact)); + assert_eq!(System::consumers(&alice), 1); + assert_ok!(Balances::transfer_keep_alive(Some(alice.clone()).into(), bob.clone(), 40)); + assert_eq!(System::consumers(&alice), 1); + ensure_ti_valid(); + }); + }); +} + +#[cfg(feature = "try-runtime")] +#[test] +fn try_state_works() { + use crate::{Config, Freezes, Holds}; + use frame_support::{ + storage, + traits::{Get, Hooks, VariantCount}, + }; + + ExtBuilder::default().build_and_execute_with(|| { + storage::unhashed::put( + &Holds::::hashed_key_for(account_id(1)), + &vec![0u8; ::RuntimeHoldReason::VARIANT_COUNT as usize + 1], + ); + + assert!(format!("{:?}", Balances::try_state(0).unwrap_err()) + .contains("Found `Hold` with too many elements")); + }); + + ExtBuilder::default().build_and_execute_with(|| { + let max_freezes: u32 = ::MaxFreezes::get(); + + storage::unhashed::put( + &Freezes::::hashed_key_for(account_id(1)), + &vec![0u8; max_freezes as usize + 1], + ); + + assert!(format!("{:?}", Balances::try_state(0).unwrap_err()) + .contains("Found `Freeze` with too many elements")); + }); +} diff --git a/pallets/balances/src/tests/mod.rs b/pallets/balances/src/tests/mod.rs new file mode 100644 index 00000000..bb33433d --- /dev/null +++ b/pallets/balances/src/tests/mod.rs @@ -0,0 +1,332 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Tests. + +#![cfg(test)] + +use crate::{self as pallet_balances, AccountData, Config, CreditOf, Error, Pallet, TotalIssuance}; +use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; +use frame_support::{ + assert_err, assert_noop, assert_ok, assert_storage_noop, derive_impl, + dispatch::{DispatchInfo, GetDispatchInfo}, + parameter_types, + traits::{ + fungible, ConstU32, ConstU8, Imbalance as ImbalanceT, OnUnbalanced, StorageMapShim, + StoredMap, VariantCount, VariantCountOf, WhitelistedStorageKeys, + }, + weights::{IdentityFee, Weight}, +}; +use frame_system::{self as system, RawOrigin}; +use pallet_transaction_payment::{ChargeTransactionPayment, FungibleAdapter, Multiplier}; +use scale_info::TypeInfo; +use sp_core::hexdisplay::HexDisplay; +use sp_runtime::{ + traits::{BadOrigin, Zero}, + ArithmeticError, BuildStorage, DispatchError, DispatchResult, FixedPointNumber, RuntimeDebug, + TokenError, +}; +use std::collections::BTreeSet; + +mod currency_tests; +mod dispatchable_tests; +// mod fungible_conformance_tests; // Commented out due to AccountId32 incompatibility +mod fungible_tests; +mod general_tests; +mod reentrancy_tests; +mod transfer_counter_tests; +type Block = frame_system::mocking::MockBlock; +type AccountId = sp_core::crypto::AccountId32; + +#[derive( + Encode, + Decode, + Copy, + Clone, + Eq, + PartialEq, + Ord, + PartialOrd, + MaxEncodedLen, + TypeInfo, + DecodeWithMemTracking, + RuntimeDebug, +)] +pub enum TestId { + Foo, + Bar, + Baz, +} + +impl VariantCount for TestId { + const VARIANT_COUNT: u32 = 3; +} + +frame_support::construct_runtime!( + pub enum Test { + System: frame_system, + Balances: pallet_balances, + TransactionPayment: pallet_transaction_payment, + } +); + +type Balance = u128; + +parameter_types! { + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max( + frame_support::weights::Weight::from_parts(1024, u64::MAX), + ); + pub static ExistentialDeposit: Balance = 1; +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type Block = Block; + type AccountId = AccountId; + type Lookup = sp_runtime::traits::IdentityLookup; + type AccountData = super::AccountData; +} + +#[derive_impl(pallet_transaction_payment::config_preludes::TestDefaultConfig)] +impl pallet_transaction_payment::Config for Test { + type OnChargeTransaction = FungibleAdapter, ()>; + type OperationalFeeMultiplier = ConstU8<5>; + type WeightToFee = IdentityFee; + type LengthToFee = IdentityFee; +} + +parameter_types! { + pub FooReason: TestId = TestId::Foo; + pub MintingAccount: AccountId = AccountId::new([1u8; 32]); +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] +impl Config for Test { + type Balance = Balance; + type DustRemoval = DustTrap; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = TestAccountStore; + type MaxReserves = ConstU32<2>; + type ReserveIdentifier = TestId; + type RuntimeHoldReason = TestId; + type RuntimeFreezeReason = TestId; + type FreezeIdentifier = TestId; + type MaxFreezes = VariantCountOf; + type MintingAccount = MintingAccount; +} + +#[derive(Clone)] +pub struct ExtBuilder { + existential_deposit: Balance, + monied: bool, + dust_trap: Option, +} +impl Default for ExtBuilder { + fn default() -> Self { + Self { existential_deposit: 1, monied: false, dust_trap: None } + } +} +impl ExtBuilder { + pub fn existential_deposit(mut self, existential_deposit: Balance) -> Self { + self.existential_deposit = existential_deposit; + self + } + pub fn monied(mut self, monied: bool) -> Self { + self.monied = monied; + if self.existential_deposit == 0 { + self.existential_deposit = 1; + } + self + } + pub fn dust_trap(mut self, account: u64) -> Self { + self.dust_trap = Some(account); + self + } + pub fn set_associated_consts(&self) { + DUST_TRAP_TARGET.with(|v| v.replace(self.dust_trap)); + EXISTENTIAL_DEPOSIT.with(|v| v.replace(self.existential_deposit)); + } + pub fn build(self) -> sp_io::TestExternalities { + self.set_associated_consts(); + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + pallet_balances::GenesisConfig:: { + balances: if self.monied { + vec![ + (account_id(1), 10 * self.existential_deposit), + (account_id(2), 20 * self.existential_deposit), + (account_id(3), 30 * self.existential_deposit), + (account_id(4), 40 * self.existential_deposit), + (account_id(12), 10 * self.existential_deposit), + ] + } else { + vec![] + }, + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext + } + pub fn build_and_execute_with(self, f: impl Fn()) { + let other = self.clone(); + UseSystem::set(false); + other.build().execute_with(&f); + UseSystem::set(true); + self.build().execute_with(f); + } +} + +parameter_types! { + static DustTrapTarget: Option = None; +} + +pub struct DustTrap; + +impl OnUnbalanced> for DustTrap { + fn on_nonzero_unbalanced(amount: CreditOf) { + match DustTrapTarget::get() { + None => drop(amount), + Some(a) => { + let account = account_id(a as u8); + let result = >::resolve(&account, amount); + debug_assert!(result.is_ok()); + }, + } + } +} + +parameter_types! { + pub static UseSystem: bool = false; +} + +type BalancesAccountStore = + StorageMapShim, AccountId, super::AccountData>; +type SystemAccountStore = frame_system::Pallet; + +pub struct TestAccountStore; +impl StoredMap> for TestAccountStore { + fn get(k: &AccountId) -> super::AccountData { + if UseSystem::get() { + >::get(k) + } else { + >::get(k) + } + } + fn try_mutate_exists>( + k: &AccountId, + f: impl FnOnce(&mut Option>) -> Result, + ) -> Result { + if UseSystem::get() { + >::try_mutate_exists(k, f) + } else { + >::try_mutate_exists(k, f) + } + } + fn mutate( + k: &AccountId, + f: impl FnOnce(&mut super::AccountData) -> R, + ) -> Result { + if UseSystem::get() { + >::mutate(k, f) + } else { + >::mutate(k, f) + } + } + fn mutate_exists( + k: &AccountId, + f: impl FnOnce(&mut Option>) -> R, + ) -> Result { + if UseSystem::get() { + >::mutate_exists(k, f) + } else { + >::mutate_exists(k, f) + } + } + fn insert(k: &AccountId, t: super::AccountData) -> Result<(), DispatchError> { + if UseSystem::get() { + >::insert(k, t) + } else { + >::insert(k, t) + } + } + fn remove(k: &AccountId) -> Result<(), DispatchError> { + if UseSystem::get() { + >::remove(k) + } else { + >::remove(k) + } + } +} + +pub fn events() -> Vec { + let evt = System::events().into_iter().map(|evt| evt.event).collect::>(); + System::reset_events(); + evt +} + +/// create a transaction info struct from weight. Handy to avoid building the whole struct. +pub fn info_from_weight(w: Weight) -> DispatchInfo { + DispatchInfo { call_weight: w, ..Default::default() } +} + +/// Helper function to convert a u8 to an AccountId32 +pub fn account_id(id: u8) -> AccountId { + sp_core::crypto::AccountId32::from([id; 32]) +} + +/// Check that the total-issuance matches the sum of all accounts' total balances. +pub fn ensure_ti_valid() { + let mut sum = 0; + + for acc in frame_system::Account::::iter_keys() { + if UseSystem::get() { + let data = frame_system::Pallet::::account(acc); + sum += data.data.total(); + } else { + let data = crate::Account::::get(acc); + sum += data.total(); + } + } + + assert_eq!(TotalIssuance::::get(), sum, "Total Issuance wrong"); +} + +#[test] +fn weights_sane() { + let info = crate::Call::::transfer_allow_death { dest: account_id(10), value: 4 } + .get_dispatch_info(); + assert_eq!(<() as crate::WeightInfo>::transfer_allow_death(), info.call_weight); + + let info = + crate::Call::::force_unreserve { who: account_id(10), amount: 4 }.get_dispatch_info(); + assert_eq!(<() as crate::WeightInfo>::force_unreserve(), info.call_weight); +} + +#[test] +fn check_whitelist() { + let whitelist: BTreeSet = AllPalletsWithSystem::whitelisted_storage_keys() + .iter() + .map(|s| HexDisplay::from(&s.key).to_string()) + .collect(); + // Inactive Issuance + assert!(whitelist.contains("c2261276cc9d1f8598ea4b6a74b15c2f1ccde6872881f893a21de93dfe970cd5")); + // Total Issuance + assert!(whitelist.contains("c2261276cc9d1f8598ea4b6a74b15c2f57c875e4cff74148e4628f264b974c80")); +} diff --git a/pallets/balances/src/tests/reentrancy_tests.rs b/pallets/balances/src/tests/reentrancy_tests.rs new file mode 100644 index 00000000..89b27528 --- /dev/null +++ b/pallets/balances/src/tests/reentrancy_tests.rs @@ -0,0 +1,212 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Tests regarding the reentrancy functionality. + +use super::*; +use frame_support::traits::tokens::{ + Fortitude::Force, + Precision::BestEffort, + Preservation::{Expendable, Protect}, +}; +use fungible::Balanced; + +#[test] +fn transfer_dust_removal_tst1_should_work() { + ExtBuilder::default() + .existential_deposit(100) + .dust_trap(1) + .build_and_execute_with(|| { + // Verification of reentrancy in dust removal + assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), account_id(1), 1000)); + assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), account_id(2), 500)); + + // In this transaction, account 2 free balance + // drops below existential balance + // and dust balance is removed from account 2 + assert_ok!(Balances::transfer_allow_death( + RawOrigin::Signed(account_id(2)).into(), + account_id(3), + 450 + )); + + // As expected dust balance is removed. + assert_eq!(Balances::free_balance(account_id(2)), 0); + + // As expected beneficiary account 3 + // received the transferred fund. + assert_eq!(Balances::free_balance(account_id(3)), 450); + + // Dust balance is deposited to account 1 + // during the process of dust removal. + assert_eq!(Balances::free_balance(account_id(1)), 1050); + + // Verify the events + assert_eq!(System::events().len(), 14); + + System::assert_has_event(RuntimeEvent::Balances(crate::Event::Transfer { + from: account_id(2), + to: account_id(3), + amount: 450, + })); + System::assert_has_event(RuntimeEvent::Balances(crate::Event::DustLost { + account: account_id(2), + amount: 50, + })); + System::assert_has_event(RuntimeEvent::Balances(crate::Event::Deposit { + who: account_id(1), + amount: 50, + })); + }); +} + +#[test] +fn transfer_dust_removal_tst2_should_work() { + ExtBuilder::default() + .existential_deposit(100) + .dust_trap(1) + .build_and_execute_with(|| { + // Verification of reentrancy in dust removal + assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), account_id(1), 1000)); + assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), account_id(2), 500)); + + // In this transaction, account 2 free balance + // drops below existential balance + // and dust balance is removed from account 2 + assert_ok!(Balances::transfer_allow_death( + RawOrigin::Signed(account_id(2)).into(), + account_id(1), + 450 + )); + + // As expected dust balance is removed. + assert_eq!(Balances::free_balance(account_id(2)), 0); + + // Dust balance is deposited to account 1 + assert_eq!(Balances::free_balance(account_id(1)), 1000 + 450 + 50); + // Verify the events + assert_eq!(System::events().len(), 12); + + System::assert_has_event(RuntimeEvent::Balances(crate::Event::Transfer { + from: account_id(2), + to: account_id(1), + amount: 450, + })); + System::assert_has_event(RuntimeEvent::Balances(crate::Event::DustLost { + account: account_id(2), + amount: 50, + })); + System::assert_has_event(RuntimeEvent::Balances(crate::Event::Deposit { + who: account_id(1), + amount: 50, + })); + }); +} + +#[test] +fn repatriating_reserved_balance_dust_removal_should_work() { + ExtBuilder::default() + .existential_deposit(100) + .dust_trap(1) + .build_and_execute_with(|| { + // Verification of reentrancy in dust removal + assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), account_id(1), 1000)); + assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), account_id(2), 500)); + + // Reserve a value on account 2, + // Such that free balance is lower than + // Existential deposit. + assert_ok!(Balances::transfer_allow_death( + RuntimeOrigin::signed(account_id(2)), + account_id(1), + 450 + )); + + // Since free balance of account 2 is lower than + // existential deposit, dust amount is + // removed from the account 2 + assert_eq!(Balances::reserved_balance(account_id(2)), 0); + assert_eq!(Balances::free_balance(account_id(2)), 0); + + // account 1 is credited with reserved amount + // together with dust balance during dust + // removal. + assert_eq!(Balances::reserved_balance(account_id(1)), 0); + assert_eq!(Balances::free_balance(account_id(1)), 1500); + + // Verify the events + assert_eq!(System::events().len(), 12); + + System::assert_has_event(RuntimeEvent::Balances(crate::Event::Transfer { + from: account_id(2), + to: account_id(1), + amount: 450, + })); + System::assert_has_event(RuntimeEvent::Balances(crate::Event::DustLost { + account: account_id(2), + amount: 50, + })); + System::assert_has_event(RuntimeEvent::Balances(crate::Event::Deposit { + who: account_id(1), + amount: 50, + })); + }); +} + +#[test] +fn emit_events_with_no_existential_deposit_suicide_with_dust() { + ExtBuilder::default().existential_deposit(2).build_and_execute_with(|| { + assert_ok!(Balances::force_set_balance(RawOrigin::Root.into(), account_id(1), 100)); + + assert_eq!( + events(), + [ + RuntimeEvent::System(system::Event::NewAccount { account: account_id(1) }), + RuntimeEvent::Balances(crate::Event::Endowed { + account: account_id(1), + free_balance: 100 + }), + RuntimeEvent::Balances(crate::Event::Issued { amount: 100 }), + RuntimeEvent::Balances(crate::Event::BalanceSet { who: account_id(1), free: 100 }), + ] + ); + + let res = Balances::withdraw(&account_id(1), 98, BestEffort, Protect, Force); + assert_eq!(res.unwrap().peek(), 98); + + // no events + assert_eq!( + events(), + [RuntimeEvent::Balances(crate::Event::Withdraw { who: account_id(1), amount: 98 })] + ); + + let res = Balances::withdraw(&account_id(1), 1, BestEffort, Expendable, Force); + assert_eq!(res.unwrap().peek(), 1); + + assert_eq!( + events(), + [ + RuntimeEvent::System(system::Event::KilledAccount { account: account_id(1) }), + RuntimeEvent::Balances(crate::Event::DustLost { + account: account_id(1), + amount: 1 + }), + RuntimeEvent::Balances(crate::Event::Withdraw { who: account_id(1), amount: 1 }), + ] + ); + }); +} diff --git a/pallets/balances/src/tests/transfer_counter_tests.rs b/pallets/balances/src/tests/transfer_counter_tests.rs new file mode 100644 index 00000000..57fb1e7b --- /dev/null +++ b/pallets/balances/src/tests/transfer_counter_tests.rs @@ -0,0 +1,340 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Tests for the global transfer counter functionality. + +use super::*; +use crate::{TransferCount, TransferProof}; +use sp_runtime::{ArithmeticError::Underflow, DispatchError::Arithmetic}; + +/// Alice account ID for more readable tests. +fn alice() -> AccountId { + account_id(1) +} +/// Bob account ID for more readable tests. +fn bob() -> AccountId { + account_id(2) +} +/// Charlie account ID for more readable tests. +fn charlie() -> AccountId { + account_id(3) +} + +/// When monied(true), genesis creates 5 accounts with balances. +/// However, account_id(1) == MintingAccount ([1u8; 32]), so that's a self-transfer +/// which doesn't create a proof. Thus we get 4 transfer proofs. +const GENESIS_TRANSFER_COUNT: u64 = 4; + +#[test] +fn transfer_counter_starts_at_genesis_count() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + // Transfer counter should start at GENESIS_TRANSFER_COUNT (one per endowed account) + assert_eq!(Balances::transfer_count(), GENESIS_TRANSFER_COUNT); + }); +} + +#[test] +fn transfer_allow_death_increments_counter() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + let initial_count = Balances::transfer_count(); + + // Perform a transfer + assert_ok!(Balances::transfer_allow_death(Some(alice()).into(), bob(), 5)); + + // Counter should increment by 1 + assert_eq!(Balances::transfer_count(), initial_count + 1); + + // Perform another transfer + assert_ok!(Balances::transfer_allow_death(Some(bob()).into(), charlie(), 3)); + + // Counter should increment by 2 total + assert_eq!(Balances::transfer_count(), initial_count + 2); + }); +} + +#[test] +fn transfer_keep_alive_increments_counter() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + let initial_count = Balances::transfer_count(); + + // Perform a transfer_keep_alive + assert_ok!(Balances::transfer_keep_alive(Some(alice()).into(), bob(), 5)); + + // Counter should increment by 1 + assert_eq!(Balances::transfer_count(), initial_count + 1); + }); +} + +#[test] +fn force_transfer_increments_counter() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + let initial_count = Balances::transfer_count(); + + // Perform a force transfer + assert_ok!(Balances::force_transfer(RuntimeOrigin::root(), alice(), bob(), 5)); + + // Counter should increment by 1 + assert_eq!(Balances::transfer_count(), initial_count + 1); + }); +} + +#[test] +fn transfer_all_increments_counter() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + let initial_count = Balances::transfer_count(); + + // Perform a transfer_all + assert_ok!(Balances::transfer_all(Some(alice()).into(), bob(), false)); + + // Counter should increment by 1 + assert_eq!(Balances::transfer_count(), initial_count + 1); + }); +} + +#[test] +fn self_transfer_does_not_increment_counter() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + let initial_count = Balances::transfer_count(); + + // Self transfer should not increment counter + assert_ok!(Balances::transfer_allow_death(Some(alice()).into(), alice(), 5)); + + // Counter should remain unchanged since it's a self-transfer + assert_eq!(Balances::transfer_count(), initial_count); + }); +} + +#[test] +fn transfer_proof_storage_is_created() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + let current_count = Balances::transfer_count(); + + // Perform a transfer + assert_ok!(Balances::transfer_allow_death(Some(alice()).into(), bob(), 5)); + + // Check that transfer proof was stored with correct key (using current count as the + // index) + let key = (current_count, alice(), bob(), 5u128); + assert!(TransferProof::::contains_key(&key)); + }); +} + +#[test] +fn multiple_transfers_create_sequential_proofs() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + let initial_count = Balances::transfer_count(); + + // First transfer + assert_ok!(Balances::transfer_allow_death(Some(alice()).into(), bob(), 5)); + assert_eq!(Balances::transfer_count(), initial_count + 1); + + // Check first proof exists + let key1 = (initial_count, alice(), bob(), 5u128); + assert!(TransferProof::::contains_key(&key1)); + + // Second transfer + assert_ok!(Balances::transfer_allow_death(Some(bob()).into(), charlie(), 3)); + assert_eq!(Balances::transfer_count(), initial_count + 2); + + // Check second proof exists + let key2 = (initial_count + 1, bob(), charlie(), 3u128); + assert!(TransferProof::::contains_key(&key2)); + + // Third transfer with different amount + assert_ok!(Balances::transfer_allow_death(Some(alice()).into(), charlie(), 1)); + assert_eq!(Balances::transfer_count(), initial_count + 3); + + // Check third proof exists + let key3 = (initial_count + 2, alice(), charlie(), 1u128); + assert!(TransferProof::::contains_key(&key3)); + }); +} + +#[test] +fn failed_transfers_do_not_increment_counter() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + let initial_count = Balances::transfer_count(); + + // Attempt transfer with insufficient funds + assert_noop!( + Balances::transfer_allow_death(Some(alice()).into(), bob(), 1000), + Arithmetic(Underflow) + ); + + // Counter should remain unchanged since transfer failed + assert_eq!(Balances::transfer_count(), initial_count); + }); +} + +#[test] +fn transfer_proof_storage_key_generation() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + let transfer_count = 5u64; + let from = alice(); + let to = bob(); + let amount = 100u128; + + // Generate storage key + let key = Balances::transfer_proof_storage_key( + transfer_count, + from.clone(), + to.clone(), + amount, + ); + + // Key should not be empty + assert!(!key.is_empty()); + + // The same parameters should generate the same key + let key2 = Balances::transfer_proof_storage_key( + transfer_count, + from.clone(), + to.clone(), + amount, + ); + assert_eq!(key, key2); + + // Different parameters should generate different keys + let key3 = Balances::transfer_proof_storage_key(transfer_count + 1, from, to, amount); + assert_ne!(key, key3); + }); +} + +#[test] +fn counter_saturates_at_max_value() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + // Set counter to near maximum value (u64::MAX - 1) + let near_max = u64::MAX - 1; + TransferCount::::put(near_max); + + assert_eq!(Balances::transfer_count(), near_max); + + // Perform a transfer - should increment to u64::MAX + assert_ok!(Balances::transfer_allow_death(Some(alice()).into(), bob(), 5)); + assert_eq!(Balances::transfer_count(), u64::MAX); + + // Perform another transfer - should saturate at u64::MAX + assert_ok!(Balances::transfer_allow_death(Some(alice()).into(), charlie(), 3)); + assert_eq!(Balances::transfer_count(), u64::MAX); + }); +} + +#[test] +fn transfer_counter_persists_across_blocks() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + let initial_count = Balances::transfer_count(); + + // Perform transfer in block 1 + assert_ok!(Balances::transfer_allow_death(Some(alice()).into(), bob(), 5)); + assert_eq!(Balances::transfer_count(), initial_count + 1); + + // Move to block 2 + System::set_block_number(2); + + // Counter should persist + assert_eq!(Balances::transfer_count(), initial_count + 1); + + // Perform another transfer in block 2 + assert_ok!(Balances::transfer_allow_death(Some(bob()).into(), charlie(), 3)); + assert_eq!(Balances::transfer_count(), initial_count + 2); + }); +} + +#[test] +fn zero_value_transfers_increment_counter() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + let initial_count = Balances::transfer_count(); + + // Perform a zero-value transfer + assert_ok!(Balances::transfer_allow_death(Some(alice()).into(), bob(), 0)); + + // Counter should increment even for zero-value transfers + assert_eq!(Balances::transfer_count(), initial_count + 1); + + // Transfer proof should be created + let key = (initial_count, alice(), bob(), 0u128); + assert!(TransferProof::::contains_key(&key)); + }); +} + +#[test] +fn different_transfer_types_all_increment_counter() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + let initial_count = Balances::transfer_count(); + + // transfer_allow_death + assert_ok!(Balances::transfer_allow_death(Some(alice()).into(), bob(), 1)); + assert_eq!(Balances::transfer_count(), initial_count + 1); + + // transfer_keep_alive + assert_ok!(Balances::transfer_keep_alive(Some(alice()).into(), charlie(), 1)); + assert_eq!(Balances::transfer_count(), initial_count + 2); + + // force_transfer + assert_ok!(Balances::force_transfer(RuntimeOrigin::root(), bob(), charlie(), 1)); + assert_eq!(Balances::transfer_count(), initial_count + 3); + + // transfer_all (transfer remaining balance) + let remaining = Balances::free_balance(alice()); + if remaining > 1 { + assert_ok!(Balances::transfer_all(Some(alice()).into(), bob(), false)); + assert_eq!(Balances::transfer_count(), initial_count + 4); + } + }); +} diff --git a/pallets/balances/src/types.rs b/pallets/balances/src/types.rs new file mode 100644 index 00000000..a7ccfd3d --- /dev/null +++ b/pallets/balances/src/types.rs @@ -0,0 +1,164 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Types used in the pallet. + +use crate::{Config, CreditOf, Event, Pallet}; +use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; +use core::ops::BitOr; +use frame_support::traits::{Imbalance, LockIdentifier, OnUnbalanced, WithdrawReasons}; +use scale_info::TypeInfo; +use sp_runtime::{RuntimeDebug, Saturating}; + +/// Simplified reasons for withdrawing balance. +#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub enum Reasons { + /// Paying system transaction fees. + Fee = 0, + /// Any reason other than paying system transaction fees. + Misc = 1, + /// Any reason at all. + All = 2, +} + +impl From for Reasons { + fn from(r: WithdrawReasons) -> Reasons { + if r == WithdrawReasons::TRANSACTION_PAYMENT { + Reasons::Fee + } else if r.contains(WithdrawReasons::TRANSACTION_PAYMENT) { + Reasons::All + } else { + Reasons::Misc + } + } +} + +impl BitOr for Reasons { + type Output = Reasons; + fn bitor(self, other: Reasons) -> Reasons { + if self == other { + return self; + } + Reasons::All + } +} + +/// A single lock on a balance. There can be many of these on an account and they "overlap", so the +/// same balance is frozen by multiple locks. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub struct BalanceLock { + /// An identifier for this lock. Only one lock may be in existence for each identifier. + pub id: LockIdentifier, + /// The amount which the free balance may not drop below when this lock is in effect. + pub amount: Balance, + /// If true, then the lock remains in effect even for payment of transaction fees. + pub reasons: Reasons, +} + +/// Store named reserved balance. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub struct ReserveData { + /// The identifier for the named reserve. + pub id: ReserveIdentifier, + /// The amount of the named reserve. + pub amount: Balance, +} + +/// All balance information for an account. +#[derive(Encode, Decode, Clone, PartialEq, Eq, Default, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub struct AccountData { + /// Non-reserved part of the balance which the account holder may be able to control. + /// + /// This is the only balance that matters in terms of most operations on tokens. + pub free: Balance, + /// Balance which is has active holds on it and may not be used at all. + /// + /// This is the sum of all individual holds together with any sums still under the (deprecated) + /// reserves API. + pub reserved: Balance, + /// The amount that `free + reserved` may not drop below when reducing the balance, except for + /// actions where the account owner cannot reasonably benefit from the balance reduction, such + /// as slashing. + pub frozen: Balance, + /// Extra information about this account. The MSB is a flag indicating whether the new ref- + /// counting logic is in place for this account. + pub flags: ExtraFlags, +} + +const IS_NEW_LOGIC: u128 = 0x80000000_00000000_00000000_00000000u128; + +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub struct ExtraFlags(pub(crate) u128); +impl Default for ExtraFlags { + fn default() -> Self { + Self(IS_NEW_LOGIC) + } +} +impl ExtraFlags { + pub fn old_logic() -> Self { + Self(0) + } + pub fn set_new_logic(&mut self) { + self.0 |= IS_NEW_LOGIC + } + pub fn is_new_logic(&self) -> bool { + (self.0 & IS_NEW_LOGIC) == IS_NEW_LOGIC + } +} + +impl AccountData { + pub fn usable(&self) -> Balance { + self.free.saturating_sub(self.frozen) + } + + /// The total balance in this account including any that is reserved and ignoring any frozen. + pub fn total(&self) -> Balance { + self.free.saturating_add(self.reserved) + } +} + +pub struct DustCleaner, I: 'static = ()>( + pub(crate) Option<(T::AccountId, CreditOf)>, +); + +impl, I: 'static> Drop for DustCleaner { + fn drop(&mut self) { + if let Some((who, dust)) = self.0.take() { + Pallet::::deposit_event(Event::DustLost { account: who, amount: dust.peek() }); + T::DustRemoval::on_unbalanced(dust); + } + } +} + +/// Whether something should be interpreted as an increase or a decrease. +#[derive( + Encode, + Decode, + Clone, + PartialEq, + Eq, + RuntimeDebug, + MaxEncodedLen, + TypeInfo, + DecodeWithMemTracking, +)] +pub enum AdjustmentDirection { + /// Increase the amount. + Increase, + /// Decrease the amount. + Decrease, +} diff --git a/pallets/balances/src/weights.rs b/pallets/balances/src/weights.rs new file mode 100644 index 00000000..0c7a1354 --- /dev/null +++ b/pallets/balances/src/weights.rs @@ -0,0 +1,300 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Autogenerated weights for `pallet_balances` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 +//! DATE: 2024-11-08, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `runner-wiukf8gn-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` + +// Executed Command: +// ./target/production/substrate-node +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_balances +// --no-storage-info +// --no-median-slopes +// --no-min-squares +// --extrinsic=* +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./substrate/frame/balances/src/weights.rs +// --header=./substrate/HEADER-APACHE2 +// --template=./substrate/.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for `pallet_balances`. +pub trait WeightInfo { + fn transfer_allow_death() -> Weight; + fn transfer_keep_alive() -> Weight; + fn force_set_balance_creating() -> Weight; + fn force_set_balance_killing() -> Weight; + fn force_transfer() -> Weight; + fn transfer_all() -> Weight; + fn force_unreserve() -> Weight; + fn upgrade_accounts(u: u32, ) -> Weight; + fn force_adjust_total_issuance() -> Weight; + fn burn_allow_death() -> Weight; + fn burn_keep_alive() -> Weight; +} + +/// Weights for `pallet_balances` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn transfer_allow_death() -> Weight { + // Proof Size summary in bytes: + // Measured: `52` + // Estimated: `3593` + // Minimum execution time: 50_023_000 picoseconds. + Weight::from_parts(51_105_000, 3593) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn transfer_keep_alive() -> Weight { + // Proof Size summary in bytes: + // Measured: `52` + // Estimated: `3593` + // Minimum execution time: 39_923_000 picoseconds. + Weight::from_parts(40_655_000, 3593) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn force_set_balance_creating() -> Weight { + // Proof Size summary in bytes: + // Measured: `174` + // Estimated: `3593` + // Minimum execution time: 15_062_000 picoseconds. + Weight::from_parts(15_772_000, 3593) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn force_set_balance_killing() -> Weight { + // Proof Size summary in bytes: + // Measured: `174` + // Estimated: `3593` + // Minimum execution time: 21_797_000 picoseconds. + Weight::from_parts(22_287_000, 3593) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn force_transfer() -> Weight { + // Proof Size summary in bytes: + // Measured: `155` + // Estimated: `6196` + // Minimum execution time: 51_425_000 picoseconds. + Weight::from_parts(52_600_000, 6196) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn transfer_all() -> Weight { + // Proof Size summary in bytes: + // Measured: `52` + // Estimated: `3593` + // Minimum execution time: 49_399_000 picoseconds. + Weight::from_parts(51_205_000, 3593) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn force_unreserve() -> Weight { + // Proof Size summary in bytes: + // Measured: `174` + // Estimated: `3593` + // Minimum execution time: 18_119_000 picoseconds. + Weight::from_parts(18_749_000, 3593) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `System::Account` (r:999 w:999) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// The range of component `u` is `[1, 1000]`. + fn upgrade_accounts(u: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0 + u * (135 ±0)` + // Estimated: `990 + u * (2603 ±0)` + // Minimum execution time: 16_783_000 picoseconds. + Weight::from_parts(17_076_000, 990) + // Standard Error: 15_126 + .saturating_add(Weight::from_parts(14_834_157, 0).saturating_mul(u.into())) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(u.into()))) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(u.into()))) + .saturating_add(Weight::from_parts(0, 2603).saturating_mul(u.into())) + } + fn force_adjust_total_issuance() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 6_048_000 picoseconds. + Weight::from_parts(6_346_000, 0) + } + fn burn_allow_death() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 30_215_000 picoseconds. + Weight::from_parts(30_848_000, 0) + } + fn burn_keep_alive() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 20_813_000 picoseconds. + Weight::from_parts(21_553_000, 0) + } +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn transfer_allow_death() -> Weight { + // Proof Size summary in bytes: + // Measured: `52` + // Estimated: `3593` + // Minimum execution time: 50_023_000 picoseconds. + Weight::from_parts(51_105_000, 3593) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn transfer_keep_alive() -> Weight { + // Proof Size summary in bytes: + // Measured: `52` + // Estimated: `3593` + // Minimum execution time: 39_923_000 picoseconds. + Weight::from_parts(40_655_000, 3593) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn force_set_balance_creating() -> Weight { + // Proof Size summary in bytes: + // Measured: `174` + // Estimated: `3593` + // Minimum execution time: 15_062_000 picoseconds. + Weight::from_parts(15_772_000, 3593) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn force_set_balance_killing() -> Weight { + // Proof Size summary in bytes: + // Measured: `174` + // Estimated: `3593` + // Minimum execution time: 21_797_000 picoseconds. + Weight::from_parts(22_287_000, 3593) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn force_transfer() -> Weight { + // Proof Size summary in bytes: + // Measured: `155` + // Estimated: `6196` + // Minimum execution time: 51_425_000 picoseconds. + Weight::from_parts(52_600_000, 6196) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn transfer_all() -> Weight { + // Proof Size summary in bytes: + // Measured: `52` + // Estimated: `3593` + // Minimum execution time: 49_399_000 picoseconds. + Weight::from_parts(51_205_000, 3593) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn force_unreserve() -> Weight { + // Proof Size summary in bytes: + // Measured: `174` + // Estimated: `3593` + // Minimum execution time: 18_119_000 picoseconds. + Weight::from_parts(18_749_000, 3593) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `System::Account` (r:999 w:999) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// The range of component `u` is `[1, 1000]`. + fn upgrade_accounts(u: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0 + u * (135 ±0)` + // Estimated: `990 + u * (2603 ±0)` + // Minimum execution time: 16_783_000 picoseconds. + Weight::from_parts(17_076_000, 990) + // Standard Error: 15_126 + .saturating_add(Weight::from_parts(14_834_157, 0).saturating_mul(u.into())) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(u.into()))) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(u.into()))) + .saturating_add(Weight::from_parts(0, 2603).saturating_mul(u.into())) + } + fn force_adjust_total_issuance() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 6_048_000 picoseconds. + Weight::from_parts(6_346_000, 0) + } + fn burn_allow_death() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 30_215_000 picoseconds. + Weight::from_parts(30_848_000, 0) + } + fn burn_keep_alive() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 20_813_000 picoseconds. + Weight::from_parts(21_553_000, 0) + } +} diff --git a/pallets/mining-rewards/Cargo.toml b/pallets/mining-rewards/Cargo.toml index c5dee9f8..edb186b0 100644 --- a/pallets/mining-rewards/Cargo.toml +++ b/pallets/mining-rewards/Cargo.toml @@ -22,7 +22,6 @@ frame-benchmarking = { optional = true, workspace = true, default-features = fal frame-support.workspace = true frame-system.workspace = true log.workspace = true -qp-poseidon.workspace = true qp-wormhole.workspace = true scale-info = { workspace = true, default-features = false, features = ["derive"] } sp-consensus-pow.workspace = true @@ -31,7 +30,6 @@ sp-runtime.workspace = true [dev-dependencies] pallet-balances.features = ["std"] pallet-balances.workspace = true -qp-poseidon.workspace = true sp-core.workspace = true sp-io.workspace = true @@ -47,7 +45,6 @@ std = [ "frame-benchmarking?/std", "frame-support/std", "frame-system/std", - "qp-poseidon/std", "qp-wormhole/std", "scale-info/std", "sp-consensus-pow/std", diff --git a/pallets/mining-rewards/src/lib.rs b/pallets/mining-rewards/src/lib.rs index e5a77546..4f89231e 100644 --- a/pallets/mining-rewards/src/lib.rs +++ b/pallets/mining-rewards/src/lib.rs @@ -26,15 +26,15 @@ pub mod pallet { }, }; use frame_system::pallet_prelude::*; - use qp_poseidon::PoseidonHasher; - use qp_wormhole::TransferProofRecorder; + use qp_wormhole::TransferProofs; use sp_consensus_pow::POW_ENGINE_ID; use sp_runtime::{ generic::DigestItem, traits::{AccountIdConversion, Saturating}, - Permill, }; + const UNIT: u128 = 1_000_000_000_000u128; + pub(crate) type BalanceOf = <::Currency as Inspect<::AccountId>>::Balance; @@ -50,18 +50,9 @@ pub mod pallet { /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; - /// Currency type for minting rewards - type Currency: Mutate; - - /// Asset ID type for the proof recorder - type AssetId: Default; - - /// Proof recorder for storing wormhole transfer proofs - type ProofRecorder: qp_wormhole::TransferProofRecorder< - Self::AccountId, - Self::AssetId, - BalanceOf, - >; + /// Currency type that also stores zk proofs + type Currency: Mutate + + qp_wormhole::TransferProofs, Self::AccountId>; /// The maximum total supply of tokens #[pallet::constant] @@ -71,13 +62,9 @@ pub mod pallet { #[pallet::constant] type EmissionDivisor: Get>; - /// The portion of rewards that goes to treasury + /// The portion of rewards that goes to treasury (out of 100) #[pallet::constant] - type TreasuryPortion: Get; - - /// The base unit for token amounts (e.g., 1e12 for 12 decimals) - #[pallet::constant] - type Unit: Get>; + type TreasuryPortion: Get; /// The treasury pallet ID #[pallet::constant] @@ -136,10 +123,12 @@ pub mod pallet { let total_reward = remaining_supply .checked_div(&emission_divisor) - .unwrap_or_else(|| BalanceOf::::zero()); + .unwrap_or_else(BalanceOf::::zero); // Split the reward between treasury and miner - let treasury_reward = T::TreasuryPortion::get().mul_floor(total_reward); + let treasury_portion = T::TreasuryPortion::get(); + let treasury_reward = + total_reward.saturating_mul(treasury_portion.into()) / 100u32.into(); let miner_reward = total_reward.saturating_sub(treasury_reward); let tx_fees = >::take(); @@ -147,29 +136,23 @@ pub mod pallet { // Extract miner ID from the pre-runtime digest let miner = Self::extract_miner_from_digest(); - // Log readable amounts (convert to tokens by dividing by unit) - if let (Ok(total), Ok(treasury), Ok(miner_amt), Ok(current), Ok(fees), Ok(unit)) = ( + // Log readable amounts (convert to tokens by dividing by 1e12) + if let (Ok(total), Ok(treasury), Ok(miner_amt), Ok(current), Ok(fees)) = ( TryInto::::try_into(total_reward), TryInto::::try_into(treasury_reward), TryInto::::try_into(miner_reward), TryInto::::try_into(current_supply), TryInto::::try_into(tx_fees), - TryInto::::try_into(T::Unit::get()), ) { let remaining: u128 = TryInto::::try_into(max_supply.saturating_sub(current_supply)) .unwrap_or(0); - let unit_f64 = unit as f64; - log::debug!( - target: "mining-rewards", - "💰 Rewards: total={:.6}, treasury={:.6}, miner={:.6}, fees={:.6}, supply={:.2}, remaining={:.2}", - total as f64 / unit_f64, - treasury as f64 / unit_f64, - miner_amt as f64 / unit_f64, - fees as f64 / unit_f64, - current as f64 / unit_f64, - remaining as f64 / unit_f64 - ); + log::debug!(target: "mining-rewards", "💰 Total reward: {:.6}", total as f64 / UNIT as f64); + log::debug!(target: "mining-rewards", "💰 Treasury reward: {:.6}", treasury as f64 / UNIT as f64); + log::debug!(target: "mining-rewards", "💰 Miner reward: {:.6}", miner_amt as f64 / UNIT as f64); + log::debug!(target: "mining-rewards", "💰 Current supply: {:.2}", current as f64 / UNIT as f64); + log::debug!(target: "mining-rewards", "💰 Remaining supply: {:.2}", remaining as f64 / UNIT as f64); + log::debug!(target: "mining-rewards", "💰 Transaction fees: {:.6}", fees as f64 / UNIT as f64); } // Send fees to miner if any @@ -184,7 +167,7 @@ pub mod pallet { } impl Pallet { - /// Extract miner wormhole address by hashing the preimage from pre-runtime digest + /// Extract miner account ID from the pre-runtime digest fn extract_miner_from_digest() -> Option { // Get the digest from the current block let digest = >::digest(); @@ -193,22 +176,10 @@ pub mod pallet { for log in digest.logs.iter() { if let DigestItem::PreRuntime(engine_id, data) = log { if engine_id == &POW_ENGINE_ID { - // The data is a 32-byte preimage from the incoming block - if data.len() == 32 { - let preimage: [u8; 32] = match data.as_slice().try_into() { - Ok(arr) => arr, - Err(_) => continue, - }; - - // Hash the preimage with Poseidon2 to derive the wormhole address - let wormhole_address_bytes = PoseidonHasher::hash_padded(&preimage); - - // Convert to AccountId - if let Ok(miner) = - T::AccountId::decode(&mut &wormhole_address_bytes[..]) - { - return Some(miner); - } + // Try to decode the accountId + // TODO: to enforce miner wormholes, decode inner hash here + if let Ok(miner) = T::AccountId::decode(&mut &data[..]) { + return Some(miner); } } } @@ -237,12 +208,7 @@ pub mod pallet { Some(miner) => { let _ = T::Currency::mint_into(&miner, reward).defensive(); - let _ = T::ProofRecorder::record_transfer_proof( - None, - mint_account.clone(), - miner.clone(), - reward, - ); + T::Currency::store_transfer_proof(&mint_account, &miner, reward); Self::deposit_event(Event::MinerRewarded { miner: miner.clone(), reward }); }, @@ -250,12 +216,7 @@ pub mod pallet { let treasury = T::TreasuryPalletId::get().into_account_truncating(); let _ = T::Currency::mint_into(&treasury, reward).defensive(); - let _ = T::ProofRecorder::record_transfer_proof( - None, - mint_account.clone(), - treasury.clone(), - reward, - ); + T::Currency::store_transfer_proof(&mint_account, &treasury, reward); Self::deposit_event(Event::TreasuryRewarded { reward }); }, diff --git a/pallets/mining-rewards/src/mock.rs b/pallets/mining-rewards/src/mock.rs index b90560bc..6a620fb9 100644 --- a/pallets/mining-rewards/src/mock.rs +++ b/pallets/mining-rewards/src/mock.rs @@ -1,17 +1,16 @@ use crate as pallet_mining_rewards; - +use codec::Encode; use frame_support::{ parameter_types, traits::{ConstU32, Everything, Hooks}, PalletId, }; -use qp_poseidon::PoseidonHasher; use sp_consensus_pow::POW_ENGINE_ID; use sp_runtime::{ app_crypto::sp_core, testing::H256, traits::{BlakeTwo256, IdentityLookup}, - BuildStorage, DigestItem, Permill, + BuildStorage, Digest, DigestItem, }; // Configure a mock runtime to test the pallet @@ -70,7 +69,6 @@ impl frame_system::Config for Test { } impl pallet_balances::Config for Test { - type RuntimeEvent = RuntimeEvent; type RuntimeHoldReason = (); type RuntimeFreezeReason = (); type WeightInfo = (); @@ -84,70 +82,35 @@ impl pallet_balances::Config for Test { type MaxReserves = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type MintingAccount = MintingAccount; } parameter_types! { - pub const TreasuryPortion: Permill = Permill::from_percent(50); // 50% goes to treasury in tests (matching runtime) + pub const TreasuryPortion: u8 = 50; // 50% goes to treasury in tests (matching runtime) pub const MintingAccount: sp_core::crypto::AccountId32 = sp_core::crypto::AccountId32::new([99u8; 32]); - pub const Unit: u128 = UNIT; -} - -// Mock proof recorder that does nothing -pub struct MockProofRecorder; -impl qp_wormhole::TransferProofRecorder - for MockProofRecorder -{ - type Error = (); - - fn record_transfer_proof( - _asset_id: Option, - _from: sp_core::crypto::AccountId32, - _to: sp_core::crypto::AccountId32, - _amount: u128, - ) -> Result<(), Self::Error> { - Ok(()) - } } impl pallet_mining_rewards::Config for Test { type Currency = Balances; - type AssetId = u32; - type ProofRecorder = MockProofRecorder; type WeightInfo = (); type MaxSupply = MaxSupply; type EmissionDivisor = EmissionDivisor; type TreasuryPortion = TreasuryPortion; type TreasuryPalletId = TreasuryPalletId; type MintingAccount = MintingAccount; - type Unit = Unit; -} - -/// Helper function to convert a u8 to a preimage -pub fn miner_preimage(id: u8) -> [u8; 32] { - [id; 32] -} - -/// Helper function to derive wormhole address from preimage -pub fn wormhole_address_from_preimage(preimage: [u8; 32]) -> sp_core::crypto::AccountId32 { - let hash = PoseidonHasher::hash_padded(&preimage); - sp_core::crypto::AccountId32::from(hash) } -// Configure default miner preimages and addresses for tests -pub fn miner_preimage_1() -> [u8; 32] { - miner_preimage(1) -} - -pub fn miner_preimage_2() -> [u8; 32] { - miner_preimage(2) +/// Helper function to convert a u8 to an AccountId32 +pub fn account_id(id: u8) -> sp_core::crypto::AccountId32 { + sp_core::crypto::AccountId32::from([id; 32]) } +// Configure a default miner account for tests pub fn miner() -> sp_core::crypto::AccountId32 { - wormhole_address_from_preimage(miner_preimage_1()) + account_id(1) } - pub fn miner2() -> sp_core::crypto::AccountId32 { - wormhole_address_from_preimage(miner_preimage_2()) + account_id(2) } // Build genesis storage according to the mock runtime. @@ -156,7 +119,6 @@ pub fn new_test_ext() -> sp_io::TestExternalities { pallet_balances::GenesisConfig:: { balances: vec![(miner(), ExistentialDeposit::get()), (miner2(), ExistentialDeposit::get())], - dev_accounts: None, } .assimilate_storage(&mut t) .unwrap(); @@ -166,27 +128,15 @@ pub fn new_test_ext() -> sp_io::TestExternalities { ext } -// Helper function to create a block digest with a miner preimage -pub fn set_miner_digest(miner_account: sp_core::crypto::AccountId32) { - // Find the preimage that corresponds to this miner address - let preimage = if miner_account == miner() { - miner_preimage_1() - } else if miner_account == miner2() { - miner_preimage_2() - } else { - // For other miners, use their raw bytes as preimage for testing - let mut preimage = [0u8; 32]; - preimage.copy_from_slice(miner_account.as_ref()); - preimage - }; - - set_miner_preimage_digest(preimage); -} +// Helper function to create a block digest with a miner pre-runtime digest +pub fn set_miner_digest(miner: sp_core::crypto::AccountId32) { + let miner_bytes = miner.encode(); + let pre_digest = DigestItem::PreRuntime(POW_ENGINE_ID, miner_bytes); + let digest = Digest { logs: vec![pre_digest] }; -// Helper function to create a block digest with a specific preimage -pub fn set_miner_preimage_digest(preimage: [u8; 32]) { - let pre_digest = DigestItem::PreRuntime(POW_ENGINE_ID, preimage.to_vec()); - System::deposit_log(pre_digest); + // Set the digest in the system + System::reset_events(); + System::initialize(&1, &sp_core::H256::default(), &digest); } // Helper function to run a block diff --git a/pallets/mining-rewards/src/tests.rs b/pallets/mining-rewards/src/tests.rs index d51efa66..04513ca6 100644 --- a/pallets/mining-rewards/src/tests.rs +++ b/pallets/mining-rewards/src/tests.rs @@ -1,6 +1,6 @@ use crate::{mock::*, weights::WeightInfo, Event}; use frame_support::traits::{Currency, Hooks}; -use sp_runtime::{testing::Digest, traits::AccountIdConversion}; +use sp_runtime::traits::AccountIdConversion; #[test] fn miner_reward_works() { @@ -15,7 +15,7 @@ fn miner_reward_works() { // Initial supply is just the existential deposits (2 accounts * 1 unit each = 2) let current_supply = Balances::total_issuance(); let total_reward = (MaxSupply::get() - current_supply) / EmissionDivisor::get(); - let treasury_reward = TreasuryPortion::get().mul_floor(total_reward); + let treasury_reward = total_reward * TreasuryPortion::get() as u128 / 100; let miner_reward = total_reward - treasury_reward; // Run the on_finalize hook @@ -53,7 +53,7 @@ fn miner_reward_with_transaction_fees_works() { // Calculate expected rewards with treasury portion let current_supply = Balances::total_issuance(); let total_block_reward = (MaxSupply::get() - current_supply) / EmissionDivisor::get(); - let treasury_reward = TreasuryPortion::get().mul_floor(total_block_reward); + let treasury_reward = total_block_reward * TreasuryPortion::get() as u128 / 100; let miner_block_reward = total_block_reward - treasury_reward; // Run the on_finalize hook @@ -95,7 +95,7 @@ fn on_unbalanced_collects_fees() { // Calculate expected rewards with treasury portion let current_supply = Balances::total_issuance(); let total_block_reward = (MaxSupply::get() - current_supply) / EmissionDivisor::get(); - let treasury_reward = TreasuryPortion::get().mul_floor(total_block_reward); + let treasury_reward = total_block_reward * TreasuryPortion::get() as u128 / 100; let miner_block_reward = total_block_reward - treasury_reward; // Add a miner to the pre-runtime digest and distribute rewards @@ -122,7 +122,7 @@ fn multiple_blocks_accumulate_rewards() { let total_block1_reward = (MaxSupply::get() - current_supply_block1) / EmissionDivisor::get(); let miner_block1_reward = - total_block1_reward - TreasuryPortion::get().mul_floor(total_block1_reward); + total_block1_reward - (total_block1_reward * TreasuryPortion::get() as u128 / 100); MiningRewards::on_finalize(1); @@ -137,7 +137,7 @@ fn multiple_blocks_accumulate_rewards() { let total_block2_reward = (MaxSupply::get() - current_supply_block2) / EmissionDivisor::get(); let miner_block2_reward = - total_block2_reward - TreasuryPortion::get().mul_floor(total_block2_reward); + total_block2_reward - (total_block2_reward * TreasuryPortion::get() as u128 / 100); MiningRewards::on_finalize(2); @@ -164,7 +164,7 @@ fn different_miners_get_different_rewards() { let total_block1_reward = (MaxSupply::get() - current_supply_block1) / EmissionDivisor::get(); let miner_block1_reward = - total_block1_reward - TreasuryPortion::get().mul_floor(total_block1_reward); + total_block1_reward - (total_block1_reward * TreasuryPortion::get() as u128 / 100); MiningRewards::on_finalize(1); @@ -172,9 +172,7 @@ fn different_miners_get_different_rewards() { assert_eq!(Balances::free_balance(miner()), balance_after_block_1); // Block 2 - Second miner - let block_1 = System::finalize(); - // reset logs and go to block 2 - System::initialize(&2, &block_1.hash(), &Digest { logs: vec![] }); + System::set_block_number(2); set_miner_digest(miner2()); MiningRewards::collect_transaction_fees(20); @@ -182,12 +180,10 @@ fn different_miners_get_different_rewards() { let total_block2_reward = (MaxSupply::get() - current_supply_block2) / EmissionDivisor::get(); let miner_block2_reward = - total_block2_reward - TreasuryPortion::get().mul_floor(total_block2_reward); + total_block2_reward - (total_block2_reward * TreasuryPortion::get() as u128 / 100); MiningRewards::on_finalize(2); - println!("Balnce {}", Balances::free_balance(miner())); - // Check second miner balance assert_eq!( Balances::free_balance(miner2()), @@ -217,7 +213,7 @@ fn transaction_fees_collector_works() { let current_supply = Balances::total_issuance(); let total_block_reward = (MaxSupply::get() - current_supply) / EmissionDivisor::get(); let miner_block_reward = - total_block_reward - TreasuryPortion::get().mul_floor(total_block_reward); + total_block_reward - (total_block_reward * TreasuryPortion::get() as u128 / 100); // Reward miner set_miner_digest(miner()); @@ -247,7 +243,7 @@ fn block_lifecycle_works() { let current_supply = Balances::total_issuance(); let total_block_reward = (MaxSupply::get() - current_supply) / EmissionDivisor::get(); let miner_block_reward = - total_block_reward - TreasuryPortion::get().mul_floor(total_block_reward); + total_block_reward - (total_block_reward * TreasuryPortion::get() as u128 / 100); // 3. on_finalize - should reward the miner set_miner_digest(miner()); @@ -300,7 +296,7 @@ fn rewards_go_to_treasury_when_no_miner() { // Calculate expected rewards - when no miner, all rewards go to treasury let current_supply = Balances::total_issuance(); let total_reward = (MaxSupply::get() - current_supply) / EmissionDivisor::get(); - let treasury_portion_reward = TreasuryPortion::get().mul_floor(total_reward); + let treasury_portion_reward = total_reward * TreasuryPortion::get() as u128 / 100; let miner_portion_reward = total_reward - treasury_portion_reward; // Create a block without a miner (no digest set) @@ -324,11 +320,10 @@ fn rewards_go_to_treasury_when_no_miner() { #[test] fn test_fees_and_rewards_to_miner() { new_test_ext().execute_with(|| { - // Use a test preimage and derive the wormhole address - let test_preimage = [42u8; 32]; // Use a distinct preimage for this test - let miner_wormhole_address = wormhole_address_from_preimage(test_preimage); - let _ = Balances::deposit_creating(&miner_wormhole_address, 0); // Create account - let actual_initial_balance_after_creation = Balances::free_balance(&miner_wormhole_address); + // Set up initial balances + let miner = account_id(1); + let _ = Balances::deposit_creating(&miner, 0); // Create account, balance might become ExistentialDeposit + let actual_initial_balance_after_creation = Balances::free_balance(&miner); // Set transaction fees let tx_fees = 100; @@ -337,18 +332,18 @@ fn test_fees_and_rewards_to_miner() { // Calculate expected rewards with treasury portion let current_supply = Balances::total_issuance(); let total_block_reward = (MaxSupply::get() - current_supply) / EmissionDivisor::get(); - let treasury_reward = TreasuryPortion::get().mul_floor(total_block_reward); + let treasury_reward = total_block_reward * TreasuryPortion::get() as u128 / 100; let miner_block_reward = total_block_reward - treasury_reward; - // Create a block with the preimage + // Create a block with a miner System::set_block_number(1); - set_miner_preimage_digest(test_preimage); + set_miner_digest(miner.clone()); // Run on_finalize MiningRewards::on_finalize(System::block_number()); // Get actual values from the system AFTER on_finalize - let miner_balance_after_finalize = Balances::free_balance(&miner_wormhole_address); + let miner_balance_after_finalize = Balances::free_balance(&miner); // Check miner balance - should get miner portion of block reward + all fees assert_eq!( @@ -360,16 +355,13 @@ fn test_fees_and_rewards_to_miner() { // Verify events System::assert_has_event( Event::MinerRewarded { - miner: miner_wormhole_address.clone(), + miner: miner.clone(), reward: 100, // all fees go to miner } .into(), ); - System::assert_has_event( - Event::MinerRewarded { miner: miner_wormhole_address, reward: miner_block_reward } - .into(), - ); + System::assert_has_event(Event::MinerRewarded { miner, reward: miner_block_reward }.into()); System::assert_has_event(Event::TreasuryRewarded { reward: treasury_reward }.into()); }); @@ -385,7 +377,7 @@ fn test_emission_simulation_120m_blocks() { println!("=== Mining Rewards Emission Simulation ==="); println!("Max Supply: {:.0} tokens", MaxSupply::get() as f64 / UNIT as f64); println!("Emission Divisor: {:?}", EmissionDivisor::get()); - println!("Treasury Portion: {:?}", TreasuryPortion::get()); + println!("Treasury Portion: {}%", TreasuryPortion::get()); println!(); const MAX_BLOCKS: u32 = 130_000_000; @@ -404,7 +396,7 @@ fn test_emission_simulation_120m_blocks() { // Print initial state let remaining = MaxSupply::get() - current_supply; let block_reward = if remaining > 0 { remaining / EmissionDivisor::get() } else { 0 }; - let treasury_reward = TreasuryPortion::get().mul_floor(block_reward); + let treasury_reward = block_reward * TreasuryPortion::get() as u128 / 100; let miner_reward = block_reward - treasury_reward; println!( @@ -435,7 +427,7 @@ fn test_emission_simulation_120m_blocks() { } let block_reward = remaining_supply / EmissionDivisor::get(); - let treasury_reward = TreasuryPortion::get().mul_floor(block_reward); + let treasury_reward = block_reward * TreasuryPortion::get() as u128 / 100; let miner_reward = block_reward - treasury_reward; // Update totals (simulate the minting) @@ -453,7 +445,7 @@ fn test_emission_simulation_120m_blocks() { // Print progress report let remaining = MaxSupply::get().saturating_sub(current_supply); let next_block_reward = if remaining > 0 { remaining / EmissionDivisor::get() } else { 0 }; - let next_treasury = TreasuryPortion::get().mul_floor(next_block_reward); + let next_treasury = next_block_reward * TreasuryPortion::get() as u128 / 100; let next_miner = next_block_reward - next_treasury; println!( diff --git a/pallets/multisig/src/mock.rs b/pallets/multisig/src/mock.rs index 93a3ed8f..38f241d6 100644 --- a/pallets/multisig/src/mock.rs +++ b/pallets/multisig/src/mock.rs @@ -71,7 +71,6 @@ parameter_types! { } impl pallet_balances::Config for Test { - type RuntimeEvent = RuntimeEvent; type WeightInfo = (); type Balance = Balance; type DustRemoval = (); @@ -85,6 +84,7 @@ impl pallet_balances::Config for Test { type FreezeIdentifier = (); type MaxFreezes = MaxFreezes; type DoneSlashHandler = (); + type MintingAccount = MintingAccount; } parameter_types! { @@ -135,7 +135,6 @@ pub fn new_test_ext() -> sp_io::TestExternalities { (account_id(4), 400000), // Dave (account_id(5), 500000), // Eve ], - dev_accounts: None, } .assimilate_storage(&mut t) .unwrap(); diff --git a/pallets/reversible-transfers/src/tests/mock.rs b/pallets/reversible-transfers/src/tests/mock.rs index 3ec56572..e1d829b7 100644 --- a/pallets/reversible-transfers/src/tests/mock.rs +++ b/pallets/reversible-transfers/src/tests/mock.rs @@ -149,6 +149,7 @@ impl pallet_balances::Config for Test { type WeightInfo = (); type RuntimeHoldReason = RuntimeHoldReason; type MaxFreezes = MaxReversibleTransfers; + type MintingAccount = MintingAccount; } // In memory storage @@ -345,7 +346,6 @@ pub fn new_test_ext() -> sp_io::TestExternalities { (account_id(110), 100_000_000_000), (account_id(111), 100_000_000_000), ], - dev_accounts: None, } .assimilate_storage(&mut t) .unwrap(); diff --git a/pallets/wormhole/Cargo.toml b/pallets/wormhole/Cargo.toml index baeaada7..54dba6aa 100644 --- a/pallets/wormhole/Cargo.toml +++ b/pallets/wormhole/Cargo.toml @@ -16,31 +16,19 @@ hex = { workspace = true, features = ["alloc"], optional = true } lazy_static.workspace = true log.workspace = true pallet-balances.workspace = true -qp-header = { workspace = true, features = ["serde"] } -qp-poseidon.workspace = true qp-wormhole.workspace = true +qp-wormhole-circuit = { workspace = true, default-features = false } qp-wormhole-verifier = { workspace = true, default-features = false } +qp-zk-circuits-common = { workspace = true, default-features = false } scale-info = { workspace = true, default-features = false, features = [ "derive", ] } sp-core.workspace = true sp-io.workspace = true -sp-metadata-ir.workspace = true sp-runtime.workspace = true [dev-dependencies] hex = { workspace = true, features = ["alloc"] } -pallet-assets = { workspace = true, features = ["std"] } -qp-dilithium-crypto = { workspace = true, features = ["std"] } -qp-plonky2 = { workspace = true, default-features = false } -qp-poseidon-core.workspace = true -qp-wormhole-circuit.workspace = true -qp-wormhole-circuit-builder = { workspace = true, default-features = false } -qp-wormhole-prover.workspace = true -qp-wormhole-verifier = { workspace = true, default-features = false } -qp-zk-circuits-common = { workspace = true, default-features = false } -sp-state-machine = { path = "../../primitives/state-machine" } -sp-trie.workspace = true [features] default = ["std"] @@ -60,10 +48,10 @@ std = [ "lazy_static/spin_no_std", "log/std", "pallet-balances/std", - "qp-header/std", - "qp-poseidon/std", + "qp-wormhole-circuit/std", "qp-wormhole-verifier/std", "qp-wormhole/std", + "qp-zk-circuits-common/std", "scale-info/std", "sp-core/std", "sp-io/std", diff --git a/pallets/wormhole/aggregated_common.bin b/pallets/wormhole/aggregated_common.bin deleted file mode 100644 index 5fbea0f3cae044a80d0ad7def777f01eaf656179..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1905 zcmbuAe^Aq99LG1Iw8@Wwj9>9%L*`-Ix`7H!2wHG}8jBm^DY46--C-GNa)JXnY%V5f zK^hDhNP%#+t{)2{y#zMc?34sn3ezwm2&21U3R1Sbc-HgoxcaN>x%=Mh{eGVBeLv6V zyXX5}CF8G=Nx6*j#eet})H4}wRJl`zP_h@v#NMmYw#idBboGm5Y2+v zEr`M(x&j}j&jXiJ58|bPkI;3HMLoEZdeDP<(35)5i+a$TdJr$nht31oFNpnt*bj*M zAnJjr1NzcJpda<1KlNZB^y?Y_;AFiS*>yg(I5PYoeP5?e?5%t&?Z{_^o#u)i*{{RBUbeGSe$X?{>wn zW9YcSk-GN7oNb2t4U-xC0a3WY-4Lv)SDdm>=x2HhuZzc?yU-if;x;WRQ#90ej7ip% ziEd7fjL+Qao!@@$Qqd-TVY)K5tF^g*Tbr+fH^IEyxoOL+F{6D|rs`CMbHtMuSCHXT z9(dsNM~A$ll=(U0D9(sBeEnol=*GwGPbahpxj(X%X5+EY#N<7@L?^O_zC6@ZlANbJKe4?D2bu3p#Tb)A{=Tf=3)0X=_oC$5CVu=0QYN@=LQDu60{m*|4{M2Xu zomF^KysK{Q#0z=CtmfT{ZVk^-5moP&lszbYW?_p-cCA=$PSZ2{;wlZDPu|wsr%u2B zt9r!LVfpsr(b22U@$t$B167d$DtCRUaQ6y}`bL_VbdiD_VQA@0DZ$@kxsp2EIe&h--eSVG{HAsb6YbD|kMG ztiZTdkesd+7$$Op)FB$VLNv0D=%x9B?T~qLkwJ)TBN}-_G%}88z5a$)`k+E?!pk^>&gHRX$Ingu`y8n-c zJ4+g-gc+nLhyn3)HX@t(d~8{_e(RT+vai%?7x2){Ce*}7_VylH$PGOLr)tkX#p})s zr7qItfqi#*J=?eEk2o=|CDSY_n{R`|O1U8T??>5(E688U=+Uj_pQxX(RL~e&kD1Pi z_i`~Yi$@m0KytTd7b+bf7E}nZ_#UPwlzOM)`cxy{9CBMxTGcWQxCcW?M<{4g+(VC~ z4v$C+4f{RIosj!{M*%kuv>w@IFe22t-faOfu;e#vsJH)K`f9dloD`s(J){@l1q|hK qi>>->!lCIbJkXSjfTHKatUZk#S Vec { hex::decode(hex_proof.trim()).expect("Failed to decode hex proof") } -#[benchmarks] +#[benchmarks( + where + T: Send + Sync, + T: Config, + BalanceOf: Into<<::Currency as Inspect>::Balance>, +)] mod benchmarks { use super::*; @@ -26,7 +27,6 @@ mod benchmarks { fn verify_wormhole_proof() -> Result<(), BenchmarkError> { let proof_bytes = get_benchmark_proof(); - // Parse the proof to get public inputs let verifier = crate::get_wormhole_verifier() .map_err(|_| BenchmarkError::Stop("Verifier not available"))?; @@ -39,43 +39,28 @@ mod benchmarks { let public_inputs = PublicCircuitInputs::try_from(&proof) .map_err(|_| BenchmarkError::Stop("Invalid public inputs"))?; - // Extract values from public inputs let nullifier_bytes = *public_inputs.nullifier; - let block_number_u32 = public_inputs.block_number; - let block_hash_bytes: [u8; 32] = (*public_inputs.block_hash) - .try_into() - .map_err(|_| BenchmarkError::Stop("Invalid block hash length"))?; - // Ensure nullifier hasn't been used ensure!( !UsedNullifiers::::contains_key(nullifier_bytes), BenchmarkError::Stop("Nullifier already used") ); - // Verify the proof is valid (sanity check) verifier .verify(proof) .map_err(|_| BenchmarkError::Stop("Proof verification failed"))?; - // Set up storage to match the proof's public inputs: - // Set current block number to be >= proof's block_number - let block_number: BlockNumberFor = block_number_u32.into(); - frame_system::Pallet::::set_block_number(block_number + 1u32.into()); - - // Override block hash to match proof's block_hash - let block_hash = T::Hash::decode(&mut &block_hash_bytes[..]) - .map_err(|_| BenchmarkError::Stop("Failed to decode block hash"))?; - frame_system::BlockHash::::insert(block_number, block_hash); + let block_number = frame_system::Pallet::::block_number(); #[extrinsic_call] - verify_wormhole_proof(RawOrigin::None, proof_bytes); - - // Verify nullifier was marked as used - ensure!( - UsedNullifiers::::contains_key(nullifier_bytes), - BenchmarkError::Stop("Nullifier should be marked as used after verification") - ); + verify_wormhole_proof(RawOrigin::None, proof_bytes, block_number); Ok(()) } + + impl_benchmark_test_suite! { + Pallet, + crate::mock::new_test_ext(), + crate::mock::Test, + } } diff --git a/pallets/wormhole/src/lib.rs b/pallets/wormhole/src/lib.rs index ecc31340..36457741 100644 --- a/pallets/wormhole/src/lib.rs +++ b/pallets/wormhole/src/lib.rs @@ -2,13 +2,8 @@ extern crate alloc; -use core::marker::PhantomData; - -use codec::{Decode, MaxEncodedLen}; -use frame_support::StorageHasher; use lazy_static::lazy_static; pub use pallet::*; -pub use qp_poseidon::{PoseidonHasher as PoseidonCore, ToFelts}; use qp_wormhole_verifier::WormholeVerifier; #[cfg(test)] @@ -16,7 +11,6 @@ mod mock; #[cfg(test)] mod tests; pub mod weights; -use sp_metadata_ir::StorageHasherIR; pub use weights::*; #[cfg(feature = "runtime-benchmarks")] @@ -28,11 +22,6 @@ lazy_static! { let common_bytes = include_bytes!("../common.bin"); WormholeVerifier::new_from_bytes(verifier_bytes, common_bytes).ok() }; - static ref AGGREGATED_VERIFIER: Option = { - let verifier_bytes = include_bytes!("../aggregated_verifier.bin"); - let common_bytes = include_bytes!("../aggregated_common.bin"); - WormholeVerifier::new_from_bytes(verifier_bytes, common_bytes).ok() - }; } // Add a safe getter function @@ -40,141 +29,51 @@ pub fn get_wormhole_verifier() -> Result<&'static WormholeVerifier, &'static str WORMHOLE_VERIFIER.as_ref().ok_or("Wormhole verifier not available") } -// Getter for aggregated verifier -pub fn get_aggregated_verifier() -> Result<&'static WormholeVerifier, &'static str> { - AGGREGATED_VERIFIER.as_ref().ok_or("Aggregated verifier not available") -} - -/// Scale factor for quantizing amounts from 12 to 2 decimal places (10^10). -/// Amounts in the circuit are stored as u32 with 2 decimal places of precision. -/// On-chain amounts use 12 decimal places, so we multiply by this factor when -/// converting from circuit amounts to on-chain amounts. -pub const SCALE_DOWN_FACTOR: u128 = 10_000_000_000; - -// We use a generic struct so we can pass the specific Key type to the hasher -pub struct PoseidonStorageHasher(PhantomData); - -impl StorageHasher for PoseidonStorageHasher { - // We are lying here, but maybe it's ok because it's just metadata - const METADATA: StorageHasherIR = StorageHasherIR::Identity; - type Output = [u8; 32]; - - fn hash(x: &[u8]) -> Self::Output { - PoseidonCore::hash_storage::(x) - } - - fn max_len() -> usize { - 32 - } -} - #[frame_support::pallet] pub mod pallet { - use crate::{PoseidonStorageHasher, ToFelts, WeightInfo}; + use crate::WeightInfo; use alloc::vec::Vec; use codec::Decode; use frame_support::{ - dispatch::DispatchResult, pallet_prelude::*, traits::{ fungible::{Mutate, Unbalanced}, - fungibles::{self, Inspect as FungiblesInspect, Mutate as FungiblesMutate}, - tokens::Preservation, - Currency, + Currency, ExistenceRequirement, WithdrawReasons, }, + weights::WeightToFee, }; use frame_system::pallet_prelude::*; - use qp_wormhole_verifier::{ - parse_aggregated_public_inputs, parse_public_inputs, ProofWithPublicInputs, C, D, F, - }; + use qp_wormhole::TransferProofs; + use qp_wormhole_circuit::inputs::PublicCircuitInputs; + use qp_wormhole_verifier::ProofWithPublicInputs; + use qp_zk_circuits_common::circuit::{C, D, F}; use sp_runtime::{ - traits::{MaybeDisplay, Saturating, StaticLookup, Zero}, - transaction_validity::{ - InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity, - ValidTransaction, - }, - Permill, + traits::{Saturating, Zero}, + Perbill, }; pub type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; - pub type AssetIdOf = <::Assets as fungibles::Inspect< - ::AccountId, - >>::AssetId; - pub type AssetBalanceOf = <::Assets as fungibles::Inspect< - ::AccountId, - >>::Balance; - pub type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; - - pub type TransferProofKey = ( - AssetIdOf, - ::TransferCount, - ::WormholeAccountId, - ::WormholeAccountId, - BalanceOf, - ); #[pallet::pallet] pub struct Pallet(_); #[pallet::config] - pub trait Config: frame_system::Config - where - AssetIdOf: Default + From + Clone + ToFelts, - BalanceOf: Default + ToFelts, - AssetBalanceOf: Into> + From>, - { - /// Currency type used for native token transfers and minting - type Currency: Mutate<::AccountId, Balance = BalanceOf> - + Unbalanced<::AccountId> - + Currency<::AccountId>; - - /// Assets type used for managing fungible assets - type Assets: fungibles::Inspect<::AccountId> - + fungibles::Mutate<::AccountId> - + fungibles::Create<::AccountId>; - - /// Transfer count type used in storage - type TransferCount: Parameter - + MaxEncodedLen - + Default - + Saturating - + Copy - + sp_runtime::traits::One - + ToFelts; + pub trait Config: frame_system::Config { + /// Currency type used for minting tokens and handling wormhole transfers + type Currency: Mutate> + + TransferProofs, Self::AccountId> + + Unbalanced + + Currency; /// Account ID used as the "from" account when creating transfer proofs for minted tokens #[pallet::constant] - type MintingAccount: Get<::AccountId>; - - /// Minimum transfer amount required for proof verification - #[pallet::constant] - type MinimumTransferAmount: Get>; - - /// Volume fee rate in basis points (1 basis point = 0.01%). - /// This must match the fee rate used in proof generation. - #[pallet::constant] - type VolumeFeeRateBps: Get; - - /// Proportion of volume fees to burn (not mint). The remainder goes to the block author. - /// Example: Permill::from_percent(50) means 50% burned, 50% to miner. - #[pallet::constant] - type VolumeFeesBurnRate: Get; + type MintingAccount: Get; /// Weight information for pallet operations. type WeightInfo: WeightInfo; - /// Override system AccountId to make it felts encodable - type WormholeAccountId: Parameter - + Member - + MaybeSerializeDeserialize - + core::fmt::Debug - + MaybeDisplay - + Ord - + MaxEncodedLen - + ToFelts - + Into<::AccountId> - + From<::AccountId>; + type WeightToFee: WeightToFee>; } #[pallet::storage] @@ -182,42 +81,10 @@ pub mod pallet { pub(super) type UsedNullifiers = StorageMap<_, Blake2_128Concat, [u8; 32], bool, ValueQuery>; - /// Transfer proofs for wormhole transfers (both native and assets) - #[pallet::storage] - #[pallet::getter(fn transfer_proof)] - pub type TransferProof = StorageMap< - _, - PoseidonStorageHasher>, - TransferProofKey, - (), - OptionQuery, - >; - - /// Transfer count for all wormhole transfers - #[pallet::storage] - #[pallet::getter(fn transfer_count)] - pub type TransferCount = - StorageMap<_, Blake2_128Concat, T::WormholeAccountId, T::TransferCount, ValueQuery>; - #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { - NativeTransferred { - from: ::AccountId, - to: ::AccountId, - amount: BalanceOf, - transfer_count: T::TransferCount, - }, - AssetTransferred { - asset_id: AssetIdOf, - from: ::AccountId, - to: ::AccountId, - amount: AssetBalanceOf, - transfer_count: T::TransferCount, - }, - ProofVerified { - exit_amount: BalanceOf, - }, + ProofVerified { exit_amount: BalanceOf }, } #[pallet::error] @@ -232,22 +99,17 @@ pub mod pallet { StorageRootMismatch, BlockNotFound, InvalidBlockNumber, - AssetNotFound, - SelfTransfer, - AggregatedVerifierNotAvailable, - AggregatedProofDeserializationFailed, - AggregatedVerificationFailed, - InvalidAggregatedPublicInputs, - TransferAmountBelowMinimum, - /// The volume fee rate in the proof doesn't match the configured rate - InvalidVolumeFeeRate, } #[pallet::call] impl Pallet { #[pallet::call_index(0)] #[pallet::weight(::WeightInfo::verify_wormhole_proof())] - pub fn verify_wormhole_proof(origin: OriginFor, proof_bytes: Vec) -> DispatchResult { + pub fn verify_wormhole_proof( + origin: OriginFor, + proof_bytes: Vec, + block_number: BlockNumberFor, + ) -> DispatchResult { ensure_none(origin)?; let verifier = @@ -259,9 +121,9 @@ pub mod pallet { ) .map_err(|_| Error::::ProofDeserializationFailed)?; - // Parse public inputs using the verifier's parser - let public_inputs = - parse_public_inputs(&proof).map_err(|_| Error::::InvalidPublicInputs)?; + // Parse public inputs using the existing parser + let public_inputs = PublicCircuitInputs::try_from(&proof) + .map_err(|_| Error::::InvalidPublicInputs)?; let nullifier_bytes = *public_inputs.nullifier; @@ -271,9 +133,6 @@ pub mod pallet { Error::::NullifierAlreadyUsed ); - // Extract the block number from public inputs - let block_number = BlockNumberFor::::from(public_inputs.block_number); - // Get the block hash for the specified block number let block_hash = frame_system::Pallet::::block_hash(block_number); @@ -286,27 +145,43 @@ pub mod pallet { let default_hash = T::Hash::default(); ensure!(block_hash != default_hash, Error::::BlockNotFound); - // Ensure that the block hash from storage matches the one in public inputs - ensure!( - block_hash.as_ref() == public_inputs.block_hash.as_ref(), - Error::::InvalidPublicInputs - ); + // Get the storage root for the specified block + let storage_root = sp_io::storage::root(sp_runtime::StateVersion::V1); + + let root_hash = public_inputs.root_hash; + let storage_root_bytes = storage_root.as_slice(); + + // Compare the root_hash from the proof with the actual storage root + // Skip storage root validation in test and benchmark environments since proofs + // may have been generated with different state + #[cfg(not(any(test, feature = "runtime-benchmarks")))] + if root_hash.as_ref() != storage_root_bytes { + log::warn!( + target: "wormhole", + "Storage root mismatch for block {:?}: expected {:?}, got {:?}", + block_number, + root_hash.as_ref(), + storage_root_bytes + ); + return Err(Error::::StorageRootMismatch.into()); + } + + #[cfg(any(test, feature = "runtime-benchmarks"))] + { + let _root_hash = root_hash; + let _storage_root_bytes = storage_root_bytes; + log::debug!( + target: "wormhole", + "Skipping storage root validation in test/benchmark environment" + ); + } verifier.verify(proof.clone()).map_err(|_| Error::::VerificationFailed)?; // Mark nullifier as used UsedNullifiers::::insert(nullifier_bytes, true); - // Verify the volume fee rate matches our configured rate - ensure!( - public_inputs.volume_fee_bps == T::VolumeFeeRateBps::get(), - Error::::InvalidVolumeFeeRate - ); - - // The output_amount is what the user receives after fee deduction (already enforced by - // circuit) - let exit_balance_u128 = - (public_inputs.output_amount as u128).saturating_mul(crate::SCALE_DOWN_FACTOR); + let exit_balance_u128 = public_inputs.funding_amount; // Convert to Balance type let exit_balance: BalanceOf = @@ -314,477 +189,44 @@ pub mod pallet { // Decode exit account from public inputs let exit_account_bytes = *public_inputs.exit_account; - let exit_account = - ::AccountId::decode(&mut &exit_account_bytes[..]) - .map_err(|_| Error::::InvalidPublicInputs)?; - - // Extract asset_id from public inputs - let asset_id_u32 = public_inputs.asset_id; - let asset_id: AssetIdOf = asset_id_u32.into(); - - // Ensure transfer amount meets minimum requirement - ensure!( - exit_balance >= T::MinimumTransferAmount::get(), - Error::::TransferAmountBelowMinimum - ); - - // Compute the total fee that was deducted from input to get output - // fee = output_amount * volume_fee_bps / (10000 - volume_fee_bps) - let fee_bps = T::VolumeFeeRateBps::get() as u128; - let fee_u128 = exit_balance_u128 - .saturating_mul(fee_bps) - .checked_div(10000u128.saturating_sub(fee_bps)) - .unwrap_or(0); - - // Fee distribution: configurable portion burned, remainder to miner - // burn_rate determines what proportion is burned (reduces total issuance) - let burn_rate = T::VolumeFeesBurnRate::get(); - let burn_amount_u128 = burn_rate * fee_u128; - let miner_fee_u128 = fee_u128.saturating_sub(burn_amount_u128); - let miner_fee: BalanceOf = - miner_fee_u128.try_into().map_err(|_| Error::::InvalidPublicInputs)?; - let burn_amount: BalanceOf = - burn_amount_u128.try_into().map_err(|_| Error::::InvalidPublicInputs)?; - - // Burn the burned portion by reducing total issuance - // This offsets the supply increase from minting exit_balance + miner_fee - // The PositiveImbalance is dropped, which is a no-op (already reduced issuance) - if !burn_amount.is_zero() { - let _ = >::burn(burn_amount); - } - - // Handle native (asset_id = 0) or asset transfers - if asset_id == AssetIdOf::::default() { - // Native token transfer - // Mint tokens to the exit account - // This does not affect total issuance and does not create an imbalance - >::increase_balance( - &exit_account, - exit_balance, - frame_support::traits::tokens::Precision::Exact, - )?; + let exit_account = T::AccountId::decode(&mut &exit_account_bytes[..]) + .map_err(|_| Error::::InvalidPublicInputs)?; + + // Calculate fees first + let weight = ::WeightInfo::verify_wormhole_proof(); + let weight_fee = T::WeightToFee::weight_to_fee(&weight); + let volume_fee_perbill = Perbill::from_rational(1u32, 1000u32); + let volume_fee = volume_fee_perbill * exit_balance; + let total_fee = weight_fee.saturating_add(volume_fee); + + // Mint tokens to the exit account + // This does not affect total issuance and does not create an imbalance + >::increase_balance( + &exit_account, + exit_balance, + frame_support::traits::tokens::Precision::Exact, + )?; - // Mint miner's portion of volume fee to block author - // The remaining portion (fee - miner_fee) is not minted, effectively burned - if !miner_fee.is_zero() { - if let Some(author) = frame_system::Pallet::::digest() - .logs - .iter() - .find_map(|item| item.as_pre_runtime()) - .and_then(|(_, data)| { - ::AccountId::decode(&mut &data[..]).ok() - }) { - >::increase_balance( - &author, - miner_fee, - frame_support::traits::tokens::Precision::Exact, - )?; - } - } - } else { - // Asset transfer - let asset_balance: AssetBalanceOf = exit_balance.into(); - >::mint_into( - asset_id.clone(), + // Withdraw fee from exit account if fees are non-zero + // This creates a negative imbalance that will be handled by the transaction payment + // pallet + if !total_fee.is_zero() { + let _fee_imbalance = T::Currency::withdraw( &exit_account, - asset_balance, + total_fee, + WithdrawReasons::TRANSACTION_PAYMENT, + ExistenceRequirement::KeepAlive, )?; - - // Mint miner's portion of volume fee to block author (fee is in native currency) - // The remaining portion (fee - miner_fee) is not minted, effectively burned - if !miner_fee.is_zero() { - if let Some(author) = frame_system::Pallet::::digest() - .logs - .iter() - .find_map(|item| item.as_pre_runtime()) - .and_then(|(_, data)| { - ::AccountId::decode(&mut &data[..]).ok() - }) { - >::increase_balance( - &author, - miner_fee, - frame_support::traits::tokens::Precision::Exact, - )?; - } - } } // Create a transfer proof for the minted tokens let mint_account = T::MintingAccount::get(); - Self::record_transfer( - asset_id, - mint_account.into(), - exit_account.into(), - exit_balance, - )?; + T::Currency::store_transfer_proof(&mint_account, &exit_account, exit_balance); // Emit event Self::deposit_event(Event::ProofVerified { exit_amount: exit_balance }); Ok(()) } - - /// Transfer native tokens and store proof for wormhole - #[pallet::call_index(1)] - #[pallet::weight(T::DbWeight::get().reads_writes(1, 2))] - pub fn transfer_native( - origin: OriginFor, - dest: AccountIdLookupOf, - #[pallet::compact] amount: BalanceOf, - ) -> DispatchResult { - let source = ensure_signed(origin)?; - let dest = T::Lookup::lookup(dest)?; - - // Prevent self-transfers - ensure!(source != dest, Error::::SelfTransfer); - - // Perform the transfer - >::transfer(&source, &dest, amount, Preservation::Expendable)?; - - // Store proof with asset_id = Default (0 for native) - Self::record_transfer(AssetIdOf::::default(), source.into(), dest.into(), amount)?; - - Ok(()) - } - - /// Transfer asset tokens and store proof for wormhole - #[pallet::call_index(2)] - #[pallet::weight(T::DbWeight::get().reads_writes(2, 2))] - pub fn transfer_asset( - origin: OriginFor, - asset_id: AssetIdOf, - dest: AccountIdLookupOf, - #[pallet::compact] amount: AssetBalanceOf, - ) -> DispatchResult { - let source = ensure_signed(origin)?; - let dest = T::Lookup::lookup(dest)?; - - // Prevent self-transfers - ensure!(source != dest, Error::::SelfTransfer); - - // Check if asset exists - ensure!( - >::asset_exists(asset_id.clone()), - Error::::AssetNotFound - ); - - // Perform the transfer - >::transfer( - asset_id.clone(), - &source, - &dest, - amount, - Preservation::Expendable, - )?; - - // Store proof - Self::record_transfer(asset_id, source.into(), dest.into(), amount.into())?; - - Ok(()) - } - - /// Verify an aggregated wormhole proof and process all transfers in the batch - #[pallet::call_index(3)] - #[pallet::weight(::WeightInfo::verify_aggregated_proof())] - pub fn verify_aggregated_proof( - origin: OriginFor, - proof_bytes: Vec, - ) -> DispatchResult { - ensure_none(origin)?; - - let verifier = crate::get_aggregated_verifier() - .map_err(|_| Error::::AggregatedVerifierNotAvailable)?; - - let proof = ProofWithPublicInputs::::from_bytes( - proof_bytes, - &verifier.circuit_data.common, - ) - .map_err(|_| Error::::AggregatedProofDeserializationFailed)?; - - // Verify the aggregated proof - verifier - .verify(proof.clone()) - .map_err(|_| Error::::AggregatedVerificationFailed)?; - - // Parse aggregated public inputs - let aggregated_inputs = parse_aggregated_public_inputs(&proof) - .map_err(|_| Error::::InvalidAggregatedPublicInputs)?; - - // Verify all nullifiers haven't been used and then mark them as used - for nullifier in &aggregated_inputs.nullifiers { - let nullifier_bytes: [u8; 32] = (*nullifier) - .as_ref() - .try_into() - .map_err(|_| Error::::InvalidAggregatedPublicInputs)?; - ensure!( - !UsedNullifiers::::contains_key(nullifier_bytes), - Error::::NullifierAlreadyUsed - ); - UsedNullifiers::::insert(nullifier_bytes, true); - } - - // Convert block number from u32 to BlockNumberFor - let block_number = BlockNumberFor::::from(aggregated_inputs.block_data.block_number); - - // Check if block number is not in the future - let current_block = frame_system::Pallet::::block_number(); - // TODO: is this check necessary? - ensure!(block_number <= current_block, Error::::InvalidBlockNumber); - - // Get the block hash for the specified block number - let block_hash = frame_system::Pallet::::block_hash(block_number); - - // Validate that the block exists by checking if it's not the default hash - // The default hash (all zeros) indicates the block doesn't exist - // TODO: is this check necessary? - let default_hash = T::Hash::default(); - ensure!(block_hash != default_hash, Error::::BlockNotFound); - - // Ensure that the block hash from storage matches the one in public inputs - ensure!( - block_hash.as_ref() == aggregated_inputs.block_data.block_hash.as_ref(), - Error::::InvalidPublicInputs - ); - - // Extract asset_id from aggregated public inputs - let asset_id: AssetIdOf = aggregated_inputs.asset_id.into(); - - // Verify the volume fee rate matches our configured rate - ensure!( - aggregated_inputs.volume_fee_bps == T::VolumeFeeRateBps::get(), - Error::::InvalidVolumeFeeRate - ); - - // Get the minting account for recording transfer proofs - let mint_account = T::MintingAccount::get(); - - // First pass: compute total exit amount and prepare account data - let mut total_exit_amount: BalanceOf = Zero::zero(); - let mut processed_accounts: Vec<( - ::AccountId, - BalanceOf, - )> = Vec::with_capacity(aggregated_inputs.account_data.len()); - - for account_data in &aggregated_inputs.account_data { - // Convert output amount to Balance type (scale up from quantized value) - let exit_balance_u128 = (account_data.summed_output_amount as u128) - .saturating_mul(crate::SCALE_DOWN_FACTOR); - let exit_balance: BalanceOf = exit_balance_u128 - .try_into() - .map_err(|_| Error::::InvalidAggregatedPublicInputs)?; - - // Decode exit account from public inputs - let exit_account_bytes: [u8; 32] = (*account_data.exit_account) - .as_ref() - .try_into() - .map_err(|_| Error::::InvalidAggregatedPublicInputs)?; - let exit_account = - ::AccountId::decode(&mut &exit_account_bytes[..]) - .map_err(|_| Error::::InvalidAggregatedPublicInputs)?; - - total_exit_amount = total_exit_amount.saturating_add(exit_balance); - processed_accounts.push((exit_account, exit_balance)); - } - - // Check minimum against total aggregated amount - ensure!( - total_exit_amount >= T::MinimumTransferAmount::get(), - Error::::TransferAmountBelowMinimum - ); - - // Compute the total fee from the input amounts - // fee = total_output_amount * volume_fee_bps / (10000 - volume_fee_bps) - // This is the fee that was deducted from input to get output. - let fee_bps = T::VolumeFeeRateBps::get() as u128; - let total_exit_u128: u128 = total_exit_amount - .try_into() - .map_err(|_| Error::::InvalidAggregatedPublicInputs)?; - let total_fee_u128 = total_exit_u128 - .saturating_mul(fee_bps) - .checked_div(10000u128.saturating_sub(fee_bps)) - .unwrap_or(0); - - // Fee distribution: configurable portion burned, remainder to miner - // - // Original deposit locked `input_amount` in an unspendable account (tokens still - // exist). On exit we mint `output_amount` to user, where: input = output + fee - // - // Fee split (controlled by VolumeFeesBurnRate): - // - burn_amount = fee * burn_rate (reduces total issuance via Currency::burn) - // - miner_fee = fee - burn_amount (minted to block author via increase_balance) - // - // Supply accounting: - // - Minting exit amounts: increases total issuance by sum(output_amounts) - // - Minting miner fee: increases balance but NOT issuance (increase_balance) - // - Burning: decreases total issuance by burn_amount - // - Net change: +sum(output_amounts) - burn_amount - let burn_rate = T::VolumeFeesBurnRate::get(); - let burn_amount_u128 = burn_rate * total_fee_u128; - let miner_fee_u128 = total_fee_u128.saturating_sub(burn_amount_u128); - let miner_fee: BalanceOf = miner_fee_u128 - .try_into() - .map_err(|_| Error::::InvalidAggregatedPublicInputs)?; - let burn_amount: BalanceOf = burn_amount_u128 - .try_into() - .map_err(|_| Error::::InvalidAggregatedPublicInputs)?; - - // Burn the burned portion by reducing total issuance - // This offsets the supply increase from minting exit amounts + miner_fee - // The PositiveImbalance is dropped, which is a no-op (already reduced issuance) - if !burn_amount.is_zero() { - let _ = >::burn(burn_amount); - } - - // Second pass: process transfers and record proofs - for (exit_account, exit_balance) in &processed_accounts { - // Handle native (asset_id = 0) or asset transfers - if asset_id == AssetIdOf::::default() { - // Native token transfer - mint tokens to the exit account - >::increase_balance( - exit_account, - *exit_balance, - frame_support::traits::tokens::Precision::Exact, - )?; - } else { - // Asset transfer - let asset_balance: AssetBalanceOf = (*exit_balance).into(); - >::mint_into( - asset_id.clone(), - exit_account, - asset_balance, - )?; - } - - // Record transfer proof for the minted tokens - Self::record_transfer( - asset_id.clone(), - mint_account.clone().into(), - exit_account.clone().into(), - *exit_balance, - )?; - - // Emit event for each exit account - Self::deposit_event(Event::ProofVerified { exit_amount: *exit_balance }); - } - - // Mint miner's portion of volume fee to block author - // The remaining portion (fee - miner_fee) is not minted, effectively burned - if !miner_fee.is_zero() { - if let Some(author) = frame_system::Pallet::::digest() - .logs - .iter() - .find_map(|item| item.as_pre_runtime()) - .and_then(|(_, data)| { - ::AccountId::decode(&mut &data[..]).ok() - }) { - >::increase_balance( - &author, - miner_fee, - frame_support::traits::tokens::Precision::Exact, - )?; - } - } - - Ok(()) - } - } - - #[pallet::validate_unsigned] - impl ValidateUnsigned for Pallet - where - AssetIdOf: Default + From + Clone + ToFelts, - BalanceOf: Default + ToFelts, - AssetBalanceOf: Into> + From>, - { - type Call = Call; - - fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity { - match call { - Call::verify_wormhole_proof { proof_bytes } => { - // Basic validation: check proof bytes are not empty - if proof_bytes.is_empty() { - return InvalidTransaction::Custom(1).into(); - } - ValidTransaction::with_tag_prefix("WormholeVerify") - .and_provides(sp_io::hashing::blake2_256(proof_bytes)) - .priority(TransactionPriority::MAX) - .longevity(5) - .propagate(true) - .build() - }, - Call::verify_aggregated_proof { proof_bytes } => { - // Basic validation: check proof bytes are not empty - if proof_bytes.is_empty() { - return InvalidTransaction::Custom(2).into(); - } - ValidTransaction::with_tag_prefix("WormholeAggregatedVerify") - .and_provides(sp_io::hashing::blake2_256(proof_bytes)) - .priority(TransactionPriority::MAX) - .longevity(5) - .propagate(true) - .build() - }, - _ => InvalidTransaction::Call.into(), - } - } - } - - // Helper functions for recording transfer proofs - impl Pallet { - /// Record a transfer proof - /// This should be called by transaction extensions or other runtime components - pub fn record_transfer( - asset_id: AssetIdOf, - from: ::WormholeAccountId, - to: ::WormholeAccountId, - amount: BalanceOf, - ) -> DispatchResult { - let current_count = TransferCount::::get(&to); - TransferProof::::insert( - (asset_id.clone(), current_count, from.clone(), to.clone(), amount), - (), - ); - TransferCount::::insert(&to, current_count.saturating_add(T::TransferCount::one())); - - if asset_id == AssetIdOf::::default() { - Self::deposit_event(Event::::NativeTransferred { - from: from.into(), - to: to.into(), - amount, - transfer_count: current_count, - }); - } else { - Self::deposit_event(Event::::AssetTransferred { - from: from.into(), - to: to.into(), - asset_id, - amount: amount.into(), - transfer_count: current_count, - }); - } - - Ok(()) - } - } - - // Implement the TransferProofRecorder trait for other pallets to use - impl - qp_wormhole::TransferProofRecorder< - ::WormholeAccountId, - AssetIdOf, - BalanceOf, - > for Pallet - { - type Error = DispatchError; - - fn record_transfer_proof( - asset_id: Option>, - from: ::WormholeAccountId, - to: ::WormholeAccountId, - amount: BalanceOf, - ) -> Result<(), Self::Error> { - let asset_id_value = asset_id.unwrap_or_default(); - Self::record_transfer(asset_id_value, from, to, amount) - } } } diff --git a/pallets/wormhole/src/mock.rs b/pallets/wormhole/src/mock.rs index 52100336..574ac336 100644 --- a/pallets/wormhole/src/mock.rs +++ b/pallets/wormhole/src/mock.rs @@ -1,28 +1,27 @@ -use crate::{self as pallet_wormhole, SCALE_DOWN_FACTOR}; +use crate as pallet_wormhole; use frame_support::{ construct_runtime, parameter_types, - traits::{ConstU128, ConstU32, Everything}, + traits::{ConstU32, Everything}, + weights::IdentityFee, }; -use frame_system::mocking::MockUncheckedExtrinsic; -use qp_poseidon::PoseidonHasher; use sp_core::H256; -use sp_runtime::{traits::IdentityLookup, BuildStorage, Permill}; +use sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, +}; +// --- MOCK RUNTIME --- construct_runtime!( pub enum Test { System: frame_system, Balances: pallet_balances, - Assets: pallet_assets, Wormhole: pallet_wormhole, } ); pub type Balance = u128; pub type AccountId = sp_core::crypto::AccountId32; -pub type Block = sp_runtime::generic::Block< - qp_header::Header, - MockUncheckedExtrinsic, ->; +pub type Block = frame_system::mocking::MockBlock; /// Helper function to convert a u64 to an AccountId32 pub fn account_id(id: u64) -> AccountId { @@ -31,6 +30,8 @@ pub fn account_id(id: u64) -> AccountId { AccountId::new(bytes) } +// --- FRAME SYSTEM --- + parameter_types! { pub const BlockHashCount: u64 = 250; } @@ -45,10 +46,10 @@ impl frame_system::Config for Test { type RuntimeTask = (); type Nonce = u64; type Hash = H256; - type Hashing = PoseidonHasher; + type Hashing = BlakeTwo256; type AccountId = AccountId; type Lookup = IdentityLookup; - type Block = Block; + type Block = Block; type BlockHashCount = BlockHashCount; type DbWeight = (); type Version = (); @@ -68,6 +69,8 @@ impl frame_system::Config for Test { type PostTransactions = (); } +// --- PALLET BALANCES --- + parameter_types! { pub const ExistentialDeposit: Balance = 1; } @@ -86,59 +89,32 @@ impl pallet_balances::Config for Test { type MaxReserves = (); type MaxFreezes = (); type DoneSlashHandler = (); - type RuntimeEvent = RuntimeEvent; + type MintingAccount = MintingAccount; } -impl pallet_assets::Config for Test { - type RuntimeEvent = RuntimeEvent; - type Balance = Balance; - type AssetId = u32; - type AssetIdParameter = u32; - type Currency = Balances; - type CreateOrigin = - frame_support::traits::AsEnsureOriginWithArg>; - type ForceOrigin = frame_system::EnsureRoot; - type AssetDeposit = ConstU128<1>; - type AssetAccountDeposit = ConstU128<1>; - type MetadataDepositBase = ConstU128<1>; - type MetadataDepositPerByte = ConstU128<1>; - type ApprovalDeposit = ConstU128<1>; - type StringLimit = ConstU32<50>; - type Freezer = (); - type Extra = (); - type WeightInfo = (); - type RemoveItemsLimit = ConstU32<1000>; - type CallbackHandle = (); - type Holder = (); -} +// --- PALLET WORMHOLE --- parameter_types! { pub const MintingAccount: AccountId = AccountId::new([ 231, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]); - /// Minimum transfer amount: 1 token (100 quantized units × SCALE_DOWN_FACTOR) - pub const MinimumTransferAmount: Balance = 100 * SCALE_DOWN_FACTOR; - /// Volume fee rate in basis points (10 bps = 0.1%) - pub const VolumeFeeRateBps: u32 = 10; - /// Proportion of volume fees to burn (50% burned, 50% to miner) - pub const VolumeFeesBurnRate: Permill = Permill::from_percent(50); } impl pallet_wormhole::Config for Test { type WeightInfo = crate::weights::SubstrateWeight; + type WeightToFee = IdentityFee; type Currency = Balances; - type Assets = Assets; - type TransferCount = u64; type MintingAccount = MintingAccount; - type MinimumTransferAmount = MinimumTransferAmount; - type VolumeFeeRateBps = VolumeFeeRateBps; - type VolumeFeesBurnRate = VolumeFeesBurnRate; - type WormholeAccountId = AccountId; } // Helper function to build a genesis configuration -pub fn new_test_ext() -> sp_state_machine::TestExternalities { - let t = frame_system::GenesisConfig::::default().build_storage().unwrap(); +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + pallet_balances::GenesisConfig:: { balances: vec![] } + .assimilate_storage(&mut t) + .unwrap(); + t.into() } diff --git a/pallets/wormhole/src/tests.rs b/pallets/wormhole/src/tests.rs index 37a96598..a96fca74 100644 --- a/pallets/wormhole/src/tests.rs +++ b/pallets/wormhole/src/tests.rs @@ -1,142 +1,211 @@ #[cfg(test)] mod wormhole_tests { - use crate::mock::*; - use frame_support::{ - assert_ok, - traits::fungible::{Inspect, Mutate}, - }; + use crate::{get_wormhole_verifier, mock::*, weights, Config, Error, WeightInfo}; + use frame_support::{assert_noop, assert_ok, weights::WeightToFee}; + use qp_wormhole_circuit::inputs::PublicCircuitInputs; + use qp_wormhole_verifier::ProofWithPublicInputs; + use sp_runtime::Perbill; + + // Helper function to generate proof and inputs for + fn get_test_proof() -> Vec { + let hex_proof = include_str!("../proof_from_bins.hex"); + hex::decode(hex_proof.trim()).expect("Failed to decode hex proof") + } #[test] - fn transfer_native_works() { + fn test_verifier_availability() { new_test_ext().execute_with(|| { - let alice = account_id(1); - let bob = account_id(2); - let amount = 1000u128; + let verifier = get_wormhole_verifier(); + assert!(verifier.is_ok(), "Verifier should be available in tests"); - assert_ok!(Balances::mint_into(&alice, amount * 2)); + // Verify the verifier can be used + let verifier = verifier.unwrap(); + // Check that the circuit data is valid by checking gates + assert!(!verifier.circuit_data.common.gates.is_empty(), "Circuit should have gates"); + }); + } - let count_before = Wormhole::transfer_count(&bob); - assert_ok!(Wormhole::transfer_native( - frame_system::RawOrigin::Signed(alice.clone()).into(), - bob.clone(), - amount, - )); + #[test] + fn test_verify_empty_proof_fails() { + new_test_ext().execute_with(|| { + let empty_proof = vec![]; + let block_number = frame_system::Pallet::::block_number(); + assert_noop!( + Wormhole::verify_wormhole_proof(RuntimeOrigin::none(), empty_proof, block_number), + Error::::ProofDeserializationFailed + ); + }); + } - assert_eq!(Balances::balance(&alice), amount); - assert_eq!(Balances::balance(&bob), amount); - assert_eq!(Wormhole::transfer_count(&bob), count_before + 1); - assert!(Wormhole::transfer_proof((0u32, count_before, alice, bob, amount)).is_some()); + #[test] + fn test_verify_invalid_proof_data_fails() { + new_test_ext().execute_with(|| { + // Create some random bytes that will fail deserialization + let invalid_proof = vec![1u8; 100]; + let block_number = frame_system::Pallet::::block_number(); + assert_noop!( + Wormhole::verify_wormhole_proof(RuntimeOrigin::none(), invalid_proof, block_number), + Error::::ProofDeserializationFailed + ); }); } #[test] - fn transfer_native_fails_on_self_transfer() { + fn test_verify_valid_proof() { new_test_ext().execute_with(|| { - let alice = account_id(1); - let amount = 1000u128; + let proof = get_test_proof(); + let block_number = frame_system::Pallet::::block_number(); + assert_ok!(Wormhole::verify_wormhole_proof(RuntimeOrigin::none(), proof, block_number)); + }); + } - assert_ok!(Balances::mint_into(&alice, amount)); + #[test] + fn test_verify_invalid_inputs() { + new_test_ext().execute_with(|| { + let mut proof = get_test_proof(); + let block_number = frame_system::Pallet::::block_number(); - let result = Wormhole::transfer_native( - frame_system::RawOrigin::Signed(alice.clone()).into(), - alice.clone(), - amount, - ); + if let Some(byte) = proof.get_mut(0) { + *byte = !*byte; // Flip bits to make proof invalid + } - assert!(result.is_err()); + assert_noop!( + Wormhole::verify_wormhole_proof(RuntimeOrigin::none(), proof, block_number), + Error::::VerificationFailed + ); }); } #[test] - fn transfer_asset_works() { + fn test_wormhole_exit_balance_and_fees() { new_test_ext().execute_with(|| { - let alice = account_id(1); - let bob = account_id(2); - let asset_id = 1u32; - let amount = 1000u128; - - assert_ok!(Balances::mint_into(&alice, 1000)); - assert_ok!(Balances::mint_into(&bob, 1000)); - - assert_ok!(Assets::create( - frame_system::RawOrigin::Signed(alice.clone()).into(), - asset_id.into(), - alice.clone(), - 1, - )); - assert_ok!(Assets::mint( - frame_system::RawOrigin::Signed(alice.clone()).into(), - asset_id.into(), - alice.clone(), - amount * 2, - )); + let proof = get_test_proof(); + let expected_exit_account = account_id(8226349481601990196u64); + + // Parse the proof to get expected funding amount + let verifier = get_wormhole_verifier().expect("Verifier should be available"); + let proof_with_inputs = ProofWithPublicInputs::from_bytes(proof.clone(), &verifier.circuit_data.common) + .expect("Should be able to parse test proof"); + + let public_inputs = PublicCircuitInputs::try_from(&proof_with_inputs) + .expect("Should be able to parse public inputs"); + + let expected_funding_amount = public_inputs.funding_amount; + + // Calculate expected fees (matching lib.rs logic exactly) + let weight = as WeightInfo>::verify_wormhole_proof(); + let weight_fee: u128 = ::WeightToFee::weight_to_fee(&weight); + let volume_fee = Perbill::from_rational(1u32, 1000u32) * expected_funding_amount; + let expected_total_fee = weight_fee.saturating_add(volume_fee); + let expected_net_balance_increase = expected_funding_amount.saturating_sub(expected_total_fee); + + let initial_exit_balance = + pallet_balances::Pallet::::free_balance(&expected_exit_account); + + let block_number = frame_system::Pallet::::block_number(); + let result = + Wormhole::verify_wormhole_proof(RuntimeOrigin::none(), proof, block_number); + assert_ok!(result); + + let final_exit_balance = + pallet_balances::Pallet::::free_balance(&expected_exit_account); + + let balance_increase = final_exit_balance - initial_exit_balance; + + // Assert the exact expected balance increase + assert_eq!( + balance_increase + , expected_net_balance_increase, + "Balance increase should equal funding amount minus fees. Funding: {}, Fees: {}, Expected net: {}, Actual: {}" + , expected_funding_amount + , expected_total_fee + , expected_net_balance_increase + , balance_increase + ); + + // NOTE: In this mock/test context, the OnUnbalanced handler is not triggered for this withdrawal. + // In production, the fee will be routed to the handler as expected. + }); + } - let count_before = Wormhole::transfer_count(&bob); - assert_ok!(Wormhole::transfer_asset( - frame_system::RawOrigin::Signed(alice.clone()).into(), - asset_id, - bob.clone(), - amount, + #[test] + fn test_nullifier_already_used() { + new_test_ext().execute_with(|| { + let proof = get_test_proof(); + let block_number = frame_system::Pallet::::block_number(); + + // First verification should succeed + assert_ok!(Wormhole::verify_wormhole_proof( + RuntimeOrigin::none(), + proof.clone(), + block_number )); - assert_eq!(Assets::balance(asset_id, &alice), amount); - assert_eq!(Assets::balance(asset_id, &bob), amount); - assert_eq!(Wormhole::transfer_count(&bob), count_before + 1); - assert!( - Wormhole::transfer_proof((asset_id, count_before, alice, bob, amount)).is_some() + // Second verification with same proof should fail due to nullifier reuse + assert_noop!( + Wormhole::verify_wormhole_proof(RuntimeOrigin::none(), proof, block_number), + Error::::NullifierAlreadyUsed ); }); } #[test] - fn transfer_asset_fails_on_nonexistent_asset() { + fn test_verify_future_block_number_fails() { new_test_ext().execute_with(|| { - let alice = account_id(1); - let bob = account_id(2); - let asset_id = 999u32; - let amount = 1000u128; - - let result = Wormhole::transfer_asset( - frame_system::RawOrigin::Signed(alice.clone()).into(), - asset_id, - bob.clone(), - amount, - ); + let proof = get_test_proof(); + let current_block = frame_system::Pallet::::block_number(); + let future_block = current_block + 1; - assert!(result.is_err()); + assert_noop!( + Wormhole::verify_wormhole_proof(RuntimeOrigin::none(), proof, future_block), + Error::::InvalidBlockNumber + ); }); } #[test] - fn transfer_asset_fails_on_self_transfer() { + fn test_verify_storage_root_mismatch_fails() { new_test_ext().execute_with(|| { - let alice = account_id(1); - let asset_id = 1u32; - let amount = 1000u128; - - assert_ok!(Balances::mint_into(&alice, 1000)); + // This test would require a proof with a different root_hash than the current storage + // root + let proof = get_test_proof(); + let block_number = frame_system::Pallet::::block_number(); + + let result = + Wormhole::verify_wormhole_proof(RuntimeOrigin::none(), proof, block_number); + + // This should either succeed (if root_hash matches) or fail with StorageRootMismatch + // We can't easily create a proof with wrong root_hash in tests, so we just verify + // that the validation logic is executed + assert!(result.is_ok() || result.is_err()); + }); + } - assert_ok!(Assets::create( - frame_system::RawOrigin::Signed(alice.clone()).into(), - asset_id.into(), - alice.clone(), - 1, - )); - assert_ok!(Assets::mint( - frame_system::RawOrigin::Signed(alice.clone()).into(), - asset_id.into(), - alice.clone(), - amount, + #[test] + fn test_verify_with_different_block_numbers() { + new_test_ext().execute_with(|| { + let proof = get_test_proof(); + let current_block = frame_system::Pallet::::block_number(); + + // Test with current block (should succeed) + assert_ok!(Wormhole::verify_wormhole_proof( + RuntimeOrigin::none(), + proof.clone(), + current_block )); - let result = Wormhole::transfer_asset( - frame_system::RawOrigin::Signed(alice.clone()).into(), - asset_id, - alice.clone(), - amount, - ); - - assert!(result.is_err()); + // Test with a recent block (should succeed if it exists) + if current_block > 1 { + let recent_block = current_block - 1; + let result = Wormhole::verify_wormhole_proof( + RuntimeOrigin::none(), + proof.clone(), + recent_block, + ); + // This might succeed or fail depending on whether the block exists + // and whether the storage root matches + assert!(result.is_ok() || result.is_err()); + } }); } } diff --git a/pallets/wormhole/src/weights.rs b/pallets/wormhole/src/weights.rs index 7cc49ac4..b1516fa8 100644 --- a/pallets/wormhole/src/weights.rs +++ b/pallets/wormhole/src/weights.rs @@ -18,10 +18,10 @@ //! Autogenerated weights for `pallet_wormhole` //! -//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 49.1.0 -//! DATE: 2025-11-27, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 46.2.0 +//! DATE: 2025-07-22, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `Dastans-MacBook-Pro.local`, CPU: `` +//! HOSTNAME: `Veronikas-MacBook-Air.local`, CPU: `` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` // Executed Command: @@ -53,7 +53,6 @@ use core::marker::PhantomData; /// Weight functions needed for `pallet_wormhole`. pub trait WeightInfo { fn verify_wormhole_proof() -> Weight; - fn verify_aggregated_proof() -> Weight; } /// Weights for `pallet_wormhole` using the Substrate node and recommended hardware. @@ -71,25 +70,13 @@ impl WeightInfo for SubstrateWeight { /// Proof: `Balances::TransferProof` (`max_values`: None, `max_size`: Some(32), added: 2507, mode: `MaxEncodedLen`) fn verify_wormhole_proof() -> Weight { // Proof Size summary in bytes: - // Measured: `256` + // Measured: `424` // Estimated: `3593` - // Minimum execution time: 14_318_000_000 picoseconds. - Weight::from_parts(14_468_000_000, 3593) + // Minimum execution time: 17_162_000_000 picoseconds. + Weight::from_parts(17_576_000_000, 3593) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } - - /// Weight for verifying an aggregated proof. - /// This is a placeholder - proper benchmarking should be done. - /// Aggregated proofs verify multiple transfers at once, so the weight - /// should be higher than single proof verification. - fn verify_aggregated_proof() -> Weight { - // Placeholder: use 8x the single proof weight as a conservative estimate - // for the default tree configuration (branching_factor=2, depth=3 = 8 proofs) - Weight::from_parts(115_744_000_000, 28744) - .saturating_add(T::DbWeight::get().reads(32_u64)) - .saturating_add(T::DbWeight::get().writes(32_u64)) - } } // For backwards compatibility and tests. @@ -106,20 +93,11 @@ impl WeightInfo for () { /// Proof: `Balances::TransferProof` (`max_values`: None, `max_size`: Some(32), added: 2507, mode: `MaxEncodedLen`) fn verify_wormhole_proof() -> Weight { // Proof Size summary in bytes: - // Measured: `256` + // Measured: `424` // Estimated: `3593` - // Minimum execution time: 14_318_000_000 picoseconds. - Weight::from_parts(14_468_000_000, 3593) + // Minimum execution time: 17_162_000_000 picoseconds. + Weight::from_parts(17_576_000_000, 3593) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } - - /// Weight for verifying an aggregated proof. - /// This is a placeholder - proper benchmarking should be done. - fn verify_aggregated_proof() -> Weight { - // Placeholder: use 8x the single proof weight as a conservative estimate - Weight::from_parts(115_744_000_000, 28744) - .saturating_add(RocksDbWeight::get().reads(32_u64)) - .saturating_add(RocksDbWeight::get().writes(32_u64)) - } } diff --git a/pallets/wormhole/verifier.bin b/pallets/wormhole/verifier.bin index 25e4a4441f28ed6774056c0f4ae846d5df260707..6f7404b667329a8017f4474738e1864f35740b79 100644 GIT binary patch literal 552 zcmV+@0@wWn000000002bB((3dmQ#8Pd=u(5Q_MO*fJ;f8QBx9Z942FUwJ!)ZY!Ao9 z8>68O>uJ)jvp@aiEmphF1 zt=EwEV3`!C%DOHzBcO`Nn}z?iC(n1J%E~RK{>@7be>23SGfkcp+}r6(aMku|TbmAU zXrvkRZ{VGl_&}lwx_azzDm4Q*Gdc-%leoNxAW=R=MBgK|-WN=A{TuL#eQa`NHwWXG zl?|0r;!mqt+Z>~oULuS3xo$NuM1my_ktngy@30kh)^C;PHLCwM6LD`H#8}24RErRi zenXR9((a41u@*5HO9_1Oa?B~~$@I(eA820>@lPW(Kqgl$_O z8ZgGiQ^L7iT)4ai5L7ua~{&D*+DfTc5EAyADHBW`zC`Z&G<3L;3+W+ ztfy#C=~AZ_jGRSU#(7p{k{q-A;z0H12G=@*vtvHf49ARUh_WGBEmm2P8 qXKRDCF!y7cTf!)t9w)1yPwkj)P{8vPStKjYr_ZO+O{pLv{XmN~s0O6~ literal 552 zcmV+@0@wWn000000001$vILPFm28;MFUWyYo>&T=D4{o_FU9JpsfgBfZ9%z*)?U8B z7*4ngS~a=VTJ$D0|4;YWJ#1{QVC*eh)=|N?I=^DQY%D4JdYSxjd_9Q-!{d5I`>eq?DSl8LRMEjs*=$slaI}V1ykSF4rapJGmKeo_KWpTj^o+~7 z=$tV>qQZ}}v@?@)?ayiDwS4`;@$|vFkXRY?zSYbTGSElIHNb)pL66C2(cF5|{ q1i%`nK|h>eWyz^LHr`=8Lx%^yx;I>YjF#a_Az0&~KI2zHbtJzcUJXM4 diff --git a/primitives/wormhole/src/lib.rs b/primitives/wormhole/src/lib.rs index 8bc383eb..ac25ce16 100644 --- a/primitives/wormhole/src/lib.rs +++ b/primitives/wormhole/src/lib.rs @@ -3,19 +3,26 @@ extern crate alloc; -/// Trait for recording transfer proofs in the wormhole pallet. -/// Other pallets can use this to record proofs when they mint/transfer tokens. -pub trait TransferProofRecorder { - /// Error type for proof recording failures - type Error; +use alloc::vec::Vec; - /// Record a transfer proof for native or asset tokens - /// - `None` for native tokens (asset_id = 0) - /// - `Some(asset_id)` for specific assets - fn record_transfer_proof( - asset_id: Option, +/// Trait for managing wormhole transfer proofs. +pub trait TransferProofs { + /// Get transfer proof, if any + fn transfer_proof_exists( + count: TxCount, + from: &AccountId, + to: &AccountId, + value: Balance, + ) -> bool; + + /// Get transfer proof key + fn transfer_proof_key( + count: TxCount, from: AccountId, to: AccountId, - amount: Balance, - ) -> Result<(), Self::Error>; + value: Balance, + ) -> Vec; + + /// Store transfer proofs for a given wormhole transfer. + fn store_transfer_proof(from: &AccountId, to: &AccountId, value: Balance); } diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index f1ba2bba..fe292af7 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -46,14 +46,11 @@ pallet-transaction-payment.workspace = true pallet-transaction-payment-rpc-runtime-api.workspace = true pallet-treasury.workspace = true pallet-utility.workspace = true -pallet-wormhole.workspace = true primitive-types.workspace = true qp-dilithium-crypto.workspace = true qp-header = { workspace = true, features = ["serde"] } qp-poseidon = { workspace = true, features = ["serde"] } qp-scheduler.workspace = true -qp-wormhole.workspace = true -qp-wormhole-verifier = { workspace = true, default-features = false } scale-info = { features = ["derive", "serde"], workspace = true } serde_json = { workspace = true, default-features = false, features = [ "alloc", @@ -113,7 +110,6 @@ std = [ "pallet-transaction-payment/std", "pallet-treasury/std", "pallet-utility/std", - "pallet-wormhole/std", "primitive-types/std", "qp-dilithium-crypto/full_crypto", "qp-dilithium-crypto/std", @@ -160,7 +156,6 @@ runtime-benchmarks = [ "pallet-timestamp/runtime-benchmarks", "pallet-transaction-payment/runtime-benchmarks", "pallet-treasury/runtime-benchmarks", - "pallet-wormhole/runtime-benchmarks", "sp-runtime/runtime-benchmarks", ] @@ -179,7 +174,6 @@ try-runtime = [ "pallet-timestamp/try-runtime", "pallet-transaction-payment/try-runtime", "pallet-treasury/try-runtime", - "pallet-wormhole/try-runtime", "sp-runtime/try-runtime", ] diff --git a/runtime/src/benchmarks.rs b/runtime/src/benchmarks.rs index f48bdacb..cf13e3e1 100644 --- a/runtime/src/benchmarks.rs +++ b/runtime/src/benchmarks.rs @@ -34,5 +34,4 @@ frame_benchmarking::define_benchmarks!( [pallet_multisig, Multisig] [pallet_scheduler, Scheduler] [pallet_qpow, QPoW] - [pallet_wormhole, Wormhole] ); diff --git a/runtime/src/configs/mod.rs b/runtime/src/configs/mod.rs index ef8621fe..a439621c 100644 --- a/runtime/src/configs/mod.rs +++ b/runtime/src/configs/mod.rs @@ -55,17 +55,14 @@ use pallet_ranked_collective::Linear; use pallet_transaction_payment::{ConstFeeMultiplier, FungibleAdapter, Multiplier}; use qp_poseidon::PoseidonHasher; use qp_scheduler::BlockNumberOrTimestamp; -use sp_runtime::{ - traits::{AccountIdConversion, One}, - AccountId32, FixedU128, Perbill, Permill, -}; +use sp_runtime::{traits::One, FixedU128, Perbill, Permill}; use sp_version::RuntimeVersion; // Local module imports use super::{ - AccountId, Assets, Balance, Balances, Block, BlockNumber, Hash, Nonce, OriginCaller, - PalletInfo, Preimage, Referenda, Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, - RuntimeHoldReason, RuntimeOrigin, RuntimeTask, Scheduler, System, Timestamp, Wormhole, DAYS, + AccountId, Balance, Balances, Block, BlockNumber, Hash, Nonce, OriginCaller, PalletInfo, + Preimage, Referenda, Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, + RuntimeHoldReason, RuntimeOrigin, RuntimeTask, Scheduler, System, Timestamp, DAYS, EXISTENTIAL_DEPOSIT, MICRO_UNIT, TARGET_BLOCK_TIME_MS, UNIT, VERSION, }; use sp_core::U512; @@ -126,22 +123,14 @@ parameter_types! { pub const DefaultMintAmount: Balance = 10 * UNIT; } -parameter_types! { - pub const TreasuryPortion: Permill = Permill::from_percent(50); - pub const MiningUnit: Balance = UNIT; -} - impl pallet_mining_rewards::Config for Runtime { type Currency = Balances; - type AssetId = AssetId; - type ProofRecorder = Wormhole; type WeightInfo = pallet_mining_rewards::weights::SubstrateWeight; type MaxSupply = ConstU128<{ 21_000_000 * UNIT }>; // 21 million tokens type EmissionDivisor = ConstU128<26_280_000>; // Divide remaining supply by this amount - type TreasuryPortion = TreasuryPortion; + type TreasuryPortion = ConstU8<50>; // % of rewards go to treasury type TreasuryPalletId = TreasuryPalletId; type MintingAccount = MintingAccount; - type Unit = MiningUnit; } parameter_types! { @@ -188,7 +177,6 @@ parameter_types! { } impl pallet_balances::Config for Runtime { - type RuntimeEvent = RuntimeEvent; type RuntimeHoldReason = RuntimeHoldReason; type RuntimeFreezeReason = RuntimeFreezeReason; type WeightInfo = pallet_balances::weights::SubstrateWeight; @@ -203,6 +191,7 @@ impl pallet_balances::Config for Runtime { type MaxReserves = (); type MaxFreezes = VariantCountOf; type DoneSlashHandler = (); + type MintingAccount = MintingAccount; } parameter_types! { @@ -624,25 +613,3 @@ impl TryFrom for pallet_assets::Call { } } } - -parameter_types! { - pub WormholeMintingAccount: AccountId = PalletId(*b"wormhole").into_account_truncating(); - /// Minimum transfer amount: 1 token (100 quantized units × SCALE_DOWN_FACTOR) - pub const MinimumTransferAmount: Balance = 100 * pallet_wormhole::SCALE_DOWN_FACTOR; - /// Volume fee rate in basis points (10 bps = 0.1%) - pub const VolumeFeeRateBps: u32 = 10; - /// Proportion of volume fees to burn (50% burned, 50% to miner) - pub const VolumeFeesBurnRate: Permill = Permill::from_percent(50); -} - -impl pallet_wormhole::Config for Runtime { - type MintingAccount = WormholeMintingAccount; - type MinimumTransferAmount = MinimumTransferAmount; - type VolumeFeeRateBps = VolumeFeeRateBps; - type VolumeFeesBurnRate = VolumeFeesBurnRate; - type WeightInfo = (); - type Currency = Balances; - type Assets = Assets; - type TransferCount = u64; - type WormholeAccountId = AccountId32; -} diff --git a/runtime/src/genesis_config_presets.rs b/runtime/src/genesis_config_presets.rs index 5a4783ae..ff91a2cd 100644 --- a/runtime/src/genesis_config_presets.rs +++ b/runtime/src/genesis_config_presets.rs @@ -19,15 +19,14 @@ #![allow(clippy::expect_used)] use crate::{ - configs::TreasuryPalletId, AccountId, AssetsConfig, BalancesConfig, RuntimeGenesisConfig, - SudoConfig, EXISTENTIAL_DEPOSIT, UNIT, + configs::TreasuryPalletId, AccountId, BalancesConfig, RuntimeGenesisConfig, SudoConfig, UNIT, }; use alloc::{vec, vec::Vec}; use qp_dilithium_crypto::pair::{crystal_alice, crystal_charlie, dilithium_bob}; use serde_json::Value; use sp_core::crypto::Ss58Codec; use sp_genesis_builder::{self, PresetId}; -use sp_runtime::traits::{AccountIdConversion, IdentifyAccount, Zero}; +use sp_runtime::traits::{AccountIdConversion, IdentifyAccount}; /// Identifier for the heisenberg runtime preset. pub const HEISENBERG_RUNTIME_PRESET: &str = "heisenberg"; @@ -66,16 +65,8 @@ fn genesis_template(endowed_accounts: Vec, root: AccountId) -> Value balances.push((treasury_account, INITIAL_TREASURY)); let config = RuntimeGenesisConfig { - balances: BalancesConfig { balances, dev_accounts: None }, + balances: BalancesConfig { balances }, sudo: SudoConfig { key: Some(root.clone()) }, - assets: AssetsConfig { - // We need to initialize and reserve the first asset id for the native token transfers - // with wormhole. - assets: vec![(Zero::zero(), root.clone(), false, EXISTENTIAL_DEPOSIT)], /* (asset_id, - * owner, is_sufficient, - * min_balance) */ - ..Default::default() - }, ..Default::default() }; diff --git a/runtime/src/governance/definitions.rs b/runtime/src/governance/definitions.rs index 7c3c9196..98db0f49 100644 --- a/runtime/src/governance/definitions.rs +++ b/runtime/src/governance/definitions.rs @@ -482,13 +482,12 @@ where let pallets_origin = o.into_caller(); match pallets_origin { - crate::OriginCaller::system(frame_system::RawOrigin::Signed(who)) => { + crate::OriginCaller::system(frame_system::RawOrigin::Signed(who)) => if pallet_ranked_collective::Members::::contains_key(&who) { Ok(0) } else { Err(original_o_for_error) - } - }, + }, _ => Err(original_o_for_error), } } @@ -532,13 +531,12 @@ where let pallets_origin = o.into_caller(); match pallets_origin { - crate::OriginCaller::system(frame_system::RawOrigin::Signed(who)) => { + crate::OriginCaller::system(frame_system::RawOrigin::Signed(who)) => if pallet_ranked_collective::Members::::contains_key(&who) { Ok(who) } else { Err(original_o_for_error) - } - }, + }, _ => Err(original_o_for_error), } } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 82f532bf..d1ef9c8f 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -158,7 +158,6 @@ pub type TxExtension = ( pallet_transaction_payment::ChargeTransactionPayment, frame_metadata_hash_extension::CheckMetadataHash, transaction_extensions::ReversibleTransactionExtension, - transaction_extensions::WormholeProofRecorderExtension, ); /// Unchecked extrinsic type as expected by this runtime. @@ -257,7 +256,4 @@ mod runtime { #[runtime::pallet_index(23)] pub type Multisig = pallet_multisig; - - #[runtime::pallet_index(24)] - pub type Wormhole = pallet_wormhole; } diff --git a/runtime/src/transaction_extensions.rs b/runtime/src/transaction_extensions.rs index 92114394..5afed1e6 100644 --- a/runtime/src/transaction_extensions.rs +++ b/runtime/src/transaction_extensions.rs @@ -2,17 +2,12 @@ use crate::*; use codec::{Decode, DecodeWithMemTracking, Encode}; use core::marker::PhantomData; -use frame_support::pallet_prelude::{ - InvalidTransaction, TransactionValidityError, ValidTransaction, -}; +use frame_support::pallet_prelude::{InvalidTransaction, ValidTransaction}; + use frame_system::ensure_signed; -use qp_wormhole::TransferProofRecorder; use scale_info::TypeInfo; use sp_core::Get; -use sp_runtime::{ - traits::{DispatchInfoOf, PostDispatchInfoOf, StaticLookup, TransactionExtension}, - DispatchResult, Weight, -}; +use sp_runtime::{traits::TransactionExtension, Weight}; /// Transaction extension for reversible accounts /// @@ -50,7 +45,7 @@ impl _call: &RuntimeCall, _info: &sp_runtime::traits::DispatchInfoOf, _len: usize, - ) -> Result { + ) -> Result { Ok(()) } @@ -64,25 +59,30 @@ impl _inherited_implication: &impl sp_runtime::traits::Implication, _source: frame_support::pallet_prelude::TransactionSource, ) -> sp_runtime::traits::ValidateResult { - let who = ensure_signed(origin.clone()) - .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::BadSigner))?; + let who = ensure_signed(origin.clone()).map_err(|_| { + frame_support::pallet_prelude::TransactionValidityError::Invalid( + InvalidTransaction::BadSigner, + ) + })?; if ReversibleTransfers::is_high_security(&who).is_some() { // High-security accounts can only call schedule_transfer and cancel match call { RuntimeCall::ReversibleTransfers( pallet_reversible_transfers::Call::schedule_transfer { .. }, - ) - | RuntimeCall::ReversibleTransfers( + ) | + RuntimeCall::ReversibleTransfers( pallet_reversible_transfers::Call::schedule_asset_transfer { .. }, - ) - | RuntimeCall::ReversibleTransfers(pallet_reversible_transfers::Call::cancel { + ) | + RuntimeCall::ReversibleTransfers(pallet_reversible_transfers::Call::cancel { .. }) => { return Ok((ValidTransaction::default(), (), origin)); }, _ => { - return Err(TransactionValidityError::Invalid(InvalidTransaction::Custom(1))); + return Err(frame_support::pallet_prelude::TransactionValidityError::Invalid( + InvalidTransaction::Custom(1), + )); }, } } @@ -91,165 +91,6 @@ impl } } -/// Details of a transfer to be recorded -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct TransferDetails { - from: AccountId, - to: AccountId, - amount: Balance, - asset_id: AssetId, -} - -/// Transaction extension that records transfer proofs in the wormhole pallet -/// -/// This extension: -/// - Extracts transfer details from balance/asset transfer calls -/// - Records proofs in wormhole storage after successful execution -/// - Increments transfer count -/// - Emits events -/// - Fails the transaction if proof recording fails -#[derive(Encode, Decode, Clone, Eq, PartialEq, Default, TypeInfo, Debug, DecodeWithMemTracking)] -#[scale_info(skip_type_params(T))] -pub struct WormholeProofRecorderExtension(PhantomData); - -impl WormholeProofRecorderExtension { - /// Creates new extension - pub fn new() -> Self { - Self(PhantomData) - } - - /// Helper to convert lookup errors to transaction validity errors - fn lookup(address: &Address) -> Result { - ::Lookup::lookup(address.clone()) - .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::BadSigner)) - } - - /// Extract transfer details from a runtime call - fn extract_transfer_details( - origin: &RuntimeOrigin, - call: &RuntimeCall, - ) -> Result, TransactionValidityError> { - // Only process signed transactions - let who = match ensure_signed(origin.clone()) { - Ok(signer) => signer, - Err(_) => return Ok(None), - }; - - let details = match call { - // Native balance transfers - RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive { dest, value }) => { - let to = Self::lookup(dest)?; - Some(TransferDetails { from: who, to, amount: *value, asset_id: 0 }) - }, - RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death { dest, value }) => { - let to = Self::lookup(dest)?; - Some(TransferDetails { from: who, to, amount: *value, asset_id: 0 }) - }, - RuntimeCall::Balances(pallet_balances::Call::transfer_all { .. }) => None, - - // Asset transfers - RuntimeCall::Assets(pallet_assets::Call::transfer { id, target, amount }) => { - let to = Self::lookup(target)?; - Some(TransferDetails { asset_id: id.0, from: who, to, amount: *amount }) - }, - RuntimeCall::Assets(pallet_assets::Call::transfer_keep_alive { - id, - target, - amount, - }) => { - let to = Self::lookup(target)?; - Some(TransferDetails { asset_id: id.0, from: who, to, amount: *amount }) - }, - - _ => None, - }; - - Ok(details) - } - - /// Record the transfer proof using the TransferProofRecorder trait - fn record_proof(details: TransferDetails) -> Result<(), TransactionValidityError> { - let asset_id = if details.asset_id == 0 { None } else { Some(details.asset_id) }; - - >::record_transfer_proof( - asset_id, - details.from, - details.to, - details.amount, - ) - .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Custom(100))) - } -} - -impl TransactionExtension - for WormholeProofRecorderExtension -{ - type Pre = Option; - type Val = (); - type Implicit = (); - - const IDENTIFIER: &'static str = "WormholeProofRecorderExtension"; - - fn weight(&self, call: &RuntimeCall) -> Weight { - // Account for proof recording in post_dispatch - match call { - RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive { .. }) - | RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death { .. }) - | RuntimeCall::Assets(pallet_assets::Call::transfer { .. }) - | RuntimeCall::Assets(pallet_assets::Call::transfer_keep_alive { .. }) => { - // 2 writes: TransferProof insert + TransferCount update - // 1 read: TransferCount get - T::DbWeight::get().reads_writes(1, 2) - }, - _ => Weight::zero(), - } - } - - fn prepare( - self, - _val: Self::Val, - origin: &sp_runtime::traits::DispatchOriginOf, - call: &RuntimeCall, - _info: &sp_runtime::traits::DispatchInfoOf, - _len: usize, - ) -> Result { - // Extract transfer details to pass to post_dispatch - Self::extract_transfer_details(origin, call) - } - - fn validate( - &self, - _origin: sp_runtime::traits::DispatchOriginOf, - _call: &RuntimeCall, - _info: &DispatchInfoOf, - _len: usize, - _self_implicit: Self::Implicit, - _inherited_implication: &impl sp_runtime::traits::Implication, - _source: frame_support::pallet_prelude::TransactionSource, - ) -> sp_runtime::traits::ValidateResult { - // No validation needed - just return Ok - Ok((ValidTransaction::default(), (), _origin)) - } - - fn post_dispatch( - pre: Self::Pre, - _info: &DispatchInfoOf, - post_info: &mut PostDispatchInfoOf, - _len: usize, - _result: &DispatchResult, - ) -> Result<(), TransactionValidityError> { - // Only record proof if the transaction succeeded (no error in post_info) - if post_info.actual_weight.is_some() || _result.is_ok() { - if let Some(details) = pre { - // Record the proof - if this fails, fail the whole transaction - Self::record_proof(details)?; - } - } - - Ok(()) - } -} - #[cfg(test)] mod tests { use super::*; @@ -276,7 +117,6 @@ mod tests { (bob(), EXISTENTIAL_DEPOSIT * 2), (charlie(), EXISTENTIAL_DEPOSIT * 100), ], - dev_accounts: None, } .assimilate_storage(&mut t) .unwrap(); @@ -486,73 +326,8 @@ mod tests { RuntimeCall::ReversibleTransfers(pallet_reversible_transfers::Call::cancel { tx_id: sp_core::H256::default(), }); + // High-security accounts can call cancel assert_ok!(check_call(call)); }); } - - #[test] - fn wormhole_proof_recorder_native_transfer() { - new_test_ext().execute_with(|| { - let alice_origin = RuntimeOrigin::signed(alice()); - let call = RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive { - dest: MultiAddress::Id(bob()), - value: 100 * UNIT, - }); - - let details = WormholeProofRecorderExtension::::extract_transfer_details( - &alice_origin, - &call, - ) - .unwrap(); - - assert!(details.is_some()); - let details = details.unwrap(); - assert_eq!(details.from, alice()); - assert_eq!(details.to, bob()); - assert_eq!(details.amount, 100 * UNIT); - assert_eq!(details.asset_id, 0); - }); - } - - #[test] - fn wormhole_proof_recorder_asset_transfer() { - new_test_ext().execute_with(|| { - let alice_origin = RuntimeOrigin::signed(alice()); - let asset_id = 42u32; - let call = RuntimeCall::Assets(pallet_assets::Call::transfer { - id: codec::Compact(asset_id), - target: MultiAddress::Id(bob()), - amount: 500, - }); - - let details = WormholeProofRecorderExtension::::extract_transfer_details( - &alice_origin, - &call, - ) - .unwrap(); - - assert!(details.is_some()); - let details = details.unwrap(); - assert_eq!(details.from, alice()); - assert_eq!(details.to, bob()); - assert_eq!(details.amount, 500); - assert_eq!(details.asset_id, asset_id); - }); - } - - #[test] - fn wormhole_proof_recorder_ignores_non_transfer() { - new_test_ext().execute_with(|| { - let alice_origin = RuntimeOrigin::signed(alice()); - let call = RuntimeCall::System(frame_system::Call::remark { remark: vec![1, 2, 3] }); - - let details = WormholeProofRecorderExtension::::extract_transfer_details( - &alice_origin, - &call, - ) - .unwrap(); - - assert!(details.is_none()); - }); - } } diff --git a/runtime/tests/governance/treasury.rs b/runtime/tests/governance/treasury.rs index 3218957a..3b4fe933 100644 --- a/runtime/tests/governance/treasury.rs +++ b/runtime/tests/governance/treasury.rs @@ -88,12 +88,9 @@ mod tests { pub fn build(self) -> sp_io::TestExternalities { let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); - pallet_balances::GenesisConfig:: { - balances: self.balances, - dev_accounts: None, - } - .assimilate_storage(&mut t) - .unwrap(); + pallet_balances::GenesisConfig:: { balances: self.balances } + .assimilate_storage(&mut t) + .unwrap(); // Pallet Treasury genesis (optional, as we fund it manually) // If your pallet_treasury::GenesisConfig needs setup, do it here.