From 456c3dbc76e9a3447d6ad66d48a29b8b9aa23749 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Sun, 5 Jul 2026 11:24:46 -0400 Subject: [PATCH 01/18] test(shielded-pool): harden pool-balance ledger invariant --- frame/shielded-pool/CHANGELOG.md | 12 ++ frame/shielded-pool/src/lib.rs | 19 ++ frame/shielded-pool/src/operations/fees.rs | 43 +++++ .../src/operations/private_transfer.rs | 48 +++++ .../shielded-pool/src/operations/unshield.rs | 175 ++++++++++++++++++ frame/shielded-pool/src/storage.rs | 23 ++- 6 files changed, 318 insertions(+), 2 deletions(-) diff --git a/frame/shielded-pool/CHANGELOG.md b/frame/shielded-pool/CHANGELOG.md index e95ee271..a4202c0e 100644 --- a/frame/shielded-pool/CHANGELOG.md +++ b/frame/shielded-pool/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to `pallet-shielded-pool` will be documented in this file. +## [Unreleased] + +### Security + +- Hardened the pool-balance ledger invariant (`PoolBalancePerAsset == physical + pool balance` for the native asset). The accounting was already correct; added + a `try_state` hook (feature `try-runtime`) that enforces it every block, a + `defensive_assert!` before the `saturating_sub` in `PoolBalanceRepository`, and + tests anchoring the invariant across the full fee lifecycle (shield → unshield + with fee → claim → unshield the fee note) and each operation. No behavioral + change to the ledger — verification and defense-in-depth only. + ## [0.8.3] - 2026-07-04 ### Security diff --git a/frame/shielded-pool/src/lib.rs b/frame/shielded-pool/src/lib.rs index ee110432..5b35fca4 100644 --- a/frame/shielded-pool/src/lib.rs +++ b/frame/shielded-pool/src/lib.rs @@ -282,6 +282,25 @@ pub mod pallet { outside a benchmark build. This must never run on a live chain." ); } + + /// Ledger-solvency invariant: the tracked native-asset pool balance must + /// equal the pool account's physical free balance. Fees stay physical in + /// the pool until their note is unshielded, so both move together. + /// Only the native asset (0) is backed by `Currency`; other assets live in + /// external backends — TODO: extend when a per-asset balance reader exists. + #[cfg(feature = "try-runtime")] + fn try_state(_: BlockNumberFor) -> Result<(), sp_runtime::TryRuntimeError> { + let pool = Self::pool_account_id(); + let physical = T::Currency::free_balance(&pool); + let tracked = PoolBalancePerAsset::::get(0u32); + frame_support::ensure!( + tracked == physical, + sp_runtime::TryRuntimeError::Other( + "shielded-pool native ledger drifted from physical pool balance" + ) + ); + Ok(()) + } } // ======================================================================== diff --git a/frame/shielded-pool/src/operations/fees.rs b/frame/shielded-pool/src/operations/fees.rs index 954c44ff..16d9b300 100644 --- a/frame/shielded-pool/src/operations/fees.rs +++ b/frame/shielded-pool/src/operations/fees.rs @@ -588,4 +588,47 @@ mod tests { ); }); } + + // ── SP-1 ledger invariant: claim must NOT change PoolBalancePerAsset ────── + + #[test] + fn claim_does_not_change_pool_balance() { + use crate::storage::PoolBalanceRepository; + use frame_support::traits::Currency; + + new_test_ext().execute_with(|| { + let validator: u64 = 1; + let asset_id = setup_asset(); + let amount = 200u128; + let commitment = make_commitment(); + + // Seed pending fee + a pool ledger/physical balance backing it. + mock_pending_fees_set(validator, asset_id, amount); + let pool = crate::Pallet::::pool_account_id(); + let _ = + as Currency>::deposit_creating(&pool, amount); + PoolBalanceRepository::set_asset_balance::(asset_id, amount); + + let before = PoolBalanceRepository::get_asset_balance::(asset_id); + + assert_ok!(FeeOperation::claim_shielded::( + validator, + commitment, + amount, + asset_id, + make_memo(), + make_proof(), + make_signals(&commitment, amount, asset_id), + )); + + // The claim swaps a pending number for a note; both are backed by the + // same physical tokens already counted in the ledger. It must NOT move + // PoolBalancePerAsset, and the ledger stays == physical balance. + let after = PoolBalanceRepository::get_asset_balance::(asset_id); + assert_eq!(after, before, "claim must not change the pool ledger"); + let physical = as Currency>::free_balance(&pool); + assert_eq!(after, physical, "ledger must equal physical pool balance"); + assert_eq!(mock_pending_fees_get(validator, asset_id), 0); + }); + } } diff --git a/frame/shielded-pool/src/operations/private_transfer.rs b/frame/shielded-pool/src/operations/private_transfer.rs index 62e671b5..a5681b8d 100644 --- a/frame/shielded-pool/src/operations/private_transfer.rs +++ b/frame/shielded-pool/src/operations/private_transfer.rs @@ -611,4 +611,52 @@ mod tests { ); }); } + + // ── SP-1: private_transfer must leave the pool ledger untouched ─────────── + + /// A transfer moves value note-to-note; nothing enters or leaves the pool + /// physically, so PoolBalancePerAsset must not change. The fee becomes a + /// pending number backed by tokens already inside the pool. + #[test] + fn transfer_preserves_pool_ledger() { + use crate::storage::PoolBalanceRepository; + use frame_support::traits::Currency; + + new_test_ext().execute_with(|| { + let asset_id = 0u32; + // Seed a pool ledger/physical balance the transfer must not disturb. + let pool = crate::Pallet::::pool_account_id(); + let _ = as Currency>::deposit_creating(&pool, 1000); + PoolBalanceRepository::set_asset_balance::(asset_id, 1000); + MerkleRepository::add_historic_poseidon_root::(KNOWN_ROOT); + + let ledger_before = PoolBalanceRepository::get_asset_balance::(asset_id); + let physical_before = + as Currency>::free_balance(&pool); + let fee = 25u128; + + assert_ok!(PrivateTransferOperation::execute::( + proof(), + KNOWN_ROOT, + nullifiers_of(&[0x40]), + commitments_of(&[0x41]), + memos_of(1), + asset_id, + fee, + None, + )); + + assert_eq!( + PoolBalanceRepository::get_asset_balance::(asset_id), + ledger_before, + "transfer must not change the pool ledger" + ); + assert_eq!( + as Currency>::free_balance(&pool), + physical_before, + "transfer must not move physical pool tokens" + ); + assert_eq!(crate::mock::mock_pending_fees_get(1u64, asset_id), fee); + }); + } } diff --git a/frame/shielded-pool/src/operations/unshield.rs b/frame/shielded-pool/src/operations/unshield.rs index c97d82ae..2590afc6 100644 --- a/frame/shielded-pool/src/operations/unshield.rs +++ b/frame/shielded-pool/src/operations/unshield.rs @@ -125,6 +125,10 @@ impl UnshieldOperation { } } + // Decrement only `amount`: the `fee` tokens stay physically in the pool as + // backing for the pending relayer fee, so the tracked balance must retain + // them too. This is correct ONLY because the guard above requires + // `>= amount + fee`; do not weaken it to `>= amount` or fees go unbacked. PoolBalanceRepository::decrease_balance::(asset_id, amount); // Insert the change note commitment into the Merkle tree (partial unshield). @@ -744,4 +748,175 @@ mod tests { assert!(UnshieldOperation::is_merkle_root_known::(&KNOWN_ROOT)); }); } + + // ── SP-1 ledger-solvency invariant ─────────────────────────────────────── + // Invariant (A): PoolBalancePerAsset[a] == Currency::free_balance(pool) for the + // native asset. Fees stay physically in the pool until their note is unshielded, + // so tracked and physical always move together. + + fn pool_physical() -> u128 { + as Currency>::free_balance( + &crate::Pallet::::pool_account_id(), + ) + } + + fn tracked(asset_id: u32) -> u128 { + PoolBalanceRepository::get_asset_balance::(asset_id) + } + + /// T2 — unshield with a fee decrements the ledger by `amount` only (the fee + /// stays physical as backing), and ledger == physical afterwards. + #[test] + fn unshield_with_fee_decrements_amount_only() { + new_test_ext().execute_with(|| { + let asset_id = setup_asset(); + let (amount, fee) = (500u128, 50u128); + fund_pool(asset_id, amount + fee); + MerkleRepository::add_historic_poseidon_root::(KNOWN_ROOT); + + assert_ok!(UnshieldOperation::execute::( + proof(), + KNOWN_ROOT, + nullifier(0x21), + asset_id, + amount, + 2u64, + fee, + [0u8; 32], + FrameEncryptedMemo::default(), + None, + )); + + // Only `amount` left the pool; `fee` stays as backing for pending fees. + assert_eq!(tracked(asset_id), fee); + assert_eq!(pool_physical(), fee); + assert_eq!(tracked(asset_id), pool_physical()); + assert_eq!(crate::mock::mock_pending_fees_get(1u64, asset_id), fee); + }); + } + + /// T6 — the guard requires `>= amount + fee`: a pool covering only `amount` + /// (fee unbacked) must be rejected; covering `amount + fee` must succeed. + #[test] + fn unshield_guard_requires_amount_plus_fee() { + new_test_ext().execute_with(|| { + let asset_id = setup_asset(); + let (amount, fee) = (500u128, 50u128); + MerkleRepository::add_historic_poseidon_root::(KNOWN_ROOT); + + // Pool covers only `amount` → rejected. + fund_pool(asset_id, amount); + assert_noop!( + UnshieldOperation::execute::( + proof(), + KNOWN_ROOT, + nullifier(0x22), + asset_id, + amount, + 2u64, + fee, + [0u8; 32], + FrameEncryptedMemo::default(), + None, + ), + crate::pallet::Error::::InsufficientPoolBalance + ); + + // Pool covers `amount + fee` → accepted. + fund_pool(asset_id, amount + fee); + assert_ok!(UnshieldOperation::execute::( + proof(), + KNOWN_ROOT, + nullifier(0x23), + asset_id, + amount, + 2u64, + fee, + [0u8; 32], + FrameEncryptedMemo::default(), + None, + )); + }); + } + + /// T1 + T4 — full fee lifecycle keeps ledger == physical at every step: + /// shield → unshield(with fee) → claim(mint fee note) → unshield(fee note). + #[test] + fn fee_lifecycle_preserves_ledger_invariant() { + use crate::operations::{fees::FeeOperation, shield::ShieldOperation}; + + new_test_ext().execute_with(|| { + let asset_id = setup_asset(); + let memo = FrameEncryptedMemo::default(); + let depositor: u64 = 7; + // Fund above the shield amount so KeepAlive leaves the ED intact. + let _ = as Currency>::deposit_creating( + &depositor, 2000, + ); + MerkleRepository::add_historic_poseidon_root::(KNOWN_ROOT); + + // Step 1: shield 1000. Ledger and physical both +1000. + assert_ok!(ShieldOperation::execute::( + depositor, + asset_id, + 1000u128, + Commitment::new([0x31u8; 32]), + FrameEncryptedMemo::from_bytes(&[0u8; 176]).unwrap(), + )); + assert_eq!(tracked(asset_id), pool_physical()); + assert_eq!(tracked(asset_id), 1000); + + // Step 2: unshield amount=700 fee=50. Only `amount` leaves; fee stays. + assert_ok!(UnshieldOperation::execute::( + proof(), + KNOWN_ROOT, + nullifier(0x32), + asset_id, + 700u128, + 2u64, + 50u128, + [0u8; 32], + memo.clone(), + None, + )); + assert_eq!(tracked(asset_id), 300); + assert_eq!(tracked(asset_id), pool_physical()); + assert_eq!(crate::mock::mock_pending_fees_get(1u64, asset_id), 50); + + // Step 3: claim the 50 fee as a note. Ledger unchanged (same backing). + let fee_note = Commitment::new([0x33u8; 32]); + let mut signals = vec![0u8; 76]; + signals[..32].copy_from_slice(&fee_note.0); + signals[32..40].copy_from_slice(&(50u64).to_le_bytes()); + signals[40..44].copy_from_slice(&asset_id.to_le_bytes()); + assert_ok!(FeeOperation::claim_shielded::( + 1u64, + fee_note, + 50u128, + asset_id, + crate::types::EncryptedMemo::from_bytes(&[0u8; 176]).unwrap(), + vec![0x01u8; 128], + signals, + )); + assert_eq!(tracked(asset_id), 300); + assert_eq!(tracked(asset_id), pool_physical()); + assert_eq!(crate::mock::mock_pending_fees_get(1u64, asset_id), 0); + + // Step 4: unshield the 50 fee note (fee=0). Ledger and physical both −50. + assert_ok!(UnshieldOperation::execute::( + proof(), + KNOWN_ROOT, + nullifier(0x34), + asset_id, + 50u128, + 2u64, + 0u128, + [0u8; 32], + memo, + None, + )); + assert_eq!(tracked(asset_id), 250); + assert_eq!(tracked(asset_id), pool_physical()); + }); + } } diff --git a/frame/shielded-pool/src/storage.rs b/frame/shielded-pool/src/storage.rs index c1a9d41a..662401ae 100644 --- a/frame/shielded-pool/src/storage.rs +++ b/frame/shielded-pool/src/storage.rs @@ -203,6 +203,10 @@ impl PoolBalanceRepository { } pub fn decrease_balance(asset_id: u32, amount: BalanceOf) { PoolBalancePerAsset::::mutate(asset_id, |balance| { + // The unshield guard (`>= amount + fee`) makes `balance >= amount` always + // hold here; `defensive!` trips in tests/try-runtime if that invariant is + // ever broken, while `saturating_sub` keeps production safe. + frame_support::defensive_assert!(*balance >= amount); *balance = balance.saturating_sub(amount); }); } @@ -562,13 +566,28 @@ mod tests { } #[test] - fn pool_balance_repo_decrease_saturates_at_zero() { + fn pool_balance_repo_decrease_to_exact_zero() { new_test_ext().execute_with(|| { - PoolBalanceRepository::set_asset_balance::(4, 10u128); + // Decrementing by the full balance reaches zero without tripping the + // `defensive_assert!(balance >= amount)` guard. + PoolBalanceRepository::set_asset_balance::(4, 100u128); PoolBalanceRepository::decrease_balance::(4, 100u128); assert_eq!(PoolBalanceRepository::get_asset_balance::(4), 0u128); }); } + #[test] + #[cfg(debug_assertions)] + #[should_panic(expected = "Defensive")] + fn pool_balance_repo_decrease_below_balance_is_defensive() { + // An over-decrement (balance < amount) is an accounting bug: the defensive + // guard trips in test/debug. In production `saturating_sub` still floors at + // zero, but this path must never be reached under invariant (A). + new_test_ext().execute_with(|| { + PoolBalanceRepository::set_asset_balance::(4, 10u128); + PoolBalanceRepository::decrease_balance::(4, 100u128); + }); + } + fn _suppress_unused(_: ConstU32<10>) {} } From 29caf5e199e0e4b30d50489560d48d6ae48acf11 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Sun, 5 Jul 2026 11:48:04 -0400 Subject: [PATCH 02/18] fix(shielded-pool): bind relayer field into the transaction-pool tag --- frame/shielded-pool/CHANGELOG.md | 7 + frame/shielded-pool/src/lib.rs | 4 + frame/shielded-pool/src/mock.rs | 17 +- frame/shielded-pool/src/operations/fees.rs | 2 +- .../src/operations/private_transfer.rs | 41 ++++- .../shielded-pool/src/operations/unshield.rs | 109 +++++++++++- frame/shielded-pool/src/validate_unsigned.rs | 166 ++++++++++++++++-- 7 files changed, 322 insertions(+), 24 deletions(-) diff --git a/frame/shielded-pool/CHANGELOG.md b/frame/shielded-pool/CHANGELOG.md index a4202c0e..47769a12 100644 --- a/frame/shielded-pool/CHANGELOG.md +++ b/frame/shielded-pool/CHANGELOG.md @@ -13,6 +13,13 @@ All notable changes to `pallet-shielded-pool` will be documented in this file. tests anchoring the invariant across the full fee lifecycle (shield → unshield with fee → claim → unshield the fee note) and each operation. No behavioral change to the ledger — verification and defense-in-depth only. +- Bound the unsigned `relayer` field into the transaction-pool `provides` tag for + `unshield` and `private_transfer`. `ValidateUnsigned` now forwards `relayer` to + validation (it was previously dropped), so a variant differing only in the fee + recipient is a distinct pool entry and cannot silently replace the honest tx. + The relayer registry is governance-gated, so an unregistered address cannot + credit itself — it falls back to the block author (griefing at worst, never + theft). User funds are never at risk; only fee attribution is affected. ## [0.8.3] - 2026-07-04 diff --git a/frame/shielded-pool/src/lib.rs b/frame/shielded-pool/src/lib.rs index 5b35fca4..7df913e2 100644 --- a/frame/shielded-pool/src/lib.rs +++ b/frame/shielded-pool/src/lib.rs @@ -805,11 +805,13 @@ pub mod pallet { merkle_root, nullifiers, fee, + relayer, .. } => crate::validate_unsigned::validate_private_transfer::( merkle_root, nullifiers, fee, + relayer, ), Call::unshield { @@ -818,6 +820,7 @@ pub mod pallet { asset_id, amount, fee, + relayer, .. } => crate::validate_unsigned::validate_unshield::( merkle_root, @@ -825,6 +828,7 @@ pub mod pallet { asset_id, amount, fee, + relayer, ), _ => InvalidTransaction::Call.into(), diff --git a/frame/shielded-pool/src/mock.rs b/frame/shielded-pool/src/mock.rs index 7447f172..545f0d49 100644 --- a/frame/shielded-pool/src/mock.rs +++ b/frame/shielded-pool/src/mock.rs @@ -178,12 +178,23 @@ pub fn mock_set_min_relay_fee(fee: u128) { sp_io::storage::set(b"mock:min_relay_fee", &fee.encode()); } +/// Register an EVM address → account mapping so `resolve_relayer` returns `Some`. +/// Mirrors the governance-gated registry in `pallet-relayer` for tests. +pub fn mock_register_relayer(who: u64, addr: sp_core::H160) { + use parity_scale_codec::Encode; + let key = [b"mock:resolve:".as_ref(), addr.as_bytes()].concat(); + sp_io::storage::set(&key, &who.encode()); +} + impl pallet_relayer::RelayerInterface for MockRelayer { type AccountId = u64; - fn resolve_relayer(_evm_address: &sp_core::H160) -> Option { - // No registry in shielded-pool unit tests; fees fall back to block_author. - None + fn resolve_relayer(evm_address: &sp_core::H160) -> Option { + // Reads the test registry seeded by `mock_register_relayer`; unregistered + // addresses return None so fees fall back to block_author. + use parity_scale_codec::Decode; + let key = [b"mock:resolve:".as_ref(), evm_address.as_bytes()].concat(); + sp_io::storage::get(&key).and_then(|v| u64::decode(&mut &v[..]).ok()) } fn min_relay_fee() -> u128 { diff --git a/frame/shielded-pool/src/operations/fees.rs b/frame/shielded-pool/src/operations/fees.rs index 16d9b300..b7678fbc 100644 --- a/frame/shielded-pool/src/operations/fees.rs +++ b/frame/shielded-pool/src/operations/fees.rs @@ -589,7 +589,7 @@ mod tests { }); } - // ── SP-1 ledger invariant: claim must NOT change PoolBalancePerAsset ────── + // ── ledger invariant: claim must NOT change PoolBalancePerAsset ─────────── #[test] fn claim_does_not_change_pool_balance() { diff --git a/frame/shielded-pool/src/operations/private_transfer.rs b/frame/shielded-pool/src/operations/private_transfer.rs index a5681b8d..9144d00a 100644 --- a/frame/shielded-pool/src/operations/private_transfer.rs +++ b/frame/shielded-pool/src/operations/private_transfer.rs @@ -612,7 +612,7 @@ mod tests { }); } - // ── SP-1: private_transfer must leave the pool ledger untouched ─────────── + // ── private_transfer must leave the pool ledger untouched ───────────────── /// A transfer moves value note-to-note; nothing enters or leaves the pool /// physically, so PoolBalancePerAsset must not change. The fee becomes a @@ -659,4 +659,43 @@ mod tests { assert_eq!(crate::mock::mock_pending_fees_get(1u64, asset_id), fee); }); } + + // ── relay-fee attribution ──────────────────────────────────────────────── + + /// A registered relayer receives the transfer fee; an unregistered attacker + /// address cannot credit itself (falls back to block author). + #[test] + fn transfer_fee_attribution_registered_vs_unregistered() { + new_test_ext().execute_with(|| { + MerkleRepository::add_historic_poseidon_root::(KNOWN_ROOT); + let relayer_acct = 7u64; + crate::mock::mock_register_relayer(relayer_acct, sp_core::H160::from([0xAA; 20])); + + // Registered relayer 0xAA → fee to account 7. + assert_ok!(PrivateTransferOperation::execute::( + proof(), + KNOWN_ROOT, + nullifiers_of(&[0x70]), + commitments_of(&[0x71]), + memos_of(1), + 0u32, + 30u128, + Some(sp_core::H160::from([0xAA; 20])), + )); + assert_eq!(crate::mock::mock_pending_fees_get(relayer_acct, 0u32), 30); + + // Unregistered 0xBB → falls back to block author (1), never the attacker. + assert_ok!(PrivateTransferOperation::execute::( + proof(), + KNOWN_ROOT, + nullifiers_of(&[0x72]), + commitments_of(&[0x73]), + memos_of(1), + 0u32, + 20u128, + Some(sp_core::H160::from([0xBB; 20])), + )); + assert_eq!(crate::mock::mock_pending_fees_get(1u64, 0u32), 20); + }); + } } diff --git a/frame/shielded-pool/src/operations/unshield.rs b/frame/shielded-pool/src/operations/unshield.rs index 2590afc6..74d10013 100644 --- a/frame/shielded-pool/src/operations/unshield.rs +++ b/frame/shielded-pool/src/operations/unshield.rs @@ -749,7 +749,7 @@ mod tests { }); } - // ── SP-1 ledger-solvency invariant ─────────────────────────────────────── + // ── ledger-solvency invariant ──────────────────────────────────────────── // Invariant (A): PoolBalancePerAsset[a] == Currency::free_balance(pool) for the // native asset. Fees stay physically in the pool until their note is unshielded, // so tracked and physical always move together. @@ -919,4 +919,111 @@ mod tests { assert_eq!(tracked(asset_id), pool_physical()); }); } + + // ── relay-fee attribution ──────────────────────────────────────────────── + // The `relayer` field steers the fee. resolve_relayer only maps + // governance-registered addresses; an unregistered address falls back to the + // block author, so an unregistered attacker can never credit themselves. + + const BLOCK_AUTHOR: u64 = 1; + + fn evm(byte: u8) -> sp_core::H160 { + sp_core::H160::from([byte; 20]) + } + + /// A registered relayer receives the fee it relayed. + #[test] + fn unshield_fee_lands_at_registered_relayer() { + new_test_ext().execute_with(|| { + let asset_id = setup_asset(); + let (amount, fee) = (500u128, 50u128); + fund_pool(asset_id, amount + fee); + MerkleRepository::add_historic_poseidon_root::(KNOWN_ROOT); + + let relayer_acct = 7u64; + crate::mock::mock_register_relayer(relayer_acct, evm(0xAA)); + + assert_ok!(UnshieldOperation::execute::( + proof(), + KNOWN_ROOT, + nullifier(0x51), + asset_id, + amount, + 2u64, + fee, + [0u8; 32], + FrameEncryptedMemo::default(), + Some(evm(0xAA)), + )); + + assert_eq!( + crate::mock::mock_pending_fees_get(relayer_acct, asset_id), + fee + ); + assert_eq!( + crate::mock::mock_pending_fees_get(BLOCK_AUTHOR, asset_id), + 0 + ); + }); + } + + /// An unregistered `relayer` address cannot credit itself — the fee falls back + /// to the block author (griefing at worst, never theft). + #[test] + fn unshield_unregistered_relayer_falls_back_to_block_author() { + new_test_ext().execute_with(|| { + let asset_id = setup_asset(); + let (amount, fee) = (500u128, 50u128); + fund_pool(asset_id, amount + fee); + MerkleRepository::add_historic_poseidon_root::(KNOWN_ROOT); + + // 0xBB is never registered → resolve_relayer returns None. + assert_ok!(UnshieldOperation::execute::( + proof(), + KNOWN_ROOT, + nullifier(0x52), + asset_id, + amount, + 2u64, + fee, + [0u8; 32], + FrameEncryptedMemo::default(), + Some(evm(0xBB)), + )); + + assert_eq!( + crate::mock::mock_pending_fees_get(BLOCK_AUTHOR, asset_id), + fee + ); + }); + } + + /// `relayer = None` routes the fee to the block author. + #[test] + fn unshield_none_relayer_goes_to_block_author() { + new_test_ext().execute_with(|| { + let asset_id = setup_asset(); + let (amount, fee) = (500u128, 50u128); + fund_pool(asset_id, amount + fee); + MerkleRepository::add_historic_poseidon_root::(KNOWN_ROOT); + + assert_ok!(UnshieldOperation::execute::( + proof(), + KNOWN_ROOT, + nullifier(0x53), + asset_id, + amount, + 2u64, + fee, + [0u8; 32], + FrameEncryptedMemo::default(), + None, + )); + + assert_eq!( + crate::mock::mock_pending_fees_get(BLOCK_AUTHOR, asset_id), + fee + ); + }); + } } diff --git a/frame/shielded-pool/src/validate_unsigned.rs b/frame/shielded-pool/src/validate_unsigned.rs index ae3df0bf..52aef531 100644 --- a/frame/shielded-pool/src/validate_unsigned.rs +++ b/frame/shielded-pool/src/validate_unsigned.rs @@ -26,6 +26,7 @@ pub fn validate_private_transfer( merkle_root: &Hash, nullifiers: &BoundedVec>, fee: &BalanceOf, + relayer: &Option, ) -> TransactionValidity { // Anti-spam: fee must meet minimum relay fee let min_fee: BalanceOf = T::Relayer::min_relay_fee().saturated_into(); @@ -54,12 +55,17 @@ pub fn validate_private_transfer( return InvalidTransaction::Custom(2).into(); } - // Exclude dummy nullifiers (zero) from provides — they carry no identity - let provides: alloc::vec::Vec> = nullifiers + // Exclude dummy nullifiers (zero) from provides — they carry no identity. + // Bind the fee recipient (`relayer`) into the tag so a variant differing only + // in `relayer` is a distinct pool entry and cannot silently replace the honest + // tx. The shared nullifier tag already makes same-nullifier variants mutually + // exclusive (first-seen wins at equal fee); this hardens that boundary. + let mut provides: alloc::vec::Vec> = nullifiers .iter() .filter(|n| n.0 != [0u8; 32]) .map(|n| n.encode()) .collect(); + provides.push(relayer.encode()); ValidTransaction::with_tag_prefix("ShieldedPoolTransfer") .priority((*fee).saturated_into()) @@ -76,6 +82,7 @@ pub fn validate_unshield( asset_id: &u32, amount: &BalanceOf, fee: &BalanceOf, + relayer: &Option, ) -> TransactionValidity { // Anti-spam: fee must meet minimum relay fee let min_fee: BalanceOf = T::Relayer::min_relay_fee().saturated_into(); @@ -101,10 +108,14 @@ pub fn validate_unshield( return InvalidTransaction::Custom(3).into(); } + // Bind `relayer` into the tag alongside the nullifier: a variant differing only + // in the fee recipient is a distinct pool entry, so it cannot silently replace + // the honest tx. Same-nullifier variants stay mutually exclusive (first-seen + // wins at equal fee). ValidTransaction::with_tag_prefix("ShieldedPoolUnshield") .priority((*fee).saturated_into()) .longevity(TransactionLongevity::MAX) - .and_provides([nullifier.encode()]) + .and_provides([nullifier.encode(), relayer.encode()]) .propagate(true) .build() } @@ -138,8 +149,12 @@ mod tests { fn private_transfer_valid_transaction() { new_test_ext().execute_with(|| { MerkleRepository::add_historic_poseidon_root::(KNOWN_ROOT); - let result = - validate_private_transfer::(&KNOWN_ROOT, &nullifiers_of(&[0x01]), &0u128); + let result = validate_private_transfer::( + &KNOWN_ROOT, + &nullifiers_of(&[0x01]), + &0u128, + &None, + ); assert!(result.is_ok()); }); } @@ -147,8 +162,12 @@ mod tests { #[test] fn private_transfer_unknown_root_rejected() { new_test_ext().execute_with(|| { - let result = - validate_private_transfer::(&[0xFFu8; 32], &nullifiers_of(&[0x01]), &0u128); + let result = validate_private_transfer::( + &[0xFFu8; 32], + &nullifiers_of(&[0x01]), + &0u128, + &None, + ); assert!(result.is_err()); }); } @@ -159,8 +178,12 @@ mod tests { MerkleRepository::add_historic_poseidon_root::(KNOWN_ROOT); let n = make_nullifier(0x05); NullifierRepository::mark_as_used::(n, 1u64); - let result = - validate_private_transfer::(&KNOWN_ROOT, &nullifiers_of(&[0x05]), &0u128); + let result = validate_private_transfer::( + &KNOWN_ROOT, + &nullifiers_of(&[0x05]), + &0u128, + &None, + ); assert!(result.is_err()); }); } @@ -176,6 +199,7 @@ mod tests { &KNOWN_ROOT, &nullifiers_of(&[0x10, 0x11]), &0u128, + &None, ); assert!(result.is_err()); }); @@ -188,7 +212,8 @@ mod tests { let result = validate_private_transfer::( &KNOWN_ROOT, &nullifiers_of(&[0xA1, 0xA2]), - &100u128, // non-zero fee + &100u128, // non-zero fee, + &None, ); assert!(result.is_ok()); }); @@ -206,7 +231,7 @@ mod tests { let mut nullifiers: BoundedVec> = BoundedVec::new(); nullifiers.try_push(make_nullifier(0x01)).ok(); nullifiers.try_push(Nullifier::new([0u8; 32])).ok(); // dummy - let result = validate_private_transfer::(&KNOWN_ROOT, &nullifiers, &0u128); + let result = validate_private_transfer::(&KNOWN_ROOT, &nullifiers, &0u128, &None); assert!( result.is_ok(), "dummy nullifier should not cause Stale rejection" @@ -225,7 +250,7 @@ mod tests { let mut nullifiers: BoundedVec> = BoundedVec::new(); nullifiers.try_push(real).ok(); nullifiers.try_push(Nullifier::new([0u8; 32])).ok(); // dummy - let result = validate_private_transfer::(&KNOWN_ROOT, &nullifiers, &0u128); + let result = validate_private_transfer::(&KNOWN_ROOT, &nullifiers, &0u128, &None); assert!( result.is_err(), "used real nullifier must still be rejected" @@ -242,7 +267,7 @@ mod tests { let mut nullifiers: BoundedVec> = BoundedVec::new(); nullifiers.try_push(Nullifier::new([0u8; 32])).ok(); nullifiers.try_push(Nullifier::new([0u8; 32])).ok(); - let result = validate_private_transfer::(&KNOWN_ROOT, &nullifiers, &0u128); + let result = validate_private_transfer::(&KNOWN_ROOT, &nullifiers, &0u128, &None); assert!(result.is_err(), "all-dummy-nullifier tx must be rejected"); }); } @@ -260,6 +285,7 @@ mod tests { &0u32, &500u128, &0u128, + &None, ); assert!(result.is_ok()); }); @@ -275,6 +301,7 @@ mod tests { &0u32, &500u128, &0u128, + &None, ); assert!(result.is_err()); }); @@ -287,7 +314,7 @@ mod tests { PoolBalanceRepository::set_asset_balance::(0, 1_000u128); let n = make_nullifier(0x20); NullifierRepository::mark_as_used::(n, 1u64); - let result = validate_unshield::(&KNOWN_ROOT, &n, &0u32, &500u128, &0u128); + let result = validate_unshield::(&KNOWN_ROOT, &n, &0u32, &500u128, &0u128, &None); assert!(result.is_err()); }); } @@ -303,6 +330,7 @@ mod tests { &0u32, &100u128, // 100 > 50 &0u128, + &None, ); assert!(result.is_err()); }); @@ -320,6 +348,7 @@ mod tests { &0u32, &100u128, &60u128, + &None, ); assert!(result.is_err()); }); @@ -337,6 +366,7 @@ mod tests { &0u32, &100u128, &50u128, + &None, ); assert!(result.is_ok()); }); @@ -355,8 +385,12 @@ mod tests { crate::mock::mock_set_min_relay_fee(100); MerkleRepository::add_historic_poseidon_root::(KNOWN_ROOT); // fee=50 < min_relay_fee=100 → InvalidTransaction::Payment - let result = - validate_private_transfer::(&KNOWN_ROOT, &nullifiers_of(&[0x01]), &50u128); + let result = validate_private_transfer::( + &KNOWN_ROOT, + &nullifiers_of(&[0x01]), + &50u128, + &None, + ); assert!(result.is_err(), "fee below minimum must be rejected"); assert_eq!( result.unwrap_err(), @@ -373,8 +407,12 @@ mod tests { crate::mock::mock_set_min_relay_fee(100); MerkleRepository::add_historic_poseidon_root::(KNOWN_ROOT); // fee == min_relay_fee → accept - let result = - validate_private_transfer::(&KNOWN_ROOT, &nullifiers_of(&[0x01]), &100u128); + let result = validate_private_transfer::( + &KNOWN_ROOT, + &nullifiers_of(&[0x01]), + &100u128, + &None, + ); assert!(result.is_ok(), "fee equal to minimum must be accepted"); }); } @@ -392,6 +430,7 @@ mod tests { &0u32, &100u128, &50u128, + &None, ); assert!(result.is_err(), "fee below minimum must be rejected"); assert_eq!( @@ -416,8 +455,99 @@ mod tests { &0u32, &100u128, &200u128, + &None, ); assert!(result.is_ok(), "fee equal to minimum must be accepted"); }); } + + // ── relayer bound into the provides tag ────────────────────────────────── + + fn evm(byte: u8) -> sp_core::H160 { + sp_core::H160::from([byte; 20]) + } + + /// Two unshield variants differing only in `relayer` produce different + /// `provides` tag sets, so a spoofed variant is a distinct pool entry and + /// cannot silently replace the honest one. + #[test] + fn unshield_relayer_changes_provides_tag() { + new_test_ext().execute_with(|| { + MerkleRepository::add_historic_poseidon_root::(KNOWN_ROOT); + PoolBalanceRepository::set_asset_balance::(0, 1000u128); + let n = make_nullifier(0x61); + + let a = validate_unshield::( + &KNOWN_ROOT, + &n, + &0u32, + &100u128, + &10u128, + &Some(evm(0xAA)), + ) + .unwrap(); + let b = validate_unshield::( + &KNOWN_ROOT, + &n, + &0u32, + &100u128, + &10u128, + &Some(evm(0xBB)), + ) + .unwrap(); + let none = validate_unshield::(&KNOWN_ROOT, &n, &0u32, &100u128, &10u128, &None) + .unwrap(); + + assert_ne!(a.provides, b.provides, "different relayer → different tags"); + assert_ne!(a.provides, none.provides, "Some vs None → different tags"); + // Fee steers priority, not the relayer field. + assert_eq!(a.priority, b.priority); + }); + } + + /// Identical relayer + nullifier yields identical provides (honest re-gossip + /// is idempotent, first-seen wins at equal fee). + #[test] + fn unshield_same_relayer_same_provides() { + new_test_ext().execute_with(|| { + MerkleRepository::add_historic_poseidon_root::(KNOWN_ROOT); + PoolBalanceRepository::set_asset_balance::(0, 1000u128); + let n = make_nullifier(0x62); + + let a = validate_unshield::( + &KNOWN_ROOT, + &n, + &0u32, + &100u128, + &10u128, + &Some(evm(0xAA)), + ) + .unwrap(); + let b = validate_unshield::( + &KNOWN_ROOT, + &n, + &0u32, + &100u128, + &10u128, + &Some(evm(0xAA)), + ) + .unwrap(); + assert_eq!(a.provides, b.provides); + }); + } + + /// Same for private_transfer. + #[test] + fn transfer_relayer_changes_provides_tag() { + new_test_ext().execute_with(|| { + MerkleRepository::add_historic_poseidon_root::(KNOWN_ROOT); + let ns = nullifiers_of(&[0x63]); + + let a = validate_private_transfer::(&KNOWN_ROOT, &ns, &10u128, &Some(evm(0xAA))) + .unwrap(); + let b = validate_private_transfer::(&KNOWN_ROOT, &ns, &10u128, &Some(evm(0xBB))) + .unwrap(); + assert_ne!(a.provides, b.provides); + }); + } } From 315d95838a664386feae9166afcf854dae735156 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Sun, 5 Jul 2026 12:00:51 -0400 Subject: [PATCH 03/18] fix(shielded-pool): bound claim_shielded_fees proof and signals inputs --- .../src/calls/claim_shielded_fees.rs | 9 ++++++--- frame/shielded-pool/CHANGELOG.md | 3 +++ frame/shielded-pool/src/benchmarking.rs | 6 +++--- frame/shielded-pool/src/lib.rs | 8 ++++---- frame/shielded-pool/src/operations/fees.rs | 16 ++++++++++++++++ 5 files changed, 32 insertions(+), 10 deletions(-) diff --git a/frame/evm/precompile/shielded-pool/src/calls/claim_shielded_fees.rs b/frame/evm/precompile/shielded-pool/src/calls/claim_shielded_fees.rs index 52f702ec..d0161422 100644 --- a/frame/evm/precompile/shielded-pool/src/calls/claim_shielded_fees.rs +++ b/frame/evm/precompile/shielded-pool/src/calls/claim_shielded_fees.rs @@ -86,18 +86,21 @@ where return Err(err("claimShieldedFees: proof must be non-empty")); } - let public_signals: Vec = abi::decode_bytes_at_slot(params, 160)?; + let public_signals_raw: Vec = abi::decode_bytes_at_slot(params, 160)?; - if public_signals.len() != 76 { + if public_signals_raw.len() != 76 { return Err(err("claimShieldedFees: public_signals must be 76 bytes")); } + let public_signals = public_signals_raw + .try_into() + .map_err(|_| err("claimShieldedFees: public_signals too long"))?; Ok(pallet_shielded_pool::Call::::claim_shielded_fees { commitment, amount, asset_id, memo, - proof: proof.into(), + proof, public_signals, }) } diff --git a/frame/shielded-pool/CHANGELOG.md b/frame/shielded-pool/CHANGELOG.md index 47769a12..09e254a0 100644 --- a/frame/shielded-pool/CHANGELOG.md +++ b/frame/shielded-pool/CHANGELOG.md @@ -20,6 +20,9 @@ All notable changes to `pallet-shielded-pool` will be documented in this file. The relayer registry is governance-gated, so an unregistered address cannot credit itself — it falls back to the block author (griefing at worst, never theft). User funds are never at risk; only fee attribution is affected. +- `claim_shielded_fees` now takes `BoundedVec` for `proof` (max 512) and + `public_signals` (max 128) instead of unbounded `Vec`, so oversized inputs + are rejected by the codec bound before dispatch, matching the other extrinsics. ## [0.8.3] - 2026-07-04 diff --git a/frame/shielded-pool/src/benchmarking.rs b/frame/shielded-pool/src/benchmarking.rs index 7e2c19a7..c80df74e 100644 --- a/frame/shielded-pool/src/benchmarking.rs +++ b/frame/shielded-pool/src/benchmarking.rs @@ -207,7 +207,7 @@ mod benchmarks { let amount: BalanceOf = T::MinShieldAmount::get() * 10u32.into(); let amount_u128: u128 = amount.saturated_into(); - // Acumular relay fees para el validator + // Accumulate relay fees for the validator. T::Relayer::accumulate_relay_fee(&caller, asset_id, amount_u128); let commitment = Commitment([0x11u8; 32]); @@ -229,8 +229,8 @@ mod benchmarks { amount, asset_id, memo, - proof, - public_signals, + proof.try_into().expect("proof fits bound"), + public_signals.try_into().expect("signals fit bound"), ); } diff --git a/frame/shielded-pool/src/lib.rs b/frame/shielded-pool/src/lib.rs index 7df913e2..d65ff150 100644 --- a/frame/shielded-pool/src/lib.rs +++ b/frame/shielded-pool/src/lib.rs @@ -764,8 +764,8 @@ pub mod pallet { amount: BalanceOf, asset_id: u32, memo: FrameEncryptedMemo, - proof: sp_std::vec::Vec, - public_signals: sp_std::vec::Vec, + proof: BoundedVec>, + public_signals: BoundedVec>, ) -> DispatchResult { let validator = ensure_signed(origin)?; crate::operations::fees::FeeOperation::claim_shielded::( @@ -774,8 +774,8 @@ pub mod pallet { amount, asset_id, memo, - proof, - public_signals, + proof.into_inner(), + public_signals.into_inner(), ) } } diff --git a/frame/shielded-pool/src/operations/fees.rs b/frame/shielded-pool/src/operations/fees.rs index b7678fbc..0775fc0c 100644 --- a/frame/shielded-pool/src/operations/fees.rs +++ b/frame/shielded-pool/src/operations/fees.rs @@ -631,4 +631,20 @@ mod tests { assert_eq!(mock_pending_fees_get(validator, asset_id), 0); }); } + + // ── extrinsic input bounds (decode-before-reject guard) ────────────────── + + /// The `claim_shielded_fees` extrinsic takes `BoundedVec`, so an oversized + /// proof/signals input is rejected by the codec bound before any body logic. + #[test] + fn claim_shielded_fees_inputs_are_bounded() { + use frame_support::{BoundedVec, traits::ConstU32}; + + // proof bound = 512: at-bound fits, over-bound rejected. + assert!(BoundedVec::>::try_from(vec![0u8; 512]).is_ok()); + assert!(BoundedVec::>::try_from(vec![0u8; 513]).is_err()); + // signals bound = 128: the real 76-byte payload fits, over-bound rejected. + assert!(BoundedVec::>::try_from(vec![0u8; 76]).is_ok()); + assert!(BoundedVec::>::try_from(vec![0u8; 129]).is_err()); + } } From 68aeb0857ef0c8c15bff2dda20f4b9baf8e8617a Mon Sep 17 00:00:00 2001 From: nol4lej Date: Sun, 5 Jul 2026 12:10:41 -0400 Subject: [PATCH 04/18] fix(shielded-pool): bind merkle capacity guard to the fixed tree depth --- .../evm/precompile/shielded-pool/src/mock.rs | 2 +- frame/shielded-pool/CHANGELOG.md | 6 +++++ frame/shielded-pool/src/lib.rs | 6 +++++ frame/shielded-pool/src/merkle.rs | 26 ++++++++++++++++++- frame/shielded-pool/src/mock.rs | 2 +- frame/shielded-pool/src/runtime_api_impl.rs | 2 +- 6 files changed, 40 insertions(+), 4 deletions(-) diff --git a/frame/evm/precompile/shielded-pool/src/mock.rs b/frame/evm/precompile/shielded-pool/src/mock.rs index 8ea7dfee..c95a212c 100644 --- a/frame/evm/precompile/shielded-pool/src/mock.rs +++ b/frame/evm/precompile/shielded-pool/src/mock.rs @@ -118,7 +118,7 @@ impl pallet_evm::Config for Test { parameter_types! { pub const ShieldedPoolPalletId: PalletId = PalletId(*b"shldpool"); - pub const MaxTreeDepth: u32 = 32; + pub const MaxTreeDepth: u32 = 20; pub const MaxHistoricRoots: u32 = 100; pub const MinShieldAmount: u128 = 100; } diff --git a/frame/shielded-pool/CHANGELOG.md b/frame/shielded-pool/CHANGELOG.md index 09e254a0..ddf78368 100644 --- a/frame/shielded-pool/CHANGELOG.md +++ b/frame/shielded-pool/CHANGELOG.md @@ -23,6 +23,12 @@ All notable changes to `pallet-shielded-pool` will be documented in this file. - `claim_shielded_fees` now takes `BoundedVec` for `proof` (max 512) and `public_signals` (max 128) instead of unbounded `Vec`, so oversized inputs are rejected by the codec bound before dispatch, matching the other extrinsics. +- The Merkle-tree capacity guard now derives its limit from the fixed + `MAX_TREE_DEPTH` constant instead of the `MaxTreeDepth` config, so the + `MerkleTreeFull` check always fires at the real 2^20 capacity even if the config + is misset. An `integrity_test` asserts `MaxTreeDepth == MAX_TREE_DEPTH` at + runtime construction, keeping the depth reported to wallets consistent with the + tree the pallet actually implements. ## [0.8.3] - 2026-07-04 diff --git a/frame/shielded-pool/src/lib.rs b/frame/shielded-pool/src/lib.rs index d65ff150..2ad89591 100644 --- a/frame/shielded-pool/src/lib.rs +++ b/frame/shielded-pool/src/lib.rs @@ -281,6 +281,12 @@ pub mod pallet { `runtime-benchmarks`: shield/unshield/transfer proofs are NOT verified \ outside a benchmark build. This must never run on a live chain." ); + + assert_eq!( + T::MaxTreeDepth::get(), + crate::types::MAX_TREE_DEPTH, + "MaxTreeDepth config must equal the fixed tree depth (MAX_TREE_DEPTH)" + ); } /// Ledger-solvency invariant: the tracked native-asset pool balance must diff --git a/frame/shielded-pool/src/merkle.rs b/frame/shielded-pool/src/merkle.rs index aada9676..ccdff320 100644 --- a/frame/shielded-pool/src/merkle.rs +++ b/frame/shielded-pool/src/merkle.rs @@ -314,7 +314,7 @@ impl MerkleTreeService { /// replacing the former O(n) full recomputation from all leaves. pub fn insert_leaf(commitment: Commitment) -> Result { let index = MerkleRepository::get_tree_size::(); - let max_leaves = 2u32.saturating_pow(T::MaxTreeDepth::get()); + let max_leaves = 2u32.saturating_pow(crate::types::MAX_TREE_DEPTH); ensure!(index < max_leaves, Error::::MerkleTreeFull); ensure!( !CommitmentMemos::::contains_key(commitment), @@ -916,4 +916,28 @@ mod tests { "frontier root after 2 round-trips must match batch root" ); } + + // ── tree-depth consistency ──────────────────────────────────────────────── + + /// integrity_test passes when MaxTreeDepth equals the fixed tree depth. The + /// mock is aligned to MAX_TREE_DEPTH, so construction must not panic; a + /// divergent config would abort at runtime construction. + #[test] + fn integrity_test_accepts_aligned_tree_depth() { + use frame_support::traits::Hooks; + new_test_ext().execute_with(|| { + as Hooks>>::integrity_test(); + }); + } + + /// The capacity guard is bound by the real tree depth, not the config, so it + /// fires at exactly 2^MAX_TREE_DEPTH regardless of MaxTreeDepth. + #[test] + fn capacity_guard_uses_fixed_depth() { + use crate::types::MAX_TREE_DEPTH; + assert_eq!(MAX_TREE_DEPTH, 20); + // insert_leaf's max_leaves is 2^MAX_TREE_DEPTH; confirm the constant the + // guard reads matches the frontier depth (20 levels). + assert_eq!(2u32.saturating_pow(MAX_TREE_DEPTH), 1 << 20); + } } diff --git a/frame/shielded-pool/src/mock.rs b/frame/shielded-pool/src/mock.rs index 545f0d49..5e856cbd 100644 --- a/frame/shielded-pool/src/mock.rs +++ b/frame/shielded-pool/src/mock.rs @@ -32,7 +32,7 @@ impl pallet_balances::Config for Test { parameter_types! { pub const ShieldedPoolPalletId: PalletId = PalletId(*b"shldpool"); - pub const MaxTreeDepth: u32 = 32; + pub const MaxTreeDepth: u32 = 20; pub const MaxHistoricRoots: u32 = 100; pub const MinShieldAmount: u128 = 100; pub const MaxProofSize: u32 = 256; diff --git a/frame/shielded-pool/src/runtime_api_impl.rs b/frame/shielded-pool/src/runtime_api_impl.rs index 6cbac419..e9c2012d 100644 --- a/frame/shielded-pool/src/runtime_api_impl.rs +++ b/frame/shielded-pool/src/runtime_api_impl.rs @@ -64,7 +64,7 @@ mod tests { new_test_ext().execute_with(|| { let (_root, size, depth) = crate::Pallet::::get_merkle_tree_info(); assert_eq!(size, 0); - assert_eq!(depth, 32); // MaxTreeDepth = 32 in mock + assert_eq!(depth, 20); // MaxTreeDepth = 20 (matches MAX_TREE_DEPTH) }); } From 7c3069a80e5e8e883caccfe4797b230983347b16 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Sun, 5 Jul 2026 15:40:33 -0400 Subject: [PATCH 05/18] fix(shielded-pool): reject non-32-byte recipient in unshield binding --- frame/shielded-pool/CHANGELOG.md | 6 + frame/shielded-pool/src/helpers.rs | 4 +- frame/shielded-pool/src/mock.rs | 56 +++++---- frame/shielded-pool/src/operations/fees.rs | 117 +++++++++--------- .../src/operations/private_transfer.rs | 26 ++-- frame/shielded-pool/src/operations/shield.rs | 52 ++++---- .../shielded-pool/src/operations/unshield.rs | 113 ++++++++++------- frame/shielded-pool/src/storage.rs | 7 +- 8 files changed, 218 insertions(+), 163 deletions(-) diff --git a/frame/shielded-pool/CHANGELOG.md b/frame/shielded-pool/CHANGELOG.md index ddf78368..36436eb7 100644 --- a/frame/shielded-pool/CHANGELOG.md +++ b/frame/shielded-pool/CHANGELOG.md @@ -29,6 +29,12 @@ All notable changes to `pallet-shielded-pool` will be documented in this file. is misset. An `integrity_test` asserts `MaxTreeDepth == MAX_TREE_DEPTH` at runtime construction, keeping the depth reported to wallets consistent with the tree the pallet actually implements. +- `unshield` now rejects a recipient whose encoding is not exactly 32 bytes with + `InvalidRecipient`, instead of silently binding a zeroed recipient into the + proof. Production `AccountId` is `AccountId32` (32 bytes) — and every signature + scheme Orbinum unifies (sr25519/ed25519/ECDSA/EVM, plus future ones like Solana) + maps to a 32-byte account — so the strict binding covers them all. The test mock + was migrated from `u64` to `AccountId32` so the binding is exercised end-to-end. ## [0.8.3] - 2026-07-04 diff --git a/frame/shielded-pool/src/helpers.rs b/frame/shielded-pool/src/helpers.rs index 1b2d8fea..37c63ff5 100644 --- a/frame/shielded-pool/src/helpers.rs +++ b/frame/shielded-pool/src/helpers.rs @@ -45,7 +45,7 @@ impl Pallet { #[cfg(test)] mod tests { use crate::{ - mock::{Test, new_test_ext}, + mock::{Test, acc, new_test_ext}, storage::MerkleRepository, types::Commitment, }; @@ -55,7 +55,7 @@ mod tests { fn pool_account_id_is_non_zero() { new_test_ext().execute_with(|| { let pool = crate::Pallet::::pool_account_id(); - assert_ne!(pool, 0u64); + assert_ne!(pool, acc(0)); }); } diff --git a/frame/shielded-pool/src/mock.rs b/frame/shielded-pool/src/mock.rs index 5e856cbd..a3d700d9 100644 --- a/frame/shielded-pool/src/mock.rs +++ b/frame/shielded-pool/src/mock.rs @@ -3,9 +3,15 @@ use crate as pallet_shielded_pool; use frame_support::{PalletId, derive_impl, parameter_types, traits::ConstU128}; use pallet_zk_verifier::ZkVerifierPort; -use sp_runtime::BuildStorage; +use sp_runtime::{AccountId32, BuildStorage, traits::IdentityLookup}; type Block = frame_system::mocking::MockBlock; +type AccountId = AccountId32; + +/// Build a deterministic 32-byte test account (mirrors production `AccountId32`). +pub fn acc(n: u8) -> AccountId { + AccountId32::new([n; 32]) +} // Configure a mock runtime to test the pallet. frame_support::construct_runtime!( @@ -21,6 +27,8 @@ frame_support::construct_runtime!( impl frame_system::Config for Test { type Block = Block; type AccountData = pallet_balances::AccountData; + type AccountId = AccountId; + type Lookup = IdentityLookup; } #[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] @@ -132,12 +140,12 @@ impl pallet_shielded_pool::Config for Test { /// Mock implementation of `RelayerInterface` for unit tests. /// /// - `min_relay_fee()` → 0 (no minimum in tests; set higher when testing fee enforcement) -/// - `block_author()` → `Some(1u64)` (Alice) +/// - `block_author()` → `Some(acc(1))` (Alice) /// - Fee tracking is backed by raw `TestExternalities` storage (auto-reset per test). pub struct MockRelayer; /// Read a pending-fee balance from raw test storage. -pub fn mock_pending_fees_get(who: u64, asset_id: u32) -> u128 { +pub fn mock_pending_fees_get(who: AccountId, asset_id: u32) -> u128 { use parity_scale_codec::{Decode, Encode}; let key = [ b"mock:fees:".as_ref(), @@ -151,7 +159,7 @@ pub fn mock_pending_fees_get(who: u64, asset_id: u32) -> u128 { } /// Write a pending-fee balance to raw test storage. -pub fn mock_pending_fees_set(who: u64, asset_id: u32, amount: u128) { +pub fn mock_pending_fees_set(who: AccountId, asset_id: u32, amount: u128) { use parity_scale_codec::Encode; let key = [ b"mock:fees:".as_ref(), @@ -163,7 +171,7 @@ pub fn mock_pending_fees_set(who: u64, asset_id: u32, amount: u128) { } /// Read the registered EVM address for an account from raw test storage. -pub fn mock_evm_address_get(who: u64) -> Option { +pub fn mock_evm_address_get(who: AccountId) -> Option { use parity_scale_codec::{Decode, Encode}; let key = [b"mock:evm:".as_ref(), who.encode().as_slice()].concat(); sp_io::storage::get(&key) @@ -180,21 +188,21 @@ pub fn mock_set_min_relay_fee(fee: u128) { /// Register an EVM address → account mapping so `resolve_relayer` returns `Some`. /// Mirrors the governance-gated registry in `pallet-relayer` for tests. -pub fn mock_register_relayer(who: u64, addr: sp_core::H160) { +pub fn mock_register_relayer(who: AccountId, addr: sp_core::H160) { use parity_scale_codec::Encode; let key = [b"mock:resolve:".as_ref(), addr.as_bytes()].concat(); sp_io::storage::set(&key, &who.encode()); } impl pallet_relayer::RelayerInterface for MockRelayer { - type AccountId = u64; + type AccountId = AccountId; - fn resolve_relayer(evm_address: &sp_core::H160) -> Option { + fn resolve_relayer(evm_address: &sp_core::H160) -> Option { // Reads the test registry seeded by `mock_register_relayer`; unregistered // addresses return None so fees fall back to block_author. use parity_scale_codec::Decode; let key = [b"mock:resolve:".as_ref(), evm_address.as_bytes()].concat(); - sp_io::storage::get(&key).and_then(|v| u64::decode(&mut &v[..]).ok()) + sp_io::storage::get(&key).and_then(|v| AccountId::decode(&mut &v[..]).ok()) } fn min_relay_fee() -> u128 { @@ -208,35 +216,35 @@ impl pallet_relayer::RelayerInterface for MockRelayer { sp_std::vec![] } - fn block_author() -> Option { - Some(1u64) + fn block_author() -> Option { + Some(acc(1)) } - fn accumulate_relay_fee(author: &u64, asset_id: u32, amount: u128) { - let current = mock_pending_fees_get(*author, asset_id); - mock_pending_fees_set(*author, asset_id, current.saturating_add(amount)); + fn accumulate_relay_fee(author: &AccountId, asset_id: u32, amount: u128) { + let current = mock_pending_fees_get(author.clone(), asset_id); + mock_pending_fees_set(author.clone(), asset_id, current.saturating_add(amount)); } - fn pending_relay_fees(who: &u64, asset_id: u32) -> u128 { - mock_pending_fees_get(*who, asset_id) + fn pending_relay_fees(who: &AccountId, asset_id: u32) -> u128 { + mock_pending_fees_get(who.clone(), asset_id) } fn consume_relay_fee( - who: &u64, + who: &AccountId, asset_id: u32, amount: u128, ) -> frame_support::dispatch::DispatchResult { - let balance = mock_pending_fees_get(*who, asset_id); + let balance = mock_pending_fees_get(who.clone(), asset_id); if balance >= amount { - mock_pending_fees_set(*who, asset_id, balance - amount); + mock_pending_fees_set(who.clone(), asset_id, balance - amount); Ok(()) } else { Err(sp_runtime::DispatchError::Other("InsufficientPendingFees")) } } - fn registered_evm_address(who: &u64) -> Option { - mock_evm_address_get(*who) + fn registered_evm_address(who: &AccountId) -> Option { + mock_evm_address_get(who.clone()) } } @@ -247,7 +255,11 @@ pub fn new_test_ext() -> sp_io::TestExternalities { .unwrap(); pallet_balances::GenesisConfig:: { - balances: vec![(1, 1_000_000), (2, 1_000_000), (3, 1_000_000)], + balances: vec![ + (acc(1), 1_000_000), + (acc(2), 1_000_000), + (acc(3), 1_000_000), + ], ..Default::default() } .assimilate_storage(&mut t) diff --git a/frame/shielded-pool/src/operations/fees.rs b/frame/shielded-pool/src/operations/fees.rs index 0775fc0c..8b045cd4 100644 --- a/frame/shielded-pool/src/operations/fees.rs +++ b/frame/shielded-pool/src/operations/fees.rs @@ -102,7 +102,7 @@ impl FeeOperation { mod tests { use super::*; use crate::{ - mock::{Test, mock_pending_fees_get, mock_pending_fees_set, new_test_ext}, + mock::{Test, acc, mock_pending_fees_get, mock_pending_fees_set, new_test_ext}, operations::assets::AssetOperation, pallet::Event as PalletEvent, storage::CommitmentRepository, @@ -115,7 +115,7 @@ mod tests { fn setup_asset() -> u32 { let name = frame_support::BoundedVec::try_from(b"ORB".to_vec()).unwrap(); let sym = frame_support::BoundedVec::try_from(b"ORB".to_vec()).unwrap(); - AssetOperation::register_asset::(name, sym, 18, None, 1u64).unwrap() + AssetOperation::register_asset::(name, sym, 18, None, acc(1)).unwrap() } fn make_commitment() -> Commitment { @@ -146,14 +146,14 @@ mod tests { #[test] fn claim_shielded_works() { new_test_ext().execute_with(|| { - let validator: u64 = 1; + let validator = acc(1); let asset_id = setup_asset(); let amount = 200u128; let commitment = make_commitment(); - mock_pending_fees_set(validator, asset_id, amount); + mock_pending_fees_set(validator.clone(), asset_id, amount); assert_ok!(FeeOperation::claim_shielded::( - validator, + validator.clone(), commitment, amount, asset_id, @@ -170,7 +170,7 @@ mod tests { let commitment = make_commitment(); assert_noop!( FeeOperation::claim_shielded::( - 1u64, + acc(1), commitment, 100u128, 99u32, // not registered — fails before proof check @@ -186,15 +186,15 @@ mod tests { #[test] fn claim_shielded_insufficient_pending_fees_fails() { new_test_ext().execute_with(|| { - let validator: u64 = 1; + let validator = acc(1); let asset_id = setup_asset(); let commitment = make_commitment(); let amount = 100u128; - mock_pending_fees_set(validator, asset_id, 50u128); // only 50 available + mock_pending_fees_set(validator.clone(), asset_id, 50u128); // only 50 available assert_noop!( FeeOperation::claim_shielded::( - validator, + validator.clone(), commitment, amount, // requesting 100 asset_id, @@ -210,16 +210,16 @@ mod tests { #[test] fn claim_shielded_inserts_commitment_into_tree() { new_test_ext().execute_with(|| { - let validator: u64 = 1; + let validator = acc(1); let asset_id = setup_asset(); let amount = 100u128; let commitment = make_commitment(); - mock_pending_fees_set(validator, asset_id, amount); + mock_pending_fees_set(validator.clone(), asset_id, amount); assert!(!CommitmentRepository::exists::(&commitment)); assert_ok!(FeeOperation::claim_shielded::( - validator, + validator.clone(), commitment, amount, asset_id, @@ -235,15 +235,15 @@ mod tests { #[test] fn claim_shielded_stores_memo() { new_test_ext().execute_with(|| { - let validator: u64 = 1; + let validator = acc(1); let asset_id = setup_asset(); let amount = 100u128; let commitment = make_commitment(); let memo = make_memo(); - mock_pending_fees_set(validator, asset_id, amount); + mock_pending_fees_set(validator.clone(), asset_id, amount); assert_ok!(FeeOperation::claim_shielded::( - validator, + validator.clone(), commitment, amount, asset_id, @@ -260,15 +260,15 @@ mod tests { #[test] fn claim_shielded_deducts_pending_fees() { new_test_ext().execute_with(|| { - let validator: u64 = 1; + let validator = acc(1); let asset_id = setup_asset(); let initial = 500u128; let claim = 200u128; - mock_pending_fees_set(validator, asset_id, initial); + mock_pending_fees_set(validator.clone(), asset_id, initial); let commitment = make_commitment(); assert_ok!(FeeOperation::claim_shielded::( - validator, + validator.clone(), commitment, claim, asset_id, @@ -285,14 +285,14 @@ mod tests { #[test] fn claim_shielded_emits_validator_fees_claimed_event() { new_test_ext().execute_with(|| { - let validator: u64 = 1; + let validator = acc(1); let asset_id = setup_asset(); let amount = 300u128; let commitment = make_commitment(); - mock_pending_fees_set(validator, asset_id, amount); + mock_pending_fees_set(validator.clone(), asset_id, amount); assert_ok!(FeeOperation::claim_shielded::( - validator, + validator.clone(), commitment, amount, asset_id, @@ -322,15 +322,15 @@ mod tests { fn claim_shielded_zero_amount_fails_with_insufficient() { new_test_ext().execute_with(|| { // pending fees = 0, amount = 0 would pass the check, but let's verify the boundary - let validator: u64 = 2; + let validator = acc(2); let asset_id = setup_asset(); - mock_pending_fees_set(validator, asset_id, 0u128); + mock_pending_fees_set(validator.clone(), asset_id, 0u128); // requesting 1 with 0 available → InsufficientPendingFees let commitment = make_commitment(); assert_noop!( FeeOperation::claim_shielded::( - validator, + validator.clone(), commitment, 1u128, asset_id, @@ -349,14 +349,14 @@ mod tests { // encodes value=0 is cryptographically valid but inserts a worthless leaf into // the Merkle tree — cheap spam that wastes tree capacity. new_test_ext().execute_with(|| { - let validator: u64 = 1; + let validator = acc(1); let asset_id = setup_asset(); - mock_pending_fees_set(validator, asset_id, 500u128); + mock_pending_fees_set(validator.clone(), asset_id, 500u128); let commitment = make_commitment(); assert_noop!( FeeOperation::claim_shielded::( - validator, + validator.clone(), commitment, 0u128, asset_id, @@ -390,17 +390,17 @@ mod tests { #[cfg(not(feature = "runtime-benchmarks"))] fn claim_shielded_with_cryptographically_invalid_proof_returns_invalid_proof_error() { new_test_ext().execute_with(|| { - let validator: u64 = 1; + let validator = acc(1); let asset_id = setup_asset(); let amount = 100u128; let commitment = make_commitment(); - mock_pending_fees_set(validator, asset_id, amount); + mock_pending_fees_set(validator.clone(), asset_id, amount); // Proof has correct length (128) and correct signals, but the mock // verifier returns Ok(false) for proofs starting with 0x00. assert_noop!( FeeOperation::claim_shielded::( - validator, + validator.clone(), commitment, amount, asset_id, @@ -418,14 +418,14 @@ mod tests { fn claim_shielded_rejected_proof_leaves_no_state_changes() { // A rejected proof must not insert the commitment or consume fees. new_test_ext().execute_with(|| { - let validator: u64 = 1; + let validator = acc(1); let asset_id = setup_asset(); let amount = 200u128; let commitment = make_commitment(); - mock_pending_fees_set(validator, asset_id, amount); + mock_pending_fees_set(validator.clone(), asset_id, amount); let _ = FeeOperation::claim_shielded::( - validator, + validator.clone(), commitment, amount, asset_id, @@ -444,15 +444,15 @@ mod tests { #[test] fn claim_shielded_empty_proof_fails() { new_test_ext().execute_with(|| { - let validator: u64 = 1; + let validator = acc(1); let asset_id = setup_asset(); let amount = 100u128; let commitment = make_commitment(); - mock_pending_fees_set(validator, asset_id, amount); + mock_pending_fees_set(validator.clone(), asset_id, amount); assert_noop!( FeeOperation::claim_shielded::( - validator, + validator.clone(), commitment, amount, asset_id, @@ -468,15 +468,15 @@ mod tests { #[test] fn claim_shielded_wrong_proof_length_fails() { new_test_ext().execute_with(|| { - let validator: u64 = 1; + let validator = acc(1); let asset_id = setup_asset(); let amount = 100u128; let commitment = make_commitment(); - mock_pending_fees_set(validator, asset_id, amount); + mock_pending_fees_set(validator.clone(), asset_id, amount); assert_noop!( FeeOperation::claim_shielded::( - validator, + validator.clone(), commitment, amount, asset_id, @@ -492,15 +492,15 @@ mod tests { #[test] fn claim_shielded_wrong_signals_length_fails() { new_test_ext().execute_with(|| { - let validator: u64 = 1; + let validator = acc(1); let asset_id = setup_asset(); let amount = 100u128; let commitment = make_commitment(); - mock_pending_fees_set(validator, asset_id, amount); + mock_pending_fees_set(validator.clone(), asset_id, amount); assert_noop!( FeeOperation::claim_shielded::( - validator, + validator.clone(), commitment, amount, asset_id, @@ -516,17 +516,17 @@ mod tests { #[test] fn claim_shielded_signals_commitment_mismatch_fails() { new_test_ext().execute_with(|| { - let validator: u64 = 1; + let validator = acc(1); let asset_id = setup_asset(); let amount = 100u128; let commitment = make_commitment(); let other_commitment = Commitment::new([0xAAu8; 32]); - mock_pending_fees_set(validator, asset_id, amount); + mock_pending_fees_set(validator.clone(), asset_id, amount); // signals have a different commitment than the extrinsic argument assert_noop!( FeeOperation::claim_shielded::( - validator, + validator.clone(), commitment, amount, asset_id, @@ -542,16 +542,16 @@ mod tests { #[test] fn claim_shielded_signals_amount_mismatch_fails() { new_test_ext().execute_with(|| { - let validator: u64 = 1; + let validator = acc(1); let asset_id = setup_asset(); let amount = 100u128; let commitment = make_commitment(); - mock_pending_fees_set(validator, asset_id, amount); + mock_pending_fees_set(validator.clone(), asset_id, amount); // signals encode 999 but extrinsic claims 100 assert_noop!( FeeOperation::claim_shielded::( - validator, + validator.clone(), commitment, amount, asset_id, @@ -567,16 +567,16 @@ mod tests { #[test] fn claim_shielded_signals_asset_id_mismatch_fails() { new_test_ext().execute_with(|| { - let validator: u64 = 1; + let validator = acc(1); let asset_id = setup_asset(); let amount = 100u128; let commitment = make_commitment(); - mock_pending_fees_set(validator, asset_id, amount); + mock_pending_fees_set(validator.clone(), asset_id, amount); // signals encode asset_id=999 but extrinsic claims registered asset_id assert_noop!( FeeOperation::claim_shielded::( - validator, + validator.clone(), commitment, amount, asset_id, @@ -595,24 +595,26 @@ mod tests { fn claim_does_not_change_pool_balance() { use crate::storage::PoolBalanceRepository; use frame_support::traits::Currency; + use sp_runtime::AccountId32; new_test_ext().execute_with(|| { - let validator: u64 = 1; + let validator = acc(1); let asset_id = setup_asset(); let amount = 200u128; let commitment = make_commitment(); // Seed pending fee + a pool ledger/physical balance backing it. - mock_pending_fees_set(validator, asset_id, amount); + mock_pending_fees_set(validator.clone(), asset_id, amount); let pool = crate::Pallet::::pool_account_id(); - let _ = - as Currency>::deposit_creating(&pool, amount); + let _ = as Currency>::deposit_creating( + &pool, amount, + ); PoolBalanceRepository::set_asset_balance::(asset_id, amount); let before = PoolBalanceRepository::get_asset_balance::(asset_id); assert_ok!(FeeOperation::claim_shielded::( - validator, + validator.clone(), commitment, amount, asset_id, @@ -626,7 +628,8 @@ mod tests { // PoolBalancePerAsset, and the ledger stays == physical balance. let after = PoolBalanceRepository::get_asset_balance::(asset_id); assert_eq!(after, before, "claim must not change the pool ledger"); - let physical = as Currency>::free_balance(&pool); + let physical = + as Currency>::free_balance(&pool); assert_eq!(after, physical, "ledger must equal physical pool balance"); assert_eq!(mock_pending_fees_get(validator, asset_id), 0); }); diff --git a/frame/shielded-pool/src/operations/private_transfer.rs b/frame/shielded-pool/src/operations/private_transfer.rs index 9144d00a..9edd5fea 100644 --- a/frame/shielded-pool/src/operations/private_transfer.rs +++ b/frame/shielded-pool/src/operations/private_transfer.rs @@ -145,7 +145,7 @@ impl PrivateTransferOperation { mod tests { use super::*; use crate::{ - mock::{Test, new_test_ext}, + mock::{Test, acc, new_test_ext}, pallet::Event as PalletEvent, storage::{CommitmentRepository, MerkleRepository, NullifierRepository}, types::{Commitment, EncryptedMemo, MAX_ENCRYPTED_MEMO_SIZE, Nullifier}, @@ -438,7 +438,7 @@ mod tests { )); // MockRelayer block_author = Some(1) - let pending = crate::mock::mock_pending_fees_get(1u64, 0u32); + let pending = crate::mock::mock_pending_fees_get(acc(1), 0u32); assert_eq!(pending, fee); }); } @@ -459,7 +459,7 @@ mod tests { None, )); - let pending = crate::mock::mock_pending_fees_get(1u64, 0u32); + let pending = crate::mock::mock_pending_fees_get(acc(1), 0u32); assert_eq!(pending, 0u128); }); } @@ -621,18 +621,21 @@ mod tests { fn transfer_preserves_pool_ledger() { use crate::storage::PoolBalanceRepository; use frame_support::traits::Currency; + use sp_runtime::AccountId32; new_test_ext().execute_with(|| { let asset_id = 0u32; // Seed a pool ledger/physical balance the transfer must not disturb. let pool = crate::Pallet::::pool_account_id(); - let _ = as Currency>::deposit_creating(&pool, 1000); + let _ = as Currency>::deposit_creating( + &pool, 1000, + ); PoolBalanceRepository::set_asset_balance::(asset_id, 1000); MerkleRepository::add_historic_poseidon_root::(KNOWN_ROOT); let ledger_before = PoolBalanceRepository::get_asset_balance::(asset_id); let physical_before = - as Currency>::free_balance(&pool); + as Currency>::free_balance(&pool); let fee = 25u128; assert_ok!(PrivateTransferOperation::execute::( @@ -652,11 +655,11 @@ mod tests { "transfer must not change the pool ledger" ); assert_eq!( - as Currency>::free_balance(&pool), + as Currency>::free_balance(&pool), physical_before, "transfer must not move physical pool tokens" ); - assert_eq!(crate::mock::mock_pending_fees_get(1u64, asset_id), fee); + assert_eq!(crate::mock::mock_pending_fees_get(acc(1), asset_id), fee); }); } @@ -668,8 +671,11 @@ mod tests { fn transfer_fee_attribution_registered_vs_unregistered() { new_test_ext().execute_with(|| { MerkleRepository::add_historic_poseidon_root::(KNOWN_ROOT); - let relayer_acct = 7u64; - crate::mock::mock_register_relayer(relayer_acct, sp_core::H160::from([0xAA; 20])); + let relayer_acct = acc(7); + crate::mock::mock_register_relayer( + relayer_acct.clone(), + sp_core::H160::from([0xAA; 20]), + ); // Registered relayer 0xAA → fee to account 7. assert_ok!(PrivateTransferOperation::execute::( @@ -695,7 +701,7 @@ mod tests { 20u128, Some(sp_core::H160::from([0xBB; 20])), )); - assert_eq!(crate::mock::mock_pending_fees_get(1u64, 0u32), 20); + assert_eq!(crate::mock::mock_pending_fees_get(acc(1), 0u32), 20); }); } } diff --git a/frame/shielded-pool/src/operations/shield.rs b/frame/shielded-pool/src/operations/shield.rs index 460d0d0d..ab4233c0 100644 --- a/frame/shielded-pool/src/operations/shield.rs +++ b/frame/shielded-pool/src/operations/shield.rs @@ -77,13 +77,14 @@ impl ShieldOperation { mod tests { use super::*; use crate::{ - mock::{Test, new_test_ext}, + mock::{Test, acc, new_test_ext}, operations::assets::AssetOperation, pallet::Event as PalletEvent, storage::{CommitmentRepository, PoolBalanceRepository}, types::{Commitment, EncryptedMemo, MAX_ENCRYPTED_MEMO_SIZE}, }; use frame_support::{assert_noop, assert_ok}; + use sp_runtime::AccountId32; // ── helpers ────────────────────────────────────────────────────────────── @@ -91,7 +92,7 @@ mod tests { fn setup_asset() -> u32 { let name = frame_support::BoundedVec::try_from(b"Orbinum".to_vec()).unwrap(); let symbol = frame_support::BoundedVec::try_from(b"ORB".to_vec()).unwrap(); - let id = AssetOperation::register_asset::(name, symbol, 18, None, 1u64).unwrap(); + let id = AssetOperation::register_asset::(name, symbol, 18, None, acc(1)).unwrap(); AssetOperation::verify::(id).unwrap(); id } @@ -117,7 +118,7 @@ mod tests { let c = commitment(0x01); assert_ok!(ShieldOperation::execute::( - 1u64, + acc(1), asset_id, 500u128, c, @@ -130,7 +131,13 @@ mod tests { fn execute_invalid_asset_fails() { new_test_ext().execute_with(|| { assert_noop!( - ShieldOperation::execute::(1u64, 99u32, 500u128, commitment(1), memo_valid()), + ShieldOperation::execute::( + acc(1), + 99u32, + 500u128, + commitment(1), + memo_valid() + ), crate::pallet::Error::::InvalidAssetId ); }); @@ -141,11 +148,12 @@ mod tests { new_test_ext().execute_with(|| { let name = frame_support::BoundedVec::try_from(b"Orbinum".to_vec()).unwrap(); let symbol = frame_support::BoundedVec::try_from(b"ORB".to_vec()).unwrap(); - let id = AssetOperation::register_asset::(name, symbol, 18, None, 1u64).unwrap(); + let id = + AssetOperation::register_asset::(name, symbol, 18, None, acc(1)).unwrap(); // Not verified assert_noop!( - ShieldOperation::execute::(1u64, id, 500u128, commitment(1), memo_valid()), + ShieldOperation::execute::(acc(1), id, 500u128, commitment(1), memo_valid()), crate::pallet::Error::::AssetNotVerified ); }); @@ -158,7 +166,7 @@ mod tests { // MinShieldAmount = 100; amount = 50 < 100 assert_noop!( ShieldOperation::execute::( - 1u64, + acc(1), asset_id, 50u128, commitment(1), @@ -175,7 +183,7 @@ mod tests { let asset_id = setup_asset(); assert_noop!( ShieldOperation::execute::( - 1u64, + acc(1), asset_id, 500u128, commitment(1), @@ -192,7 +200,7 @@ mod tests { let asset_id = setup_asset(); let before = PoolBalanceRepository::get_asset_balance::(asset_id); assert_ok!(ShieldOperation::execute::( - 1u64, + acc(1), asset_id, 500u128, commitment(0x02), @@ -209,7 +217,7 @@ mod tests { let asset_id = setup_asset(); let c = commitment(0x03); assert_ok!(ShieldOperation::execute::( - 1u64, + acc(1), asset_id, 200u128, c, @@ -225,7 +233,7 @@ mod tests { let asset_id = setup_asset(); let c = commitment(0x04); assert_ok!(ShieldOperation::execute::( - 1u64, + acc(1), asset_id, 300u128, c, @@ -236,11 +244,11 @@ mod tests { matches!( r.event, crate::mock::RuntimeEvent::ShieldedPool(PalletEvent::Shielded { - depositor: 1, + depositor: ref ed, amount: 300, commitment: ec, .. - }) if ec == c + }) if ec == c && *ed == acc(1) ) }); assert!(found, "Shielded event not emitted"); @@ -251,13 +259,13 @@ mod tests { fn execute_transfers_currency_to_pool() { new_test_ext().execute_with(|| { let asset_id = setup_asset(); - let sender: u64 = 1; + let sender = acc(1); let pool = crate::Pallet::::pool_account_id(); let balance_before = - as frame_support::traits::Currency>::free_balance(&sender); + as frame_support::traits::Currency>::free_balance(&sender); assert_ok!(ShieldOperation::execute::( - sender, + sender.clone(), asset_id, 1_000u128, commitment(0x05), @@ -265,9 +273,9 @@ mod tests { )); let balance_after = - as frame_support::traits::Currency>::free_balance(&sender); + as frame_support::traits::Currency>::free_balance(&sender); let pool_balance = as frame_support::traits::Currency< - u64, + AccountId32, >>::free_balance(&pool); assert_eq!(balance_before - balance_after, 1_000u128); @@ -292,7 +300,7 @@ mod tests { let asset_id = setup_asset(); let c = commitment(0xBB); assert_ok!(ShieldOperation::execute::( - 1u64, + acc(1), asset_id, 200u128, c, @@ -319,7 +327,7 @@ mod tests { // Unverified asset let name = frame_support::BoundedVec::try_from(b"Other".to_vec()).unwrap(); let sym = frame_support::BoundedVec::try_from(b"OTH".to_vec()).unwrap(); - let id2 = AssetOperation::register_asset::(name, sym, 6, None, 2u64).unwrap(); + let id2 = AssetOperation::register_asset::(name, sym, 6, None, acc(2)).unwrap(); assert!(!ShieldOperation::is_asset_verified::(id2)); }); } @@ -335,7 +343,7 @@ mod tests { let c = commitment(0xDE); assert_ok!(ShieldOperation::execute::( - 1u64, + acc(1), asset_id, 500u128, c, @@ -343,7 +351,7 @@ mod tests { )); assert_noop!( - ShieldOperation::execute::(1u64, asset_id, 500u128, c, memo_valid()), + ShieldOperation::execute::(acc(1), asset_id, 500u128, c, memo_valid()), crate::pallet::Error::::CommitmentAlreadyExists ); }); diff --git a/frame/shielded-pool/src/operations/unshield.rs b/frame/shielded-pool/src/operations/unshield.rs index 74d10013..05342237 100644 --- a/frame/shielded-pool/src/operations/unshield.rs +++ b/frame/shielded-pool/src/operations/unshield.rs @@ -20,6 +20,18 @@ use sp_runtime::{SaturatedConversion, traits::Zero}; pub struct UnshieldOperation; +/// Encode a recipient account into the exact 32-byte field element bound into the +/// unshield proof. Orbinum's `AccountId` is always `AccountId32` (every signature +/// scheme — sr25519/ed25519/ECDSA/EVM, and future ones like Solana — unifies to a +/// 32-byte account). A non-32-byte encoding is rejected with `InvalidRecipient` +/// rather than silently binding a zeroed recipient. +#[cfg(not(feature = "skip-proof-verification"))] +fn recipient_to_field( + recipient: &::AccountId, +) -> Result<[u8; 32], Error> { + <[u8; 32]>::try_from(recipient.encode().as_slice()).map_err(|_| Error::::InvalidRecipient) +} + impl UnshieldOperation { #[allow(clippy::too_many_arguments)] pub fn execute( @@ -87,7 +99,7 @@ impl UnshieldOperation { #[cfg(not(feature = "skip-proof-verification"))] { - let recipient_bytes: [u8; 32] = recipient.encode().try_into().unwrap_or([0u8; 32]); + let recipient_bytes = recipient_to_field::(&recipient)?; let valid = T::ZkVerifier::verify_unshield_proof( proof, &merkle_root, @@ -192,7 +204,7 @@ impl UnshieldOperation { mod tests { use super::*; use crate::{ - mock::{Test, new_test_ext}, + mock::{Test, acc, new_test_ext}, operations::assets::AssetOperation, pallet::Event as PalletEvent, storage::{ @@ -201,6 +213,7 @@ mod tests { types::{Commitment, Nullifier}, }; use frame_support::{assert_noop, assert_ok, traits::Currency}; + use sp_runtime::AccountId32; // ── helpers ────────────────────────────────────────────────────────────── @@ -209,7 +222,7 @@ mod tests { fn setup_asset() -> u32 { let name = frame_support::BoundedVec::try_from(b"Orbinum".to_vec()).unwrap(); let symbol = frame_support::BoundedVec::try_from(b"ORB".to_vec()).unwrap(); - let id = AssetOperation::register_asset::(name, symbol, 18, None, 1u64).unwrap(); + let id = AssetOperation::register_asset::(name, symbol, 18, None, acc(1)).unwrap(); AssetOperation::verify::(id).unwrap(); id } @@ -217,7 +230,9 @@ mod tests { /// Fund pool account (currency + pool balance tracker). fn fund_pool(asset_id: u32, total: u128) { let pool = crate::Pallet::::pool_account_id(); - let _ = as Currency>::deposit_creating(&pool, total); + let _ = as Currency>::deposit_creating( + &pool, total, + ); PoolBalanceRepository::set_asset_balance::(asset_id, total); } @@ -246,7 +261,7 @@ mod tests { nullifier(0x01), asset_id, amount, - 2u64, // recipient + acc(2), // recipient 0u128, [0u8; 32], FrameEncryptedMemo::default(), @@ -266,7 +281,7 @@ mod tests { nullifier(1), 99u32, 100u128, - 2u64, + acc(2), 0u128, [0u8; 32], FrameEncryptedMemo::default(), @@ -282,7 +297,7 @@ mod tests { new_test_ext().execute_with(|| { let name = frame_support::BoundedVec::try_from(b"T".to_vec()).unwrap(); let sym = frame_support::BoundedVec::try_from(b"T".to_vec()).unwrap(); - let id = AssetOperation::register_asset::(name, sym, 18, None, 1u64).unwrap(); + let id = AssetOperation::register_asset::(name, sym, 18, None, acc(1)).unwrap(); MerkleRepository::add_historic_poseidon_root::(KNOWN_ROOT); fund_pool(id, 1_000u128); @@ -293,7 +308,7 @@ mod tests { nullifier(1), id, 100u128, - 2u64, + acc(2), 0u128, [0u8; 32], FrameEncryptedMemo::default(), @@ -321,7 +336,7 @@ mod tests { nullifier(0x77), asset_id, 0u128, - 2u64, + acc(2), 0u128, [0u8; 32], FrameEncryptedMemo::default(), @@ -372,7 +387,7 @@ mod tests { nullifier(1), asset_id, 100u128, - 2u64, + acc(2), 0u128, [0u8; 32], FrameEncryptedMemo::default(), @@ -400,7 +415,7 @@ mod tests { n, asset_id, 500u128, - 2u64, + acc(2), 0u128, [0u8; 32], FrameEncryptedMemo::default(), @@ -426,7 +441,7 @@ mod tests { nullifier(1), asset_id, 100u128, - 2u64, + acc(2), 0u128, [0u8; 32], FrameEncryptedMemo::default(), @@ -452,7 +467,7 @@ mod tests { n, asset_id, 300u128, - 2u64, + acc(2), 0u128, [0u8; 32], FrameEncryptedMemo::default(), @@ -476,7 +491,7 @@ mod tests { nullifier(0x06), asset_id, amount, - 2u64, + acc(2), 0u128, [0u8; 32], FrameEncryptedMemo::default(), @@ -492,12 +507,13 @@ mod tests { fn execute_transfers_currency_to_recipient() { new_test_ext().execute_with(|| { let asset_id = setup_asset(); - let recipient: u64 = 2; + let recipient = acc(2); let amount = 300u128; fund_pool(asset_id, 1_000u128); MerkleRepository::add_historic_poseidon_root::(KNOWN_ROOT); - let before = as Currency>::free_balance(&recipient); + let before = + as Currency>::free_balance(&recipient); assert_ok!(UnshieldOperation::execute::( proof(), @@ -505,14 +521,15 @@ mod tests { nullifier(0x07), asset_id, amount, - recipient, + recipient.clone(), 0u128, [0u8; 32], FrameEncryptedMemo::default(), None, )); - let after = as Currency>::free_balance(&recipient); + let after = + as Currency>::free_balance(&recipient); assert_eq!(after - before, amount); }); } @@ -531,7 +548,7 @@ mod tests { n, asset_id, 200u128, - 2u64, + acc(2), 0u128, [0u8; 32], FrameEncryptedMemo::default(), @@ -545,11 +562,11 @@ mod tests { crate::mock::RuntimeEvent::ShieldedPool(PalletEvent::Unshielded { nullifier: en, amount: 200, - recipient: 2, + recipient: ref er, change_commitment: None, change_encrypted_memo: None, change_leaf_index: None, - }) if en == n + }) if en == n && *er == acc(2) ) }); assert!(found, "Unshielded event not emitted"); @@ -571,7 +588,7 @@ mod tests { nullifier(0x09), asset_id, amount, - 2u64, + acc(2), fee, [0u8; 32], FrameEncryptedMemo::default(), @@ -579,7 +596,7 @@ mod tests { )); // MockRelayer block_author returns Some(1); fee should be accumulated there - let pending = crate::mock::mock_pending_fees_get(1u64, asset_id); + let pending = crate::mock::mock_pending_fees_get(acc(1), asset_id); assert_eq!(pending, fee); }); } @@ -603,7 +620,7 @@ mod tests { nullifier(0x10), asset_id, amount, - 2u64, + acc(2), 0u128, change_comm_bytes, FrameEncryptedMemo::default(), @@ -659,7 +676,7 @@ mod tests { nullifier(0x11), asset_id, amount, - 2u64, + acc(2), 0u128, [0u8; 32], // zero change_commitment = total unshield FrameEncryptedMemo::default(), @@ -716,7 +733,7 @@ mod tests { nullifier(0x12), asset_id, 500u128, - 2u64, + acc(2), 0u128, change_comm_bytes, FrameEncryptedMemo::default(), @@ -755,7 +772,7 @@ mod tests { // so tracked and physical always move together. fn pool_physical() -> u128 { - as Currency>::free_balance( + as Currency>::free_balance( &crate::Pallet::::pool_account_id(), ) } @@ -780,7 +797,7 @@ mod tests { nullifier(0x21), asset_id, amount, - 2u64, + acc(2), fee, [0u8; 32], FrameEncryptedMemo::default(), @@ -791,7 +808,7 @@ mod tests { assert_eq!(tracked(asset_id), fee); assert_eq!(pool_physical(), fee); assert_eq!(tracked(asset_id), pool_physical()); - assert_eq!(crate::mock::mock_pending_fees_get(1u64, asset_id), fee); + assert_eq!(crate::mock::mock_pending_fees_get(acc(1), asset_id), fee); }); } @@ -813,7 +830,7 @@ mod tests { nullifier(0x22), asset_id, amount, - 2u64, + acc(2), fee, [0u8; 32], FrameEncryptedMemo::default(), @@ -830,7 +847,7 @@ mod tests { nullifier(0x23), asset_id, amount, - 2u64, + acc(2), fee, [0u8; 32], FrameEncryptedMemo::default(), @@ -848,9 +865,9 @@ mod tests { new_test_ext().execute_with(|| { let asset_id = setup_asset(); let memo = FrameEncryptedMemo::default(); - let depositor: u64 = 7; + let depositor = acc(7); // Fund above the shield amount so KeepAlive leaves the ED intact. - let _ = as Currency>::deposit_creating( + let _ = as Currency>::deposit_creating( &depositor, 2000, ); MerkleRepository::add_historic_poseidon_root::(KNOWN_ROOT); @@ -873,7 +890,7 @@ mod tests { nullifier(0x32), asset_id, 700u128, - 2u64, + acc(2), 50u128, [0u8; 32], memo.clone(), @@ -881,7 +898,7 @@ mod tests { )); assert_eq!(tracked(asset_id), 300); assert_eq!(tracked(asset_id), pool_physical()); - assert_eq!(crate::mock::mock_pending_fees_get(1u64, asset_id), 50); + assert_eq!(crate::mock::mock_pending_fees_get(acc(1), asset_id), 50); // Step 3: claim the 50 fee as a note. Ledger unchanged (same backing). let fee_note = Commitment::new([0x33u8; 32]); @@ -890,7 +907,7 @@ mod tests { signals[32..40].copy_from_slice(&(50u64).to_le_bytes()); signals[40..44].copy_from_slice(&asset_id.to_le_bytes()); assert_ok!(FeeOperation::claim_shielded::( - 1u64, + acc(1), fee_note, 50u128, asset_id, @@ -900,7 +917,7 @@ mod tests { )); assert_eq!(tracked(asset_id), 300); assert_eq!(tracked(asset_id), pool_physical()); - assert_eq!(crate::mock::mock_pending_fees_get(1u64, asset_id), 0); + assert_eq!(crate::mock::mock_pending_fees_get(acc(1), asset_id), 0); // Step 4: unshield the 50 fee note (fee=0). Ledger and physical both −50. assert_ok!(UnshieldOperation::execute::( @@ -909,7 +926,7 @@ mod tests { nullifier(0x34), asset_id, 50u128, - 2u64, + acc(2), 0u128, [0u8; 32], memo, @@ -925,7 +942,9 @@ mod tests { // governance-registered addresses; an unregistered address falls back to the // block author, so an unregistered attacker can never credit themselves. - const BLOCK_AUTHOR: u64 = 1; + fn block_author() -> sp_runtime::AccountId32 { + acc(1) + } fn evm(byte: u8) -> sp_core::H160 { sp_core::H160::from([byte; 20]) @@ -940,8 +959,8 @@ mod tests { fund_pool(asset_id, amount + fee); MerkleRepository::add_historic_poseidon_root::(KNOWN_ROOT); - let relayer_acct = 7u64; - crate::mock::mock_register_relayer(relayer_acct, evm(0xAA)); + let relayer_acct = acc(7); + crate::mock::mock_register_relayer(relayer_acct.clone(), evm(0xAA)); assert_ok!(UnshieldOperation::execute::( proof(), @@ -949,7 +968,7 @@ mod tests { nullifier(0x51), asset_id, amount, - 2u64, + acc(2), fee, [0u8; 32], FrameEncryptedMemo::default(), @@ -961,7 +980,7 @@ mod tests { fee ); assert_eq!( - crate::mock::mock_pending_fees_get(BLOCK_AUTHOR, asset_id), + crate::mock::mock_pending_fees_get(block_author(), asset_id), 0 ); }); @@ -984,7 +1003,7 @@ mod tests { nullifier(0x52), asset_id, amount, - 2u64, + acc(2), fee, [0u8; 32], FrameEncryptedMemo::default(), @@ -992,7 +1011,7 @@ mod tests { )); assert_eq!( - crate::mock::mock_pending_fees_get(BLOCK_AUTHOR, asset_id), + crate::mock::mock_pending_fees_get(block_author(), asset_id), fee ); }); @@ -1013,7 +1032,7 @@ mod tests { nullifier(0x53), asset_id, amount, - 2u64, + acc(2), fee, [0u8; 32], FrameEncryptedMemo::default(), @@ -1021,7 +1040,7 @@ mod tests { )); assert_eq!( - crate::mock::mock_pending_fees_get(BLOCK_AUTHOR, asset_id), + crate::mock::mock_pending_fees_get(block_author(), asset_id), fee ); }); diff --git a/frame/shielded-pool/src/storage.rs b/frame/shielded-pool/src/storage.rs index 662401ae..85e1e3e3 100644 --- a/frame/shielded-pool/src/storage.rs +++ b/frame/shielded-pool/src/storage.rs @@ -218,10 +218,11 @@ impl PoolBalanceRepository { mod tests { use super::*; use crate::{ - mock::{Test, new_test_ext}, + mock::{Test, acc, new_test_ext}, types::{AssetMetadata, Commitment, EncryptedMemo, MAX_ENCRYPTED_MEMO_SIZE, Nullifier}, }; use frame_support::{BoundedVec, pallet_prelude::ConstU32}; + use sp_runtime::AccountId32; // ── helpers ─────────────────────────────────────────────────────────────── @@ -237,14 +238,14 @@ mod tests { EncryptedMemo::from_bytes(&[0x01u8; MAX_ENCRYPTED_MEMO_SIZE as usize]).unwrap() } - fn make_asset_metadata(id: u32) -> AssetMetadata { + fn make_asset_metadata(id: u32) -> AssetMetadata { AssetMetadata::new( id, BoundedVec::try_from(b"Test".to_vec()).unwrap(), BoundedVec::try_from(b"TST".to_vec()).unwrap(), 18, 0u64, - 1u64, + acc(1), ) } From f00c613f951a06f2ecd5bd6a051aa0bcfb62ed2e Mon Sep 17 00:00:00 2001 From: nol4lej Date: Sun, 5 Jul 2026 16:00:04 -0400 Subject: [PATCH 06/18] fix(shielded-pool): error on unattributable relay fee instead of dropping it --- frame/shielded-pool/CHANGELOG.md | 5 ++ frame/shielded-pool/src/lib.rs | 3 + frame/shielded-pool/src/mock.rs | 12 +++- .../src/operations/private_transfer.rs | 34 ++++++++-- .../shielded-pool/src/operations/unshield.rs | 68 +++++++++++++++++-- 5 files changed, 109 insertions(+), 13 deletions(-) diff --git a/frame/shielded-pool/CHANGELOG.md b/frame/shielded-pool/CHANGELOG.md index 36436eb7..a6c34603 100644 --- a/frame/shielded-pool/CHANGELOG.md +++ b/frame/shielded-pool/CHANGELOG.md @@ -35,6 +35,11 @@ All notable changes to `pallet-shielded-pool` will be documented in this file. scheme Orbinum unifies (sr25519/ed25519/ECDSA/EVM, plus future ones like Solana) maps to a 32-byte account — so the strict binding covers them all. The test mock was migrated from `u64` to `AccountId32` so the binding is exercised end-to-end. +- A non-zero relay fee that cannot be attributed to any recipient (no resolved + relayer and no block author) now errors with `FeeRecipientUnavailable` instead + of silently skipping accumulation and stranding the fee tokens in the pool. This + is unreachable under normal operation (a transaction always executes inside a + block, so a block author exists) but fails loudly on a misconfigured provider. ## [0.8.3] - 2026-07-04 diff --git a/frame/shielded-pool/src/lib.rs b/frame/shielded-pool/src/lib.rs index 2ad89591..63a59df5 100644 --- a/frame/shielded-pool/src/lib.rs +++ b/frame/shielded-pool/src/lib.rs @@ -453,6 +453,9 @@ pub mod pallet { CommitmentNotFound, /// Pending validator fees are less than the requested claim amount InsufficientPendingFees, + /// A non-zero fee could not be attributed to any recipient (no resolved + /// relayer and no block author). The fee tokens would otherwise be stranded. + FeeRecipientUnavailable, } // ======================================================================== diff --git a/frame/shielded-pool/src/mock.rs b/frame/shielded-pool/src/mock.rs index a3d700d9..f692b77b 100644 --- a/frame/shielded-pool/src/mock.rs +++ b/frame/shielded-pool/src/mock.rs @@ -194,6 +194,12 @@ pub fn mock_register_relayer(who: AccountId, addr: sp_core::H160) { sp_io::storage::set(&key, &who.encode()); } +/// Force `block_author()` to return `None` (default is `Some(acc(1))`), to test +/// the no-fee-recipient path. +pub fn mock_clear_block_author() { + sp_io::storage::set(b"mock:no_author", &[1u8]); +} + impl pallet_relayer::RelayerInterface for MockRelayer { type AccountId = AccountId; @@ -217,7 +223,11 @@ impl pallet_relayer::RelayerInterface for MockRelayer { } fn block_author() -> Option { - Some(acc(1)) + if sp_io::storage::get(b"mock:no_author").is_some() { + None + } else { + Some(acc(1)) + } } fn accumulate_relay_fee(author: &AccountId, asset_id: u32, amount: u128) { diff --git a/frame/shielded-pool/src/operations/private_transfer.rs b/frame/shielded-pool/src/operations/private_transfer.rs index 9edd5fea..a2732be4 100644 --- a/frame/shielded-pool/src/operations/private_transfer.rs +++ b/frame/shielded-pool/src/operations/private_transfer.rs @@ -112,12 +112,11 @@ impl PrivateTransferOperation { } if fee > >::Balance::zero() { - let fee_recipient: Option = relayer_evm + let recipient_account = relayer_evm .and_then(|addr| T::Relayer::resolve_relayer(&addr)) - .or_else(T::Relayer::block_author); - if let Some(recipient_account) = fee_recipient { - T::Relayer::accumulate_relay_fee(&recipient_account, asset_id, fee_u128); - } + .or_else(T::Relayer::block_author) + .ok_or(Error::::FeeRecipientUnavailable)?; + T::Relayer::accumulate_relay_fee(&recipient_account, asset_id, fee_u128); } Pallet::::deposit_event(Event::NullifiersSpent { @@ -150,7 +149,7 @@ mod tests { storage::{CommitmentRepository, MerkleRepository, NullifierRepository}, types::{Commitment, EncryptedMemo, MAX_ENCRYPTED_MEMO_SIZE, Nullifier}, }; - use frame_support::{assert_noop, assert_ok}; + use frame_support::{assert_err, assert_noop, assert_ok}; // ── helpers ─────────────────────────────────────────────────────────────── @@ -704,4 +703,27 @@ mod tests { assert_eq!(crate::mock::mock_pending_fees_get(acc(1), 0u32), 20); }); } + + /// A non-zero fee with no resolvable recipient errors, mirroring unshield. + #[test] + fn transfer_nonzero_fee_without_recipient_errors() { + new_test_ext().execute_with(|| { + MerkleRepository::add_historic_poseidon_root::(KNOWN_ROOT); + crate::mock::mock_clear_block_author(); + + assert_err!( + PrivateTransferOperation::execute::( + proof(), + KNOWN_ROOT, + nullifiers_of(&[0x80]), + commitments_of(&[0x81]), + memos_of(1), + 0u32, + 25u128, + None, + ), + Error::::FeeRecipientUnavailable + ); + }); + } } diff --git a/frame/shielded-pool/src/operations/unshield.rs b/frame/shielded-pool/src/operations/unshield.rs index 05342237..1c6136ed 100644 --- a/frame/shielded-pool/src/operations/unshield.rs +++ b/frame/shielded-pool/src/operations/unshield.rs @@ -129,12 +129,11 @@ impl UnshieldOperation { )?; if fee > >::Balance::zero() { - let fee_recipient: Option = relayer_evm + let recipient_account = relayer_evm .and_then(|addr| T::Relayer::resolve_relayer(&addr)) - .or_else(T::Relayer::block_author); - if let Some(recipient_account) = fee_recipient { - T::Relayer::accumulate_relay_fee(&recipient_account, asset_id, fee_u128); - } + .or_else(T::Relayer::block_author) + .ok_or(Error::::FeeRecipientUnavailable)?; + T::Relayer::accumulate_relay_fee(&recipient_account, asset_id, fee_u128); } // Decrement only `amount`: the `fee` tokens stay physically in the pool as @@ -212,7 +211,7 @@ mod tests { }, types::{Commitment, Nullifier}, }; - use frame_support::{assert_noop, assert_ok, traits::Currency}; + use frame_support::{assert_err, assert_noop, assert_ok, traits::Currency}; use sp_runtime::AccountId32; // ── helpers ────────────────────────────────────────────────────────────── @@ -1045,4 +1044,61 @@ mod tests { ); }); } + + /// A non-zero fee with no resolvable recipient (no relayer, no block author) + /// errors instead of stranding the fee tokens in the pool. + #[test] + fn unshield_nonzero_fee_without_recipient_errors() { + new_test_ext().execute_with(|| { + let asset_id = setup_asset(); + let (amount, fee) = (500u128, 50u128); + fund_pool(asset_id, amount + fee); + MerkleRepository::add_historic_poseidon_root::(KNOWN_ROOT); + crate::mock::mock_clear_block_author(); + + // assert_err (not assert_noop): the fee attribution runs after currency + // effects; in a real extrinsic the dispatch rolls those back on Err. Here + // we assert the error itself — the transactional rollback is Substrate's. + assert_err!( + UnshieldOperation::execute::( + proof(), + KNOWN_ROOT, + nullifier(0x60), + asset_id, + amount, + acc(2), + fee, + [0u8; 32], + FrameEncryptedMemo::default(), + None, + ), + crate::pallet::Error::::FeeRecipientUnavailable + ); + }); + } + + /// A zero fee with no recipient is fine — nothing to attribute. + #[test] + fn unshield_zero_fee_without_recipient_ok() { + new_test_ext().execute_with(|| { + let asset_id = setup_asset(); + let amount = 500u128; + fund_pool(asset_id, amount); + MerkleRepository::add_historic_poseidon_root::(KNOWN_ROOT); + crate::mock::mock_clear_block_author(); + + assert_ok!(UnshieldOperation::execute::( + proof(), + KNOWN_ROOT, + nullifier(0x61), + asset_id, + amount, + acc(2), + 0u128, + [0u8; 32], + FrameEncryptedMemo::default(), + None, + )); + }); + } } From 5610c10b123fdb94a3454450c662887c31d36a1e Mon Sep 17 00:00:00 2001 From: nol4lej Date: Sun, 5 Jul 2026 16:10:29 -0400 Subject: [PATCH 07/18] perf(shielded-pool): bound unsigned transaction pool longevity --- frame/shielded-pool/CHANGELOG.md | 3 ++ frame/shielded-pool/src/validate_unsigned.rs | 43 +++++++++++++++++--- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/frame/shielded-pool/CHANGELOG.md b/frame/shielded-pool/CHANGELOG.md index a6c34603..19ff7623 100644 --- a/frame/shielded-pool/CHANGELOG.md +++ b/frame/shielded-pool/CHANGELOG.md @@ -40,6 +40,9 @@ All notable changes to `pallet-shielded-pool` will be documented in this file. of silently skipping accumulation and stranding the fee tokens in the pool. This is unreachable under normal operation (a transaction always executes inside a block, so a block author exists) but fails loudly on a misconfigured provider. +- Unsigned `unshield`/`private_transfer` transactions now carry a bounded pool + longevity (64 blocks) instead of `TransactionLongevity::MAX`, so a transaction + that is never included does not linger in the pool indefinitely. ## [0.8.3] - 2026-07-04 diff --git a/frame/shielded-pool/src/validate_unsigned.rs b/frame/shielded-pool/src/validate_unsigned.rs index 52aef531..934985c5 100644 --- a/frame/shielded-pool/src/validate_unsigned.rs +++ b/frame/shielded-pool/src/validate_unsigned.rs @@ -16,11 +16,13 @@ use pallet_relayer::RelayerInterface as _; use parity_scale_codec::Encode; use sp_runtime::{ SaturatedConversion, - transaction_validity::{ - InvalidTransaction, TransactionLongevity, TransactionValidity, ValidTransaction, - }, + transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction}, }; +/// How long an unsigned transaction stays valid in the pool, in blocks. Bounded +/// so a transaction that never gets included does not linger indefinitely. +const TX_LONGEVITY: u64 = 64; + /// Validate an incoming `private_transfer` unsigned transaction. pub fn validate_private_transfer( merkle_root: &Hash, @@ -69,7 +71,7 @@ pub fn validate_private_transfer( ValidTransaction::with_tag_prefix("ShieldedPoolTransfer") .priority((*fee).saturated_into()) - .longevity(TransactionLongevity::MAX) + .longevity(TX_LONGEVITY) .and_provides(provides) .propagate(true) .build() @@ -114,7 +116,7 @@ pub fn validate_unshield( // wins at equal fee). ValidTransaction::with_tag_prefix("ShieldedPoolUnshield") .priority((*fee).saturated_into()) - .longevity(TransactionLongevity::MAX) + .longevity(TX_LONGEVITY) .and_provides([nullifier.encode(), relayer.encode()]) .propagate(true) .build() @@ -550,4 +552,35 @@ mod tests { assert_ne!(a.provides, b.provides); }); } + + /// Unsigned transactions carry a bounded longevity (not `MAX`), so an + /// un-included transaction does not persist in the pool indefinitely. + #[test] + fn unsigned_txs_have_bounded_longevity() { + new_test_ext().execute_with(|| { + MerkleRepository::add_historic_poseidon_root::(KNOWN_ROOT); + PoolBalanceRepository::set_asset_balance::(0, 1000u128); + + let t = validate_private_transfer::( + &KNOWN_ROOT, + &nullifiers_of(&[0x90]), + &10u128, + &None, + ) + .unwrap(); + assert_eq!(t.longevity, TX_LONGEVITY); + assert!(t.longevity < sp_runtime::transaction_validity::TransactionLongevity::MAX); + + let u = validate_unshield::( + &KNOWN_ROOT, + &make_nullifier(0x91), + &0u32, + &100u128, + &10u128, + &None, + ) + .unwrap(); + assert_eq!(u.longevity, TX_LONGEVITY); + }); + } } From 5437cc76f4db5fc3051fc7dcc5eb1a7fc60cc2d7 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Sun, 5 Jul 2026 20:28:40 -0400 Subject: [PATCH 08/18] docs(shielded-pool): clarify unverify_asset is a total freeze kill-switch --- frame/shielded-pool/CHANGELOG.md | 7 +++++ frame/shielded-pool/src/lib.rs | 9 ++++-- .../shielded-pool/src/operations/unshield.rs | 30 +++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/frame/shielded-pool/CHANGELOG.md b/frame/shielded-pool/CHANGELOG.md index 19ff7623..044d514d 100644 --- a/frame/shielded-pool/CHANGELOG.md +++ b/frame/shielded-pool/CHANGELOG.md @@ -44,6 +44,13 @@ All notable changes to `pallet-shielded-pool` will be documented in this file. longevity (64 blocks) instead of `TransactionLongevity::MAX`, so a transaction that is never included does not linger in the pool indefinitely. +### Fixed + +- Corrected the `unverify_asset` doc-comment: it claimed existing notes could + still be spent, but unverifying an asset freezes both shields and unshields + (an intentional emergency kill-switch for a compromised asset). Behavior + unchanged; documentation now matches. + ## [0.8.3] - 2026-07-04 ### Security diff --git a/frame/shielded-pool/src/lib.rs b/frame/shielded-pool/src/lib.rs index 63a59df5..2f32bf45 100644 --- a/frame/shielded-pool/src/lib.rs +++ b/frame/shielded-pool/src/lib.rs @@ -729,10 +729,13 @@ pub mod pallet { crate::operations::assets::AssetOperation::verify::(asset_id) } - /// Unverify an asset, preventing its use in new transactions + /// Unverify an asset — an emergency freeze for a compromised asset. /// - /// Marks an asset as unverified. Existing private notes with this asset - /// can still be spent, but new shield operations are prevented. + /// Marks an asset as unverified. This freezes ALL activity for the asset: + /// both new shields AND unshields of existing notes require `is_verified`, + /// so governance can halt inflows and outflows if the asset is compromised. + /// Note: this also traps legitimate notes until the asset is re-verified — + /// a deliberate trade-off for a fund-holding pool. /// /// # Arguments /// * `origin` - Must be root (governance) diff --git a/frame/shielded-pool/src/operations/unshield.rs b/frame/shielded-pool/src/operations/unshield.rs index 1c6136ed..9a12e2e2 100644 --- a/frame/shielded-pool/src/operations/unshield.rs +++ b/frame/shielded-pool/src/operations/unshield.rs @@ -1101,4 +1101,34 @@ mod tests { )); }); } + + /// Unverifying an asset freezes existing notes too: an unshield of a + /// previously-verified asset fails once it is unverified (emergency kill-switch). + #[test] + fn unverifying_asset_freezes_existing_note_unshield() { + new_test_ext().execute_with(|| { + let asset_id = setup_asset(); // registered + verified + fund_pool(asset_id, 1_000u128); + MerkleRepository::add_historic_poseidon_root::(KNOWN_ROOT); + + // Freeze the asset via governance. + AssetOperation::unverify::(asset_id).unwrap(); + + assert_noop!( + UnshieldOperation::execute::( + proof(), + KNOWN_ROOT, + nullifier(0x70), + asset_id, + 100u128, + acc(2), + 0u128, + [0u8; 32], + FrameEncryptedMemo::default(), + None, + ), + crate::pallet::Error::::AssetNotVerified + ); + }); + } } From 6264bb675821b5bafae753c526df10dc5cb2a0db Mon Sep 17 00:00:00 2001 From: nol4lej Date: Sun, 5 Jul 2026 20:56:23 -0400 Subject: [PATCH 09/18] fix(shielded-pool): gate private_transfer on registered, verified asset --- frame/shielded-pool/CHANGELOG.md | 5 ++ .../src/operations/private_transfer.rs | 53 ++++++++++++++++++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/frame/shielded-pool/CHANGELOG.md b/frame/shielded-pool/CHANGELOG.md index 044d514d..0c28bc34 100644 --- a/frame/shielded-pool/CHANGELOG.md +++ b/frame/shielded-pool/CHANGELOG.md @@ -6,6 +6,11 @@ All notable changes to `pallet-shielded-pool` will be documented in this file. ### Security +- `private_transfer` now rejects an unregistered or unverified asset before any + effect (`InvalidAssetId` / `AssetNotVerified`), matching `shield` and + `unshield`. It was the only path that skipped the asset state-machine, so + value already shielded under a frozen asset could still be moved and split + in-pool; the emergency freeze (`unverify_asset`) now covers every path. - Hardened the pool-balance ledger invariant (`PoolBalancePerAsset == physical pool balance` for the native asset). The accounting was already correct; added a `try_state` hook (feature `try-runtime`) that enforces it every block, a diff --git a/frame/shielded-pool/src/operations/private_transfer.rs b/frame/shielded-pool/src/operations/private_transfer.rs index a2732be4..0261f5ea 100644 --- a/frame/shielded-pool/src/operations/private_transfer.rs +++ b/frame/shielded-pool/src/operations/private_transfer.rs @@ -1,7 +1,7 @@ use crate::{ merkle::MerkleTreeService, pallet::{Config, Error, Event, Pallet}, - storage::{CommitmentRepository, MerkleRepository, NullifierRepository}, + storage::{AssetRepository, CommitmentRepository, MerkleRepository, NullifierRepository}, types::{Commitment, EncryptedMemo, MAX_ENCRYPTED_MEMO_SIZE, Nullifier}, }; use frame_support::{BoundedVec, pallet_prelude::*, traits::Currency}; @@ -24,6 +24,9 @@ impl PrivateTransferOperation { fee: <::Currency as Currency<::AccountId>>::Balance, relayer_evm: Option, ) -> DispatchResult { + let asset = AssetRepository::get_asset::(asset_id).ok_or(Error::::InvalidAssetId)?; + ensure!(asset.is_verified, Error::::AssetNotVerified); + ensure!( nullifiers.len() == commitments.len(), Error::::TooManyInputsOrOutputs @@ -726,4 +729,52 @@ mod tests { ); }); } + + // ── asset state-machine gate ───────────────────────────────────────────── + + /// Unverifying an asset freezes in-pool transfers too, mirroring the + /// shield/unshield freeze (no path escapes the emergency kill-switch). + #[test] + fn transfer_frozen_asset_fails() { + new_test_ext().execute_with(|| { + MerkleRepository::add_historic_poseidon_root::(KNOWN_ROOT); + crate::operations::assets::AssetOperation::unverify::(0u32).unwrap(); + + assert_noop!( + PrivateTransferOperation::execute::( + proof(), + KNOWN_ROOT, + nullifiers_of(&[0x90]), + commitments_of(&[0x91]), + memos_of(1), + 0u32, + 0u128, + None, + ), + Error::::AssetNotVerified + ); + }); + } + + /// A transfer on an unregistered asset id is rejected before any effect. + #[test] + fn transfer_unknown_asset_fails() { + new_test_ext().execute_with(|| { + MerkleRepository::add_historic_poseidon_root::(KNOWN_ROOT); + + assert_noop!( + PrivateTransferOperation::execute::( + proof(), + KNOWN_ROOT, + nullifiers_of(&[0x92]), + commitments_of(&[0x93]), + memos_of(1), + 999u32, + 0u128, + None, + ), + Error::::InvalidAssetId + ); + }); + } } From f727869036447c36ea2e924f270029b0eb4e840a Mon Sep 17 00:00:00 2001 From: nol4lej Date: Sun, 5 Jul 2026 21:33:08 -0400 Subject: [PATCH 10/18] fix(shielded-pool): use benchmarked weight and reject empty shield_batch --- frame/shielded-pool/CHANGELOG.md | 12 ++++++++ frame/shielded-pool/src/benchmarking.rs | 24 +++++++++------ frame/shielded-pool/src/lib.rs | 10 +++++-- .../src/operations/private_transfer.rs | 20 +++++++++++++ frame/shielded-pool/src/operations/shield.rs | 29 +++++++++++++++++++ frame/shielded-pool/src/weights.rs | 18 ++++++++++-- 6 files changed, 98 insertions(+), 15 deletions(-) diff --git a/frame/shielded-pool/CHANGELOG.md b/frame/shielded-pool/CHANGELOG.md index 0c28bc34..432c4880 100644 --- a/frame/shielded-pool/CHANGELOG.md +++ b/frame/shielded-pool/CHANGELOG.md @@ -11,6 +11,18 @@ All notable changes to `pallet-shielded-pool` will be documented in this file. `unshield`. It was the only path that skipped the asset state-machine, so value already shielded under a frozen asset could still be moved and split in-pool; the emergency freeze (`unverify_asset`) now covers every path. +- `private_transfer` weight is now parameterized by the number of outputs. It + inserts up to two Merkle leaves (a 2-in/2-out transfer) but was charged a flat + weight benchmarked for a single leaf, under-pricing the second insert (~20 + extra Poseidon hashes plus storage) and letting an attacker fill blocks past + the metered limit. The benchmark now sweeps `n` outputs and the extrinsic + charges `private_transfer(commitments.len())`. (Weights carry an interim + upper-bound placeholder for the extra leaf; regenerate on the benchmark VPS.) +- `shield_batch` now uses its benchmarked `shield_batch(n)` weight and rejects an + empty batch with `EmptyBatch`. It previously used an ad-hoc `shield() * n * 0.8` + weight with no fixed base term, which evaluated to zero for an empty batch — + a free-to-submit signed spam vector — and mispriced small batches versus the + measured curve. - Hardened the pool-balance ledger invariant (`PoolBalancePerAsset == physical pool balance` for the native asset). The accounting was already correct; added a `try_state` hook (feature `try-runtime`) that enforces it every block, a diff --git a/frame/shielded-pool/src/benchmarking.rs b/frame/shielded-pool/src/benchmarking.rs index c80df74e..fbf08552 100644 --- a/frame/shielded-pool/src/benchmarking.rs +++ b/frame/shielded-pool/src/benchmarking.rs @@ -94,7 +94,7 @@ mod benchmarks { } #[benchmark] - fn private_transfer() { + fn private_transfer(n: Linear<1, 2>) { let (_caller, _) = setup_benchmark_env::(); let merkle_root = [1u8; 32]; @@ -102,15 +102,21 @@ mod benchmarks { HistoricPoseidonRoots::::insert(merkle_root, true); let proof: BoundedVec> = vec![0u8; 128].try_into().unwrap(); - let nullifiers: BoundedVec> = - vec![Nullifier([2u8; 32])].try_into().unwrap(); - let commitments: BoundedVec> = - vec![Commitment([3u8; 32])].try_into().unwrap(); - let memo_bytes = vec![0u8; MAX_ENCRYPTED_MEMO_SIZE as usize]; + + // n inputs/outputs: the leaf-insertion loop runs n times (worst case n=2). + let mut nulls = Vec::new(); + let mut comms = Vec::new(); + let mut memos = Vec::new(); + for i in 0..n { + nulls.push(Nullifier([(0x20 + i) as u8; 32])); + comms.push(Commitment([(0x30 + i) as u8; 32])); + let memo_bytes = vec![0u8; MAX_ENCRYPTED_MEMO_SIZE as usize]; + memos.push(FrameEncryptedMemo(memo_bytes.try_into().unwrap())); + } + let nullifiers: BoundedVec> = nulls.try_into().unwrap(); + let commitments: BoundedVec> = comms.try_into().unwrap(); let encrypted_memos: BoundedVec> = - vec![FrameEncryptedMemo(memo_bytes.try_into().unwrap())] - .try_into() - .unwrap(); + memos.try_into().unwrap(); let asset_id = 0u32; // Must be >= T::Relayer::min_relay_fee() to pass the FeeTooLow check. diff --git a/frame/shielded-pool/src/lib.rs b/frame/shielded-pool/src/lib.rs index 2f32bf45..2539f00e 100644 --- a/frame/shielded-pool/src/lib.rs +++ b/frame/shielded-pool/src/lib.rs @@ -456,6 +456,8 @@ pub mod pallet { /// A non-zero fee could not be attributed to any recipient (no resolved /// relayer and no block author). The fee tokens would otherwise be stranded. FeeRecipientUnavailable, + /// Batch operation submitted with no operations. + EmptyBatch, } // ======================================================================== @@ -518,15 +520,16 @@ pub mod pallet { /// /// # Errors /// * Same as `shield()` for any individual operation + /// * `EmptyBatch` - Batch submitted with no operations /// * `TooManyOperations` - Batch exceeds maximum size (20) /// /// # Events /// * `Shielded` - Emitted for each successful shield in the batch /// /// # Weight - /// Approximately `N * shield_weight * 0.8` (20% batch discount) + /// Benchmarked per operation count via `shield_batch(n)`. #[pallet::call_index(12)] - #[pallet::weight(T::WeightInfo::shield().saturating_mul(operations.len() as u64).saturating_mul(4) / 5)] + #[pallet::weight(T::WeightInfo::shield_batch(operations.len() as u32))] pub fn shield_batch( origin: OriginFor, operations: BoundedVec< @@ -535,6 +538,7 @@ pub mod pallet { >, ) -> DispatchResult { let who = ensure_signed(origin)?; + ensure!(!operations.is_empty(), Error::::EmptyBatch); // Process each shield operation for (asset_id, amount, commitment, encrypted_memo) in operations.into_iter() { @@ -576,7 +580,7 @@ pub mod pallet { /// * `InvalidMemoSize` - Any encrypted memo is not exactly 168 bytes /// * `MemoCommitmentMismatch` - Number of memos does not match number of commitments #[pallet::call_index(1)] - #[pallet::weight(T::WeightInfo::private_transfer())] + #[pallet::weight(T::WeightInfo::private_transfer(commitments.len() as u32))] #[allow(clippy::too_many_arguments)] pub fn private_transfer( origin: OriginFor, diff --git a/frame/shielded-pool/src/operations/private_transfer.rs b/frame/shielded-pool/src/operations/private_transfer.rs index 0261f5ea..fe57c722 100644 --- a/frame/shielded-pool/src/operations/private_transfer.rs +++ b/frame/shielded-pool/src/operations/private_transfer.rs @@ -777,4 +777,24 @@ mod tests { ); }); } + + // ── weight scales with outputs ─────────────────────────────────────────── + + /// A 2-output transfer inserts two leaves and must be weighted heavier than a + /// 1-output one (guards against the flat weight that under-priced the second + /// insert). + #[test] + fn private_transfer_weight_scales_with_outputs() { + use crate::weights::WeightInfo; + let one = <() as WeightInfo>::private_transfer(1); + let two = <() as WeightInfo>::private_transfer(2); + assert!( + two.ref_time() > one.ref_time(), + "two outputs must cost more ref_time than one" + ); + assert!( + two.proof_size() > one.proof_size(), + "two outputs must cost more proof_size than one" + ); + } } diff --git a/frame/shielded-pool/src/operations/shield.rs b/frame/shielded-pool/src/operations/shield.rs index ab4233c0..b3bb280c 100644 --- a/frame/shielded-pool/src/operations/shield.rs +++ b/frame/shielded-pool/src/operations/shield.rs @@ -356,4 +356,33 @@ mod tests { ); }); } + + // ── batch guard ────────────────────────────────────────────────────────── + + /// An empty `shield_batch` is rejected instead of dispatching at zero weight. + #[test] + fn shield_batch_empty_fails() { + use crate::mock::{RuntimeOrigin, ShieldedPool}; + new_test_ext().execute_with(|| { + let empty = frame_support::BoundedVec::default(); + assert_noop!( + ShieldedPool::shield_batch(RuntimeOrigin::signed(acc(1)), empty), + crate::pallet::Error::::EmptyBatch + ); + }); + } + + /// The benchmarked `shield_batch(n)` weight has a non-zero base and scales + /// with n (guards against the old ad-hoc `shield()*n*0.8` that hit zero at n=0). + #[test] + fn shield_batch_weight_has_base_and_scales() { + use crate::weights::WeightInfo; + let zero = <() as WeightInfo>::shield_batch(0); + let one = <() as WeightInfo>::shield_batch(1); + assert!( + zero.ref_time() > 0, + "empty batch must still carry a base weight" + ); + assert!(one.ref_time() > zero.ref_time(), "weight must scale with n"); + } } diff --git a/frame/shielded-pool/src/weights.rs b/frame/shielded-pool/src/weights.rs index 1a5cfaec..d9c7163b 100644 --- a/frame/shielded-pool/src/weights.rs +++ b/frame/shielded-pool/src/weights.rs @@ -41,7 +41,7 @@ use core::marker::PhantomData; pub trait WeightInfo { fn shield() -> Weight; fn shield_batch(n: u32, ) -> Weight; - fn private_transfer() -> Weight; + fn private_transfer(n: u32, ) -> Weight; fn unshield() -> Weight; fn register_asset() -> Weight; fn verify_asset() -> Weight; @@ -180,14 +180,20 @@ impl WeightInfo for SubstrateWeight { /// Proof: `ShieldedPool::MerkleLeaves` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) /// Storage: `ShieldedPool::CommitmentToLeafIndex` (r:0 w:1) /// Proof: `ShieldedPool::CommitmentToLeafIndex` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) - fn private_transfer() -> Weight { + /// The range of component `n` (outputs/leaves inserted) is `[1, 2]`. + fn private_transfer(n: u32, ) -> Weight { // Proof Size summary in bytes: // Measured: `1016` // Estimated: `4687` // Minimum execution time: 696_789_000 picoseconds. + // Base measures one leaf insertion; each extra output adds a full insert + // (~20 Poseidon hashes + storage). Per-leaf cost taken from shield_batch's + // benchmarked per-item figure as a safe upper bound until re-benchmarked. Weight::from_parts(707_598_000, 4687) + .saturating_add(Weight::from_parts(677_433_214, 2701).saturating_mul(n.saturating_sub(1).into())) .saturating_add(T::DbWeight::get().reads(16_u64)) .saturating_add(T::DbWeight::get().writes(13_u64)) + .saturating_add(T::DbWeight::get().writes((7_u64).saturating_mul(n.saturating_sub(1).into()))) } /// Storage: `ShieldedPool::Assets` (r:1 w:0) /// Proof: `ShieldedPool::Assets` (`max_values`: None, `max_size`: Some(166), added: 2641, mode: `MaxEncodedLen`) @@ -456,14 +462,20 @@ impl WeightInfo for () { /// Proof: `ShieldedPool::MerkleLeaves` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) /// Storage: `ShieldedPool::CommitmentToLeafIndex` (r:0 w:1) /// Proof: `ShieldedPool::CommitmentToLeafIndex` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) - fn private_transfer() -> Weight { + /// The range of component `n` (outputs/leaves inserted) is `[1, 2]`. + fn private_transfer(n: u32, ) -> Weight { // Proof Size summary in bytes: // Measured: `1016` // Estimated: `4687` // Minimum execution time: 696_789_000 picoseconds. + // Base measures one leaf insertion; each extra output adds a full insert + // (~20 Poseidon hashes + storage). Per-leaf cost taken from shield_batch's + // benchmarked per-item figure as a safe upper bound until re-benchmarked. Weight::from_parts(707_598_000, 4687) + .saturating_add(Weight::from_parts(677_433_214, 2701).saturating_mul(n.saturating_sub(1).into())) .saturating_add(RocksDbWeight::get().reads(16_u64)) .saturating_add(RocksDbWeight::get().writes(13_u64)) + .saturating_add(RocksDbWeight::get().writes((7_u64).saturating_mul(n.saturating_sub(1).into()))) } /// Storage: `ShieldedPool::Assets` (r:1 w:0) /// Proof: `ShieldedPool::Assets` (`max_values`: None, `max_size`: Some(166), added: 2641, mode: `MaxEncodedLen`) From dbdc0e512283383f8d2eb3e1387d88fc6e9113cc Mon Sep 17 00:00:00 2001 From: nol4lej Date: Sun, 5 Jul 2026 21:39:47 -0400 Subject: [PATCH 11/18] fix(shielded-pool): harden the historic Merkle-root window --- frame/shielded-pool/CHANGELOG.md | 6 +++ frame/shielded-pool/src/lib.rs | 6 +++ frame/shielded-pool/src/merkle.rs | 87 +++++++++++++++++++++++++++++-- 3 files changed, 94 insertions(+), 5 deletions(-) diff --git a/frame/shielded-pool/CHANGELOG.md b/frame/shielded-pool/CHANGELOG.md index 432c4880..f16a39a5 100644 --- a/frame/shielded-pool/CHANGELOG.md +++ b/frame/shielded-pool/CHANGELOG.md @@ -23,6 +23,12 @@ All notable changes to `pallet-shielded-pool` will be documented in this file. weight with no fixed base term, which evaluated to zero for an empty batch — a free-to-submit signed spam vector — and mispriced small batches versus the measured curve. +- Hardened the historic Merkle-root window. `integrity_test` now asserts + `MaxHistoricRoots > 0` (a zero window would let the known-root map grow + unbounded while accepting every root forever). Root insertion is now atomic — + the order vector is pushed before the map is marked, so the two can never + desync — and eviction keeps a root known while any duplicate copy remains in + the window. - Hardened the pool-balance ledger invariant (`PoolBalancePerAsset == physical pool balance` for the native asset). The accounting was already correct; added a `try_state` hook (feature `try-runtime`) that enforces it every block, a diff --git a/frame/shielded-pool/src/lib.rs b/frame/shielded-pool/src/lib.rs index 2539f00e..fba934eb 100644 --- a/frame/shielded-pool/src/lib.rs +++ b/frame/shielded-pool/src/lib.rs @@ -287,6 +287,12 @@ pub mod pallet { crate::types::MAX_TREE_DEPTH, "MaxTreeDepth config must equal the fixed tree depth (MAX_TREE_DEPTH)" ); + + assert!( + T::MaxHistoricRoots::get() > 0, + "MaxHistoricRoots must be non-zero, otherwise the historic-root map \ + grows unbounded and the root window never evicts" + ); } /// Ledger-solvency invariant: the tracked native-asset pool balance must diff --git a/frame/shielded-pool/src/merkle.rs b/frame/shielded-pool/src/merkle.rs index ccdff320..441d8318 100644 --- a/frame/shielded-pool/src/merkle.rs +++ b/frame/shielded-pool/src/merkle.rs @@ -359,17 +359,20 @@ impl MerkleTreeService { Ok(index) } - fn add_poseidon_historic_root(poseidon_root: Hash) { + pub(crate) fn add_poseidon_historic_root(poseidon_root: Hash) { let mut order = MerkleRepository::get_historic_roots_order::(); if order.len() >= T::MaxHistoricRoots::get() as usize { if let Some(oldest_root) = order.first().copied() { - MerkleRepository::remove_poseidon_historic_root::(&oldest_root); order.remove(0); + if !order.contains(&oldest_root) { + MerkleRepository::remove_poseidon_historic_root::(&oldest_root); + } } } - MerkleRepository::add_historic_poseidon_root::(poseidon_root); - let _ = order.try_push(poseidon_root); - MerkleRepository::set_historic_roots_order::(order); + if order.try_push(poseidon_root).is_ok() { + MerkleRepository::add_historic_poseidon_root::(poseidon_root); + MerkleRepository::set_historic_roots_order::(order); + } } pub fn is_known_root(root: &Hash) -> bool { @@ -940,4 +943,78 @@ mod tests { // guard reads matches the frontier depth (20 levels). assert_eq!(2u32.saturating_pow(MAX_TREE_DEPTH), 1 << 20); } + + // ── historic-root window ────────────────────────────────────────────────── + + /// integrity_test rejects a zero root window (checked via the mock's non-zero + /// MaxHistoricRoots passing construction). + #[test] + fn integrity_test_accepts_nonzero_root_window() { + use frame_support::traits::Hooks; + new_test_ext().execute_with(|| { + assert!(::MaxHistoricRoots::get() > 0); + as Hooks>>::integrity_test(); + }); + } + + /// Once the window is full, the oldest root is evicted from both the order + /// vector and the known-root map, and the map/order stay in sync (SP-15). + #[test] + fn historic_root_window_evicts_oldest() { + new_test_ext().execute_with(|| { + let cap = ::MaxHistoricRoots::get(); + // Fill the window with `cap` distinct roots. + for i in 0..cap { + let mut root = [0u8; 32]; + root[..4].copy_from_slice(&i.to_le_bytes()); + MerkleTreeService::add_poseidon_historic_root::(root); + } + let mut oldest = [0u8; 32]; + oldest[..4].copy_from_slice(&0u32.to_le_bytes()); + assert!(MerkleTreeService::is_known_root::(&oldest)); + + // One more root evicts the oldest. + let mut newest = [0u8; 32]; + newest[..4].copy_from_slice(&cap.to_le_bytes()); + MerkleTreeService::add_poseidon_historic_root::(newest); + + assert!( + !MerkleTreeService::is_known_root::(&oldest), + "oldest must be evicted" + ); + assert!( + MerkleTreeService::is_known_root::(&newest), + "newest must be known" + ); + // Order vector holds exactly `cap` entries — no unbounded growth. + assert_eq!( + MerkleRepository::get_historic_roots_order::().len(), + cap as usize + ); + }); + } + + /// A duplicate root value is not removed from the map while another copy of it + /// still sits in the window (guards the map/order multiplicity mismatch). + #[test] + fn historic_root_duplicate_survives_partial_eviction() { + new_test_ext().execute_with(|| { + let dup = [0x77u8; 32]; + // Two copies of `dup` plus fillers spanning the window. + MerkleTreeService::add_poseidon_historic_root::(dup); + MerkleTreeService::add_poseidon_historic_root::(dup); + let cap = ::MaxHistoricRoots::get(); + for i in 0..(cap - 1) { + let mut root = [0u8; 32]; + root[..4].copy_from_slice(&i.to_le_bytes()); + root[31] = 1; // distinct namespace from `dup` + MerkleTreeService::add_poseidon_historic_root::(root); + } + // The first `dup` was evicted, but the second copy keeps it known. + assert!( + MerkleTreeService::is_known_root::(&dup), + "duplicate root must stay known while a copy remains in the window" + ); + }); + } } From d621ddc16eb9a813a61030dc7af12615dcd76999 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Sun, 5 Jul 2026 21:43:13 -0400 Subject: [PATCH 12/18] docs(shielded-pool): clarify claim_shielded_fees note ownership --- frame/shielded-pool/src/lib.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frame/shielded-pool/src/lib.rs b/frame/shielded-pool/src/lib.rs index fba934eb..931b8593 100644 --- a/frame/shielded-pool/src/lib.rs +++ b/frame/shielded-pool/src/lib.rs @@ -771,6 +771,11 @@ pub mod pallet { /// A value_proof ZK proof must be supplied, proving that `commitment` /// encodes exactly `amount` and `asset_id`. /// + /// The caller can only spend its own pending fees (keyed by the signed + /// origin), but the resulting note's owner is chosen by the caller — the + /// note need not belong to the validator. This is intentional: a relayer + /// may direct its own earned fees to any shielded recipient. + /// /// # Errors /// * `InvalidProof` - ZK proof verification failed or wrong length (expected 128 bytes) /// * `InvalidPublicSignals` - Signals mismatch commitment/amount/asset_id, or wrong length (expected 76 bytes) From 5232cd89ebe31e36a1d9b10e061c0b3a37b61df2 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Sun, 5 Jul 2026 21:48:58 -0400 Subject: [PATCH 13/18] fix(shielded-pool): guard asset registration against id collision --- frame/shielded-pool/src/genesis.rs | 7 ++++-- frame/shielded-pool/src/lib.rs | 2 ++ frame/shielded-pool/src/operations/assets.rs | 23 +++++++++++++++++++- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/frame/shielded-pool/src/genesis.rs b/frame/shielded-pool/src/genesis.rs index 81c6ebfb..d3fddb00 100644 --- a/frame/shielded-pool/src/genesis.rs +++ b/frame/shielded-pool/src/genesis.rs @@ -36,8 +36,11 @@ pub fn initialize_genesis(initial_root: Hash) { name: b"Orbinum Native Token" .to_vec() .try_into() - .unwrap_or_default(), - symbol: b"ORB".to_vec().try_into().unwrap_or_default(), + .expect("native asset name fits the metadata bound"), + symbol: b"ORB" + .to_vec() + .try_into() + .expect("native asset symbol fits the metadata bound"), decimals: 18, is_verified: true, contract_address: None, diff --git a/frame/shielded-pool/src/lib.rs b/frame/shielded-pool/src/lib.rs index 931b8593..1cde708d 100644 --- a/frame/shielded-pool/src/lib.rs +++ b/frame/shielded-pool/src/lib.rs @@ -464,6 +464,8 @@ pub mod pallet { FeeRecipientUnavailable, /// Batch operation submitted with no operations. EmptyBatch, + /// Asset id counter collided with an existing asset (would overwrite it). + AssetIdAlreadyExists, } // ======================================================================== diff --git a/frame/shielded-pool/src/operations/assets.rs b/frame/shielded-pool/src/operations/assets.rs index efba6919..1bfe01df 100644 --- a/frame/shielded-pool/src/operations/assets.rs +++ b/frame/shielded-pool/src/operations/assets.rs @@ -30,6 +30,11 @@ impl AssetOperation { Self::validate_metadata::(&name, &symbol, decimals)?; let asset_id = AssetRepository::increment_asset_id::(); + + ensure!( + !AssetRepository::exists::(asset_id), + Error::::AssetIdAlreadyExists + ); let current_block = frame_system::Pallet::::block_number(); let metadata = AssetMetadata { @@ -122,7 +127,7 @@ mod tests { pallet::Event as PalletEvent, storage::AssetRepository, }; - use frame_support::{assert_noop, assert_ok}; + use frame_support::{assert_err, assert_noop, assert_ok}; // ── helpers ────────────────────────────────────────────────────────────── @@ -373,4 +378,20 @@ mod tests { assert!(AssetOperation::get_asset_metadata::(42u32).is_none()); }); } + + // ── id-collision guard ──────────────────────────────────────────────────── + + /// Registration refuses to overwrite an existing asset if the id counter is + /// reset over a live slot (e.g. a genesis re-init). + #[test] + fn register_rejects_colliding_asset_id() { + new_test_ext().execute_with(|| { + let id = register_orb().unwrap(); + // Rewind the counter so the next register targets the same slot. + crate::pallet::NextAssetId::::put(id); + // increment_asset_id runs before the guard, so storage is touched; + // the dispatch layer rolls back — assert the error, not no-op. + assert_err!(register_orb(), Error::::AssetIdAlreadyExists); + }); + } } From aca7f8c2d5237ed84b8beec4a28098f601564c80 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Sun, 5 Jul 2026 21:55:30 -0400 Subject: [PATCH 14/18] fix(shielded-pool): make Note.asset_id u32 to match the circuit --- frame/shielded-pool/CHANGELOG.md | 9 +++++++++ frame/shielded-pool/src/types.rs | 19 ++++++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/frame/shielded-pool/CHANGELOG.md b/frame/shielded-pool/CHANGELOG.md index f16a39a5..60bb041a 100644 --- a/frame/shielded-pool/CHANGELOG.md +++ b/frame/shielded-pool/CHANGELOG.md @@ -29,6 +29,15 @@ All notable changes to `pallet-shielded-pool` will be documented in this file. the order vector is pushed before the map is marked, so the two can never desync — and eviction keeps a root known while any duplicate copy remains in the window. +- Asset registration now rejects an id collision with `AssetIdAlreadyExists` + instead of silently overwriting an existing asset, guarding against a genesis + re-init resetting the id counter over live slots. Genesis native-asset + metadata now uses `expect` instead of `unwrap_or_default`, so an over-long + name/symbol fails the build loudly rather than launching with an empty string. +- `Note.asset_id` is now `u32` (was `u64`), matching the on-chain registry and + the circuit's public signal (4-byte LE). The wider field serialized an + incompatible commitment preimage; the type is test-only today, but the mismatch + was a latent fund-loss footgun for any wallet building notes from it. - Hardened the pool-balance ledger invariant (`PoolBalancePerAsset == physical pool balance` for the native asset). The accounting was already correct; added a `try_state` hook (feature `try-runtime`) that enforces it every block, a diff --git a/frame/shielded-pool/src/types.rs b/frame/shielded-pool/src/types.rs index be3f8535..fcdac149 100644 --- a/frame/shielded-pool/src/types.rs +++ b/frame/shielded-pool/src/types.rs @@ -200,7 +200,7 @@ pub struct Note { value: u128, owner_pubkey: Hash, blinding: Hash, - asset_id: u64, + asset_id: u32, } impl Note { @@ -226,7 +226,7 @@ impl Note { value: u128, owner_pubkey: Hash, blinding: Hash, - asset_id: u64, + asset_id: u32, ) -> Result { let mut note = Self::new(value, owner_pubkey, blinding)?; note.asset_id = asset_id; @@ -242,7 +242,7 @@ impl Note { pub fn blinding(&self) -> &Hash { &self.blinding } - pub fn asset_id(&self) -> u64 { + pub fn asset_id(&self) -> u32 { self.asset_id } @@ -601,8 +601,17 @@ mod tests { #[test] fn note_to_bytes_has_correct_length() { let note = Note::new(100, [0x01u8; 32], [0x02u8; 32]).unwrap(); - // 16 (value u128) + 8 (asset_id u64) + 32 (pubkey) + 32 (blinding) = 88 - assert_eq!(note.to_bytes().len(), 88); + // 16 (value u128) + 4 (asset_id u32) + 32 (pubkey) + 32 (blinding) = 84 + assert_eq!(note.to_bytes().len(), 84); + } + + #[test] + fn note_asset_id_serializes_as_4_bytes() { + // asset_id must be 4 LE bytes to match the circuit's public signal + // (commitment[0..32] | value[32..40] | asset_id[40..44] | ...). + let note = Note::new_with_asset(100, [0x01u8; 32], [0x02u8; 32], 0x01020304).unwrap(); + let bytes = note.to_bytes(); + assert_eq!(&bytes[16..20], &0x01020304u32.to_le_bytes()); } // ── MerklePath ────────────────────────────────────────────────────────── From c598415a8b032952b7da78425ca914f45824587b Mon Sep 17 00:00:00 2001 From: nol4lej Date: Sun, 5 Jul 2026 22:26:46 -0400 Subject: [PATCH 15/18] fix(shielded-pool): register a relayer in fee-bearing benchmarks --- frame/shielded-pool/src/benchmarking.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/frame/shielded-pool/src/benchmarking.rs b/frame/shielded-pool/src/benchmarking.rs index fbf08552..3a89d672 100644 --- a/frame/shielded-pool/src/benchmarking.rs +++ b/frame/shielded-pool/src/benchmarking.rs @@ -18,15 +18,23 @@ extern crate alloc; use alloc::vec; #[benchmarks( - where T: pallet_zk_verifier::Config + where T: pallet_zk_verifier::Config + pallet_relayer::Config )] mod benchmarks { use super::*; use crate::FrameEncryptedMemo; use crate::pallet::{Assets, HistoricPoseidonRoots, NextAssetId, PoolBalancePerAsset}; use pallet_relayer::RelayerInterface; + use sp_core::H160; use sp_std::vec::Vec; + fn setup_relayer() -> H160 { + let addr = H160::from([0xAA; 20]); + let relayer: T::AccountId = account("relayer", 0, 0); + pallet_relayer::RelayerRegistry::::insert(addr, relayer); + addr + } + fn setup_benchmark_env() -> (T::AccountId, u32) { let caller: T::AccountId = whitelisted_caller(); let asset_id = 0u32; @@ -121,6 +129,7 @@ mod benchmarks { let asset_id = 0u32; // Must be >= T::Relayer::min_relay_fee() to pass the FeeTooLow check. let fee: BalanceOf = T::Relayer::min_relay_fee().saturated_into(); + let relayer = setup_relayer::(); #[extrinsic_call] private_transfer( @@ -132,7 +141,7 @@ mod benchmarks { encrypted_memos, asset_id, fee, - None, + Some(relayer), ); } @@ -157,6 +166,7 @@ mod benchmarks { // Must be >= T::Relayer::min_relay_fee() to pass the FeeTooLow check. let fee: BalanceOf = T::Relayer::min_relay_fee().saturated_into(); + let relayer = setup_relayer::(); #[extrinsic_call] unshield( @@ -170,7 +180,7 @@ mod benchmarks { fee, Hash::default(), // change_commitment: [0u8; 32] for total unshield Default::default(), // change_encrypted_memo: empty for total unshield - None, // relayer + Some(relayer), // relayer resolves the fee recipient ); } From d3aa39a49b7a5de67dc95912adfc89e46d3eb8a1 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Mon, 6 Jul 2026 02:37:48 +0000 Subject: [PATCH 16/18] chore: regenerate pallet weights on cpx62 VPS Regenerate weights for account-mapping, relayer, shielded-pool and zk-verifier. Replaces the interim placeholder for shielded-pool's parameterized private_transfer(n) with measured figures. --- frame/account-mapping/src/weights.rs | 130 +++++++++--------- frame/relayer/src/weights.rs | 42 +++--- frame/shielded-pool/src/weights.rs | 198 ++++++++++++++------------- frame/zk-verifier/src/weights.rs | 67 ++++----- 4 files changed, 221 insertions(+), 216 deletions(-) diff --git a/frame/account-mapping/src/weights.rs b/frame/account-mapping/src/weights.rs index c1d9b567..d9225b7b 100644 --- a/frame/account-mapping/src/weights.rs +++ b/frame/account-mapping/src/weights.rs @@ -2,7 +2,7 @@ //! Autogenerated weights for pallet_account_mapping //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 53.0.0 -//! DATE: 2026-07-04, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-07-06, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `ubuntu-32gb-fsn1-1`, CPU: `AMD EPYC-Genoa Processor` //! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 @@ -72,8 +72,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `19` // Estimated: `1504` - // Minimum execution time: 7_250_000 picoseconds. - Weight::from_parts(7_520_000, 1504) + // Minimum execution time: 8_450_000 picoseconds. + Weight::from_parts(8_900_000, 1504) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -93,8 +93,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `189` // Estimated: `3533` - // Minimum execution time: 14_800_000 picoseconds. - Weight::from_parts(15_250_000, 3533) + // Minimum execution time: 16_630_000 picoseconds. + Weight::from_parts(17_570_000, 3533) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } @@ -114,8 +114,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `61` // Estimated: `5728` - // Minimum execution time: 32_150_000 picoseconds. - Weight::from_parts(33_079_000, 5728) + // Minimum execution time: 37_300_000 picoseconds. + Weight::from_parts(38_810_000, 5728) .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } @@ -139,8 +139,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `257` // Estimated: `5728` - // Minimum execution time: 35_530_000 picoseconds. - Weight::from_parts(36_750_000, 5728) + // Minimum execution time: 41_600_000 picoseconds. + Weight::from_parts(42_370_000, 5728) .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(6_u64)) } @@ -164,8 +164,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `360` // Estimated: `6102` - // Minimum execution time: 58_210_000 picoseconds. - Weight::from_parts(59_360_000, 6102) + // Minimum execution time: 67_349_000 picoseconds. + Weight::from_parts(68_879_000, 6102) .saturating_add(T::DbWeight::get().reads(8_u64)) .saturating_add(T::DbWeight::get().writes(7_u64)) } @@ -185,8 +185,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `186` // Estimated: `4172` - // Minimum execution time: 17_520_000 picoseconds. - Weight::from_parts(18_400_000, 4172) + // Minimum execution time: 20_320_000 picoseconds. + Weight::from_parts(20_940_000, 4172) .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -206,8 +206,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `256` // Estimated: `4172` - // Minimum execution time: 18_780_000 picoseconds. - Weight::from_parts(19_170_000, 4172) + // Minimum execution time: 21_720_000 picoseconds. + Weight::from_parts(22_510_000, 4172) .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -233,8 +233,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `546` // Estimated: `5728` - // Minimum execution time: 103_490_000 picoseconds. - Weight::from_parts(107_560_000, 5728) + // Minimum execution time: 119_829_000 picoseconds. + Weight::from_parts(123_109_000, 5728) .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().writes(7_u64)) } @@ -258,8 +258,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `299` // Estimated: `5728` - // Minimum execution time: 61_740_000 picoseconds. - Weight::from_parts(63_240_000, 5728) + // Minimum execution time: 79_380_000 picoseconds. + Weight::from_parts(82_719_000, 5728) .saturating_add(T::DbWeight::get().reads(7_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } @@ -281,8 +281,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `286` // Estimated: `5728` - // Minimum execution time: 21_290_000 picoseconds. - Weight::from_parts(21_780_000, 5728) + // Minimum execution time: 24_779_000 picoseconds. + Weight::from_parts(25_740_000, 5728) .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } @@ -302,8 +302,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `186` // Estimated: `3546` - // Minimum execution time: 15_510_000 picoseconds. - Weight::from_parts(16_100_000, 3546) + // Minimum execution time: 18_130_000 picoseconds. + Weight::from_parts(18_920_000, 3546) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -323,8 +323,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `248` // Estimated: `3647` - // Minimum execution time: 56_150_000 picoseconds. - Weight::from_parts(60_390_000, 3647) + // Minimum execution time: 71_810_000 picoseconds. + Weight::from_parts(73_830_000, 3647) .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -344,8 +344,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `237` // Estimated: `4091` - // Minimum execution time: 18_669_000 picoseconds. - Weight::from_parts(19_100_000, 4091) + // Minimum execution time: 22_110_000 picoseconds. + Weight::from_parts(23_971_000, 4091) .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -365,8 +365,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `281` // Estimated: `4091` - // Minimum execution time: 20_020_000 picoseconds. - Weight::from_parts(20_920_000, 4091) + // Minimum execution time: 23_269_000 picoseconds. + Weight::from_parts(24_750_000, 4091) .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -392,8 +392,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `393` // Estimated: `5728` - // Minimum execution time: 126_160_000 picoseconds. - Weight::from_parts(130_300_000, 5728) + // Minimum execution time: 158_099_000 picoseconds. + Weight::from_parts(165_799_000, 5728) .saturating_add(T::DbWeight::get().reads(8_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) } @@ -413,8 +413,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `281` // Estimated: `4091` - // Minimum execution time: 20_990_000 picoseconds. - Weight::from_parts(21_680_000, 4091) + // Minimum execution time: 24_210_000 picoseconds. + Weight::from_parts(25_259_000, 4091) .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -434,8 +434,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `19` // Estimated: `1504` - // Minimum execution time: 7_250_000 picoseconds. - Weight::from_parts(7_520_000, 1504) + // Minimum execution time: 8_450_000 picoseconds. + Weight::from_parts(8_900_000, 1504) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -455,8 +455,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `189` // Estimated: `3533` - // Minimum execution time: 14_800_000 picoseconds. - Weight::from_parts(15_250_000, 3533) + // Minimum execution time: 16_630_000 picoseconds. + Weight::from_parts(17_570_000, 3533) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } @@ -476,8 +476,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `61` // Estimated: `5728` - // Minimum execution time: 32_150_000 picoseconds. - Weight::from_parts(33_079_000, 5728) + // Minimum execution time: 37_300_000 picoseconds. + Weight::from_parts(38_810_000, 5728) .saturating_add(RocksDbWeight::get().reads(6_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } @@ -501,8 +501,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `257` // Estimated: `5728` - // Minimum execution time: 35_530_000 picoseconds. - Weight::from_parts(36_750_000, 5728) + // Minimum execution time: 41_600_000 picoseconds. + Weight::from_parts(42_370_000, 5728) .saturating_add(RocksDbWeight::get().reads(6_u64)) .saturating_add(RocksDbWeight::get().writes(6_u64)) } @@ -526,8 +526,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `360` // Estimated: `6102` - // Minimum execution time: 58_210_000 picoseconds. - Weight::from_parts(59_360_000, 6102) + // Minimum execution time: 67_349_000 picoseconds. + Weight::from_parts(68_879_000, 6102) .saturating_add(RocksDbWeight::get().reads(8_u64)) .saturating_add(RocksDbWeight::get().writes(7_u64)) } @@ -547,8 +547,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `186` // Estimated: `4172` - // Minimum execution time: 17_520_000 picoseconds. - Weight::from_parts(18_400_000, 4172) + // Minimum execution time: 20_320_000 picoseconds. + Weight::from_parts(20_940_000, 4172) .saturating_add(RocksDbWeight::get().reads(6_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -568,8 +568,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `256` // Estimated: `4172` - // Minimum execution time: 18_780_000 picoseconds. - Weight::from_parts(19_170_000, 4172) + // Minimum execution time: 21_720_000 picoseconds. + Weight::from_parts(22_510_000, 4172) .saturating_add(RocksDbWeight::get().reads(6_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -595,8 +595,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `546` // Estimated: `5728` - // Minimum execution time: 103_490_000 picoseconds. - Weight::from_parts(107_560_000, 5728) + // Minimum execution time: 119_829_000 picoseconds. + Weight::from_parts(123_109_000, 5728) .saturating_add(RocksDbWeight::get().reads(9_u64)) .saturating_add(RocksDbWeight::get().writes(7_u64)) } @@ -620,8 +620,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `299` // Estimated: `5728` - // Minimum execution time: 61_740_000 picoseconds. - Weight::from_parts(63_240_000, 5728) + // Minimum execution time: 79_380_000 picoseconds. + Weight::from_parts(82_719_000, 5728) .saturating_add(RocksDbWeight::get().reads(7_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } @@ -643,8 +643,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `286` // Estimated: `5728` - // Minimum execution time: 21_290_000 picoseconds. - Weight::from_parts(21_780_000, 5728) + // Minimum execution time: 24_779_000 picoseconds. + Weight::from_parts(25_740_000, 5728) .saturating_add(RocksDbWeight::get().reads(6_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } @@ -664,8 +664,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `186` // Estimated: `3546` - // Minimum execution time: 15_510_000 picoseconds. - Weight::from_parts(16_100_000, 3546) + // Minimum execution time: 18_130_000 picoseconds. + Weight::from_parts(18_920_000, 3546) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -685,8 +685,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `248` // Estimated: `3647` - // Minimum execution time: 56_150_000 picoseconds. - Weight::from_parts(60_390_000, 3647) + // Minimum execution time: 71_810_000 picoseconds. + Weight::from_parts(73_830_000, 3647) .saturating_add(RocksDbWeight::get().reads(6_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -706,8 +706,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `237` // Estimated: `4091` - // Minimum execution time: 18_669_000 picoseconds. - Weight::from_parts(19_100_000, 4091) + // Minimum execution time: 22_110_000 picoseconds. + Weight::from_parts(23_971_000, 4091) .saturating_add(RocksDbWeight::get().reads(6_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -727,8 +727,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `281` // Estimated: `4091` - // Minimum execution time: 20_020_000 picoseconds. - Weight::from_parts(20_920_000, 4091) + // Minimum execution time: 23_269_000 picoseconds. + Weight::from_parts(24_750_000, 4091) .saturating_add(RocksDbWeight::get().reads(6_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -754,8 +754,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `393` // Estimated: `5728` - // Minimum execution time: 126_160_000 picoseconds. - Weight::from_parts(130_300_000, 5728) + // Minimum execution time: 158_099_000 picoseconds. + Weight::from_parts(165_799_000, 5728) .saturating_add(RocksDbWeight::get().reads(8_u64)) .saturating_add(RocksDbWeight::get().writes(5_u64)) } @@ -775,8 +775,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `281` // Estimated: `4091` - // Minimum execution time: 20_990_000 picoseconds. - Weight::from_parts(21_680_000, 4091) + // Minimum execution time: 24_210_000 picoseconds. + Weight::from_parts(25_259_000, 4091) .saturating_add(RocksDbWeight::get().reads(6_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } diff --git a/frame/relayer/src/weights.rs b/frame/relayer/src/weights.rs index a1f493d4..9ce7b980 100644 --- a/frame/relayer/src/weights.rs +++ b/frame/relayer/src/weights.rs @@ -2,7 +2,7 @@ //! Autogenerated weights for pallet_relayer //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 53.0.0 -//! DATE: 2026-07-04, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-07-06, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `ubuntu-32gb-fsn1-1`, CPU: `AMD EPYC-Genoa Processor` //! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 @@ -62,8 +62,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `19` // Estimated: `1504` - // Minimum execution time: 7_080_000 picoseconds. - Weight::from_parts(7_260_000, 1504) + // Minimum execution time: 8_070_000 picoseconds. + Weight::from_parts(8_500_000, 1504) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -82,10 +82,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `19` // Estimated: `1504` - // Minimum execution time: 7_050_000 picoseconds. - Weight::from_parts(7_612_478, 1504) - // Standard Error: 3_271 - .saturating_add(Weight::from_parts(23_856, 0).saturating_mul(n.into())) + // Minimum execution time: 8_260_000 picoseconds. + Weight::from_parts(9_079_107, 1504) + // Standard Error: 2_952 + .saturating_add(Weight::from_parts(13_029, 0).saturating_mul(n.into())) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -105,8 +105,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `61` // Estimated: `3533` - // Minimum execution time: 16_510_000 picoseconds. - Weight::from_parts(16_850_000, 3533) + // Minimum execution time: 18_179_000 picoseconds. + Weight::from_parts(18_800_000, 3533) .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } @@ -126,8 +126,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `189` // Estimated: `3533` - // Minimum execution time: 16_140_000 picoseconds. - Weight::from_parts(16_750_000, 3533) + // Minimum execution time: 18_140_000 picoseconds. + Weight::from_parts(18_790_000, 3533) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } @@ -149,8 +149,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `19` // Estimated: `1504` - // Minimum execution time: 7_080_000 picoseconds. - Weight::from_parts(7_260_000, 1504) + // Minimum execution time: 8_070_000 picoseconds. + Weight::from_parts(8_500_000, 1504) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -169,10 +169,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `19` // Estimated: `1504` - // Minimum execution time: 7_050_000 picoseconds. - Weight::from_parts(7_612_478, 1504) - // Standard Error: 3_271 - .saturating_add(Weight::from_parts(23_856, 0).saturating_mul(n.into())) + // Minimum execution time: 8_260_000 picoseconds. + Weight::from_parts(9_079_107, 1504) + // Standard Error: 2_952 + .saturating_add(Weight::from_parts(13_029, 0).saturating_mul(n.into())) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -192,8 +192,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `61` // Estimated: `3533` - // Minimum execution time: 16_510_000 picoseconds. - Weight::from_parts(16_850_000, 3533) + // Minimum execution time: 18_179_000 picoseconds. + Weight::from_parts(18_800_000, 3533) .saturating_add(RocksDbWeight::get().reads(6_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } @@ -213,8 +213,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `189` // Estimated: `3533` - // Minimum execution time: 16_140_000 picoseconds. - Weight::from_parts(16_750_000, 3533) + // Minimum execution time: 18_140_000 picoseconds. + Weight::from_parts(18_790_000, 3533) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } diff --git a/frame/shielded-pool/src/weights.rs b/frame/shielded-pool/src/weights.rs index d9c7163b..3a82a0b7 100644 --- a/frame/shielded-pool/src/weights.rs +++ b/frame/shielded-pool/src/weights.rs @@ -2,7 +2,7 @@ //! Autogenerated weights for pallet_shielded_pool //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 53.0.0 -//! DATE: 2026-07-04, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-07-06, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `ubuntu-32gb-fsn1-1`, CPU: `AMD EPYC-Genoa Processor` //! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 @@ -90,8 +90,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `1127` // Estimated: `4687` - // Minimum execution time: 732_198_000 picoseconds. - Weight::from_parts(747_138_000, 4687) + // Minimum execution time: 892_444_000 picoseconds. + Weight::from_parts(908_654_000, 4687) .saturating_add(T::DbWeight::get().reads(14_u64)) .saturating_add(T::DbWeight::get().writes(13_u64)) } @@ -134,19 +134,21 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `1127` // Estimated: `4687 + n * (2701 ±0)` - // Minimum execution time: 728_458_000 picoseconds. - Weight::from_parts(745_678_000, 4687) - // Standard Error: 1_681_312 - .saturating_add(Weight::from_parts(677_433_214, 0).saturating_mul(n.into())) + // Minimum execution time: 888_364_000 picoseconds. + Weight::from_parts(99_550_075, 4687) + // Standard Error: 709_747 + .saturating_add(Weight::from_parts(869_647_941, 0).saturating_mul(n.into())) .saturating_add(T::DbWeight::get().reads(13_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(n.into()))) .saturating_add(T::DbWeight::get().writes(9_u64)) .saturating_add(T::DbWeight::get().writes((4_u64).saturating_mul(n.into()))) .saturating_add(Weight::from_parts(0, 2701).saturating_mul(n.into())) } - /// Storage: `ShieldedPool::HistoricPoseidonRoots` (r:1 w:1) + /// Storage: `ShieldedPool::Assets` (r:1 w:0) + /// Proof: `ShieldedPool::Assets` (`max_values`: None, `max_size`: Some(166), added: 2641, mode: `MaxEncodedLen`) + /// Storage: `ShieldedPool::HistoricPoseidonRoots` (r:1 w:2) /// Proof: `ShieldedPool::HistoricPoseidonRoots` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`) - /// Storage: `ShieldedPool::NullifierSet` (r:1 w:1) + /// Storage: `ShieldedPool::NullifierSet` (r:2 w:2) /// Proof: `ShieldedPool::NullifierSet` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) /// Storage: `Relayer::MinRelayFee` (r:1 w:0) /// Proof: `Relayer::MinRelayFee` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) @@ -156,7 +158,7 @@ impl WeightInfo for SubstrateWeight { /// Proof: `ShieldedPool::TotalNullifiersSpent` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `ShieldedPool::MerkleTreeSize` (r:1 w:1) /// Proof: `ShieldedPool::MerkleTreeSize` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) - /// Storage: `ShieldedPool::CommitmentMemos` (r:1 w:1) + /// Storage: `ShieldedPool::CommitmentMemos` (r:2 w:2) /// Proof: `ShieldedPool::CommitmentMemos` (`max_values`: None, `max_size`: Some(226), added: 2701, mode: `MaxEncodedLen`) /// Storage: `ShieldedPool::MerkleTreeFrontier` (r:1 w:1) /// Proof: `ShieldedPool::MerkleTreeFrontier` (`max_values`: Some(1), `max_size`: Some(640), added: 1135, mode: `MaxEncodedLen`) @@ -172,28 +174,28 @@ impl WeightInfo for SubstrateWeight { /// Proof: `System::EventCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `System::Events` (r:1 w:1) /// Proof: `System::Events` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) - /// Storage: `Authorship::Author` (r:1 w:0) - /// Proof: `Authorship::Author` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) - /// Storage: `System::Digest` (r:1 w:0) - /// Proof: `System::Digest` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) - /// Storage: `ShieldedPool::MerkleLeaves` (r:0 w:1) + /// Storage: `Relayer::RelayerRegistry` (r:1 w:0) + /// Proof: `Relayer::RelayerRegistry` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + /// Storage: `Relayer::PendingRelayerFees` (r:1 w:1) + /// Proof: `Relayer::PendingRelayerFees` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `ShieldedPool::MerkleLeaves` (r:0 w:2) /// Proof: `ShieldedPool::MerkleLeaves` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) - /// Storage: `ShieldedPool::CommitmentToLeafIndex` (r:0 w:1) + /// Storage: `ShieldedPool::CommitmentToLeafIndex` (r:0 w:2) /// Proof: `ShieldedPool::CommitmentToLeafIndex` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) - /// The range of component `n` (outputs/leaves inserted) is `[1, 2]`. + /// The range of component `n` is `[1, 2]`. fn private_transfer(n: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `1016` - // Estimated: `4687` - // Minimum execution time: 696_789_000 picoseconds. - // Base measures one leaf insertion; each extra output adds a full insert - // (~20 Poseidon hashes + storage). Per-leaf cost taken from shield_batch's - // benchmarked per-item figure as a safe upper bound until re-benchmarked. - Weight::from_parts(707_598_000, 4687) - .saturating_add(Weight::from_parts(677_433_214, 2701).saturating_mul(n.saturating_sub(1).into())) - .saturating_add(T::DbWeight::get().reads(16_u64)) - .saturating_add(T::DbWeight::get().writes(13_u64)) - .saturating_add(T::DbWeight::get().writes((7_u64).saturating_mul(n.saturating_sub(1).into()))) + // Measured: `1182` + // Estimated: `4687 + n * (2701 ±0)` + // Minimum execution time: 845_115_000 picoseconds. + Weight::from_parts(83_928_940, 4687) + // Standard Error: 2_278_134 + .saturating_add(Weight::from_parts(798_570_579, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(15_u64)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().writes(9_u64)) + .saturating_add(T::DbWeight::get().writes((5_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 2701).saturating_mul(n.into())) } /// Storage: `ShieldedPool::Assets` (r:1 w:0) /// Proof: `ShieldedPool::Assets` (`max_values`: None, `max_size`: Some(166), added: 2641, mode: `MaxEncodedLen`) @@ -217,23 +219,25 @@ impl WeightInfo for SubstrateWeight { /// Proof: `System::EventCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `System::Events` (r:1 w:1) /// Proof: `System::Events` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) - /// Storage: `Authorship::Author` (r:1 w:0) - /// Proof: `Authorship::Author` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) - /// Storage: `System::Digest` (r:1 w:0) - /// Proof: `System::Digest` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Relayer::RelayerRegistry` (r:1 w:0) + /// Proof: `Relayer::RelayerRegistry` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + /// Storage: `Relayer::PendingRelayerFees` (r:1 w:1) + /// Proof: `Relayer::PendingRelayerFees` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) /// Storage: `ShieldedPool::TotalNullifiersSpent` (r:1 w:1) /// Proof: `ShieldedPool::TotalNullifiersSpent` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) fn unshield() -> Weight { // Proof Size summary in bytes: - // Measured: `815` + // Measured: `903` // Estimated: `6196` - // Minimum execution time: 92_130_000 picoseconds. - Weight::from_parts(95_350_000, 6196) + // Minimum execution time: 111_590_000 picoseconds. + Weight::from_parts(114_739_000, 6196) .saturating_add(T::DbWeight::get().reads(15_u64)) - .saturating_add(T::DbWeight::get().writes(7_u64)) + .saturating_add(T::DbWeight::get().writes(8_u64)) } /// Storage: `ShieldedPool::NextAssetId` (r:1 w:1) /// Proof: `ShieldedPool::NextAssetId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `ShieldedPool::Assets` (r:1 w:1) + /// Proof: `ShieldedPool::Assets` (`max_values`: None, `max_size`: Some(166), added: 2641, mode: `MaxEncodedLen`) /// Storage: `System::Number` (r:1 w:0) /// Proof: `System::Number` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `System::ExecutionPhase` (r:1 w:0) @@ -242,15 +246,13 @@ impl WeightInfo for SubstrateWeight { /// Proof: `System::EventCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `System::Events` (r:1 w:1) /// Proof: `System::Events` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) - /// Storage: `ShieldedPool::Assets` (r:0 w:1) - /// Proof: `ShieldedPool::Assets` (`max_values`: None, `max_size`: Some(166), added: 2641, mode: `MaxEncodedLen`) fn register_asset() -> Weight { // Proof Size summary in bytes: - // Measured: `201` - // Estimated: `1686` - // Minimum execution time: 13_300_000 picoseconds. - Weight::from_parts(13_710_000, 1686) - .saturating_add(T::DbWeight::get().reads(5_u64)) + // Measured: `239` + // Estimated: `3631` + // Minimum execution time: 17_960_000 picoseconds. + Weight::from_parts(18_710_000, 3631) + .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } /// Storage: `ShieldedPool::Assets` (r:1 w:1) @@ -267,8 +269,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `328` // Estimated: `3631` - // Minimum execution time: 15_290_000 picoseconds. - Weight::from_parts(15_830_000, 3631) + // Minimum execution time: 17_370_000 picoseconds. + Weight::from_parts(18_150_000, 3631) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -286,8 +288,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `328` // Estimated: `3631` - // Minimum execution time: 15_120_000 picoseconds. - Weight::from_parts(15_620_000, 3631) + // Minimum execution time: 17_540_000 picoseconds. + Weight::from_parts(18_319_000, 3631) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -325,8 +327,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `1085` // Estimated: `4687` - // Minimum execution time: 696_778_000 picoseconds. - Weight::from_parts(713_979_000, 4687) + // Minimum execution time: 833_605_000 picoseconds. + Weight::from_parts(852_403_000, 4687) .saturating_add(T::DbWeight::get().reads(12_u64)) .saturating_add(T::DbWeight::get().writes(12_u64)) } @@ -372,8 +374,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `1127` // Estimated: `4687` - // Minimum execution time: 732_198_000 picoseconds. - Weight::from_parts(747_138_000, 4687) + // Minimum execution time: 892_444_000 picoseconds. + Weight::from_parts(908_654_000, 4687) .saturating_add(RocksDbWeight::get().reads(14_u64)) .saturating_add(RocksDbWeight::get().writes(13_u64)) } @@ -416,19 +418,21 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `1127` // Estimated: `4687 + n * (2701 ±0)` - // Minimum execution time: 728_458_000 picoseconds. - Weight::from_parts(745_678_000, 4687) - // Standard Error: 1_681_312 - .saturating_add(Weight::from_parts(677_433_214, 0).saturating_mul(n.into())) + // Minimum execution time: 888_364_000 picoseconds. + Weight::from_parts(99_550_075, 4687) + // Standard Error: 709_747 + .saturating_add(Weight::from_parts(869_647_941, 0).saturating_mul(n.into())) .saturating_add(RocksDbWeight::get().reads(13_u64)) .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(n.into()))) .saturating_add(RocksDbWeight::get().writes(9_u64)) .saturating_add(RocksDbWeight::get().writes((4_u64).saturating_mul(n.into()))) .saturating_add(Weight::from_parts(0, 2701).saturating_mul(n.into())) } - /// Storage: `ShieldedPool::HistoricPoseidonRoots` (r:1 w:1) + /// Storage: `ShieldedPool::Assets` (r:1 w:0) + /// Proof: `ShieldedPool::Assets` (`max_values`: None, `max_size`: Some(166), added: 2641, mode: `MaxEncodedLen`) + /// Storage: `ShieldedPool::HistoricPoseidonRoots` (r:1 w:2) /// Proof: `ShieldedPool::HistoricPoseidonRoots` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`) - /// Storage: `ShieldedPool::NullifierSet` (r:1 w:1) + /// Storage: `ShieldedPool::NullifierSet` (r:2 w:2) /// Proof: `ShieldedPool::NullifierSet` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) /// Storage: `Relayer::MinRelayFee` (r:1 w:0) /// Proof: `Relayer::MinRelayFee` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) @@ -438,7 +442,7 @@ impl WeightInfo for () { /// Proof: `ShieldedPool::TotalNullifiersSpent` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `ShieldedPool::MerkleTreeSize` (r:1 w:1) /// Proof: `ShieldedPool::MerkleTreeSize` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) - /// Storage: `ShieldedPool::CommitmentMemos` (r:1 w:1) + /// Storage: `ShieldedPool::CommitmentMemos` (r:2 w:2) /// Proof: `ShieldedPool::CommitmentMemos` (`max_values`: None, `max_size`: Some(226), added: 2701, mode: `MaxEncodedLen`) /// Storage: `ShieldedPool::MerkleTreeFrontier` (r:1 w:1) /// Proof: `ShieldedPool::MerkleTreeFrontier` (`max_values`: Some(1), `max_size`: Some(640), added: 1135, mode: `MaxEncodedLen`) @@ -454,28 +458,28 @@ impl WeightInfo for () { /// Proof: `System::EventCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `System::Events` (r:1 w:1) /// Proof: `System::Events` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) - /// Storage: `Authorship::Author` (r:1 w:0) - /// Proof: `Authorship::Author` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) - /// Storage: `System::Digest` (r:1 w:0) - /// Proof: `System::Digest` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) - /// Storage: `ShieldedPool::MerkleLeaves` (r:0 w:1) + /// Storage: `Relayer::RelayerRegistry` (r:1 w:0) + /// Proof: `Relayer::RelayerRegistry` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + /// Storage: `Relayer::PendingRelayerFees` (r:1 w:1) + /// Proof: `Relayer::PendingRelayerFees` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `ShieldedPool::MerkleLeaves` (r:0 w:2) /// Proof: `ShieldedPool::MerkleLeaves` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) - /// Storage: `ShieldedPool::CommitmentToLeafIndex` (r:0 w:1) + /// Storage: `ShieldedPool::CommitmentToLeafIndex` (r:0 w:2) /// Proof: `ShieldedPool::CommitmentToLeafIndex` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) - /// The range of component `n` (outputs/leaves inserted) is `[1, 2]`. + /// The range of component `n` is `[1, 2]`. fn private_transfer(n: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `1016` - // Estimated: `4687` - // Minimum execution time: 696_789_000 picoseconds. - // Base measures one leaf insertion; each extra output adds a full insert - // (~20 Poseidon hashes + storage). Per-leaf cost taken from shield_batch's - // benchmarked per-item figure as a safe upper bound until re-benchmarked. - Weight::from_parts(707_598_000, 4687) - .saturating_add(Weight::from_parts(677_433_214, 2701).saturating_mul(n.saturating_sub(1).into())) - .saturating_add(RocksDbWeight::get().reads(16_u64)) - .saturating_add(RocksDbWeight::get().writes(13_u64)) - .saturating_add(RocksDbWeight::get().writes((7_u64).saturating_mul(n.saturating_sub(1).into()))) + // Measured: `1182` + // Estimated: `4687 + n * (2701 ±0)` + // Minimum execution time: 845_115_000 picoseconds. + Weight::from_parts(83_928_940, 4687) + // Standard Error: 2_278_134 + .saturating_add(Weight::from_parts(798_570_579, 0).saturating_mul(n.into())) + .saturating_add(RocksDbWeight::get().reads(15_u64)) + .saturating_add(RocksDbWeight::get().reads((2_u64).saturating_mul(n.into()))) + .saturating_add(RocksDbWeight::get().writes(9_u64)) + .saturating_add(RocksDbWeight::get().writes((5_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 2701).saturating_mul(n.into())) } /// Storage: `ShieldedPool::Assets` (r:1 w:0) /// Proof: `ShieldedPool::Assets` (`max_values`: None, `max_size`: Some(166), added: 2641, mode: `MaxEncodedLen`) @@ -499,23 +503,25 @@ impl WeightInfo for () { /// Proof: `System::EventCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `System::Events` (r:1 w:1) /// Proof: `System::Events` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) - /// Storage: `Authorship::Author` (r:1 w:0) - /// Proof: `Authorship::Author` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) - /// Storage: `System::Digest` (r:1 w:0) - /// Proof: `System::Digest` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Relayer::RelayerRegistry` (r:1 w:0) + /// Proof: `Relayer::RelayerRegistry` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + /// Storage: `Relayer::PendingRelayerFees` (r:1 w:1) + /// Proof: `Relayer::PendingRelayerFees` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) /// Storage: `ShieldedPool::TotalNullifiersSpent` (r:1 w:1) /// Proof: `ShieldedPool::TotalNullifiersSpent` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) fn unshield() -> Weight { // Proof Size summary in bytes: - // Measured: `815` + // Measured: `903` // Estimated: `6196` - // Minimum execution time: 92_130_000 picoseconds. - Weight::from_parts(95_350_000, 6196) + // Minimum execution time: 111_590_000 picoseconds. + Weight::from_parts(114_739_000, 6196) .saturating_add(RocksDbWeight::get().reads(15_u64)) - .saturating_add(RocksDbWeight::get().writes(7_u64)) + .saturating_add(RocksDbWeight::get().writes(8_u64)) } /// Storage: `ShieldedPool::NextAssetId` (r:1 w:1) /// Proof: `ShieldedPool::NextAssetId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `ShieldedPool::Assets` (r:1 w:1) + /// Proof: `ShieldedPool::Assets` (`max_values`: None, `max_size`: Some(166), added: 2641, mode: `MaxEncodedLen`) /// Storage: `System::Number` (r:1 w:0) /// Proof: `System::Number` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `System::ExecutionPhase` (r:1 w:0) @@ -524,15 +530,13 @@ impl WeightInfo for () { /// Proof: `System::EventCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `System::Events` (r:1 w:1) /// Proof: `System::Events` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) - /// Storage: `ShieldedPool::Assets` (r:0 w:1) - /// Proof: `ShieldedPool::Assets` (`max_values`: None, `max_size`: Some(166), added: 2641, mode: `MaxEncodedLen`) fn register_asset() -> Weight { // Proof Size summary in bytes: - // Measured: `201` - // Estimated: `1686` - // Minimum execution time: 13_300_000 picoseconds. - Weight::from_parts(13_710_000, 1686) - .saturating_add(RocksDbWeight::get().reads(5_u64)) + // Measured: `239` + // Estimated: `3631` + // Minimum execution time: 17_960_000 picoseconds. + Weight::from_parts(18_710_000, 3631) + .saturating_add(RocksDbWeight::get().reads(6_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } /// Storage: `ShieldedPool::Assets` (r:1 w:1) @@ -549,8 +553,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `328` // Estimated: `3631` - // Minimum execution time: 15_290_000 picoseconds. - Weight::from_parts(15_830_000, 3631) + // Minimum execution time: 17_370_000 picoseconds. + Weight::from_parts(18_150_000, 3631) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -568,8 +572,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `328` // Estimated: `3631` - // Minimum execution time: 15_120_000 picoseconds. - Weight::from_parts(15_620_000, 3631) + // Minimum execution time: 17_540_000 picoseconds. + Weight::from_parts(18_319_000, 3631) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -607,8 +611,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `1085` // Estimated: `4687` - // Minimum execution time: 696_778_000 picoseconds. - Weight::from_parts(713_979_000, 4687) + // Minimum execution time: 833_605_000 picoseconds. + Weight::from_parts(852_403_000, 4687) .saturating_add(RocksDbWeight::get().reads(12_u64)) .saturating_add(RocksDbWeight::get().writes(12_u64)) } diff --git a/frame/zk-verifier/src/weights.rs b/frame/zk-verifier/src/weights.rs index 9c8e7196..76a5792d 100644 --- a/frame/zk-verifier/src/weights.rs +++ b/frame/zk-verifier/src/weights.rs @@ -2,7 +2,7 @@ //! Autogenerated weights for pallet_zk_verifier //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 53.0.0 -//! DATE: 2026-07-04, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-07-06, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `ubuntu-32gb-fsn1-1`, CPU: `AMD EPYC-Genoa Processor` //! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 @@ -21,12 +21,13 @@ // 50 // --repeat // 20 +// --execution=wasm // --wasm-execution=compiled // --heap-pages=4096 +// --output +// ./frame/zk-verifier/src/weights.rs // --template // ./scripts/frame-weight-template.hbs -// --output -// frame/zk-verifier/src/weights.rs #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] @@ -62,15 +63,15 @@ impl WeightInfo for SubstrateWeight { /// Proof: `System::EventCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `System::Events` (r:1 w:1) /// Proof: `System::Events` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) - /// The range of component `n` is `[1, 32]`. + /// The range of component `n` is `[1, 16]`. fn verify_proof(n: u32, ) -> Weight { // Proof Size summary in bytes: // Measured: `445 + n * (32 ±0)` // Estimated: `11704 + n * (32 ±0)` - // Minimum execution time: 10_282_202_000 picoseconds. - Weight::from_parts(10_666_582_908, 11704) - // Standard Error: 842_593 - .saturating_add(Weight::from_parts(37_117_571, 0).saturating_mul(n.into())) + // Minimum execution time: 11_597_520_000 picoseconds. + Weight::from_parts(11_846_175_078, 11704) + // Standard Error: 996_773 + .saturating_add(Weight::from_parts(25_332_234, 0).saturating_mul(n.into())) .saturating_add(T::DbWeight::get().reads(7_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) .saturating_add(Weight::from_parts(0, 32).saturating_mul(n.into())) @@ -91,8 +92,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `61` // Estimated: `11704` - // Minimum execution time: 2_272_994_000 picoseconds. - Weight::from_parts(2_284_843_000, 11704) + // Minimum execution time: 2_567_035_000 picoseconds. + Weight::from_parts(2_598_264_000, 11704) .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } @@ -112,8 +113,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `231` // Estimated: `11704` - // Minimum execution time: 14_310_000 picoseconds. - Weight::from_parts(14_659_000, 11704) + // Minimum execution time: 16_670_000 picoseconds. + Weight::from_parts(17_320_000, 11704) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -133,8 +134,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `242` // Estimated: `11704` - // Minimum execution time: 17_710_000 picoseconds. - Weight::from_parts(18_110_000, 11704) + // Minimum execution time: 20_820_000 picoseconds. + Weight::from_parts(21_470_000, 11704) .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -155,10 +156,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `61` // Estimated: `1546 + n * (10714 ±0)` - // Minimum execution time: 2_270_374_000 picoseconds. - Weight::from_parts(10_871_225, 1546) - // Standard Error: 2_740_399 - .saturating_add(Weight::from_parts(2_290_830_855, 0).saturating_mul(n.into())) + // Minimum execution time: 2_535_745_000 picoseconds. + Weight::from_parts(51_613_689, 1546) + // Standard Error: 1_581_789 + .saturating_add(Weight::from_parts(2_558_352_793, 0).saturating_mul(n.into())) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(n.into()))) .saturating_add(T::DbWeight::get().writes(2_u64)) @@ -183,15 +184,15 @@ impl WeightInfo for () { /// Proof: `System::EventCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `System::Events` (r:1 w:1) /// Proof: `System::Events` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) - /// The range of component `n` is `[1, 32]`. + /// The range of component `n` is `[1, 16]`. fn verify_proof(n: u32, ) -> Weight { // Proof Size summary in bytes: // Measured: `445 + n * (32 ±0)` // Estimated: `11704 + n * (32 ±0)` - // Minimum execution time: 10_282_202_000 picoseconds. - Weight::from_parts(10_666_582_908, 11704) - // Standard Error: 842_593 - .saturating_add(Weight::from_parts(37_117_571, 0).saturating_mul(n.into())) + // Minimum execution time: 11_597_520_000 picoseconds. + Weight::from_parts(11_846_175_078, 11704) + // Standard Error: 996_773 + .saturating_add(Weight::from_parts(25_332_234, 0).saturating_mul(n.into())) .saturating_add(RocksDbWeight::get().reads(7_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) .saturating_add(Weight::from_parts(0, 32).saturating_mul(n.into())) @@ -212,8 +213,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `61` // Estimated: `11704` - // Minimum execution time: 2_272_994_000 picoseconds. - Weight::from_parts(2_284_843_000, 11704) + // Minimum execution time: 2_567_035_000 picoseconds. + Weight::from_parts(2_598_264_000, 11704) .saturating_add(RocksDbWeight::get().reads(6_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } @@ -233,8 +234,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `231` // Estimated: `11704` - // Minimum execution time: 14_310_000 picoseconds. - Weight::from_parts(14_659_000, 11704) + // Minimum execution time: 16_670_000 picoseconds. + Weight::from_parts(17_320_000, 11704) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -254,8 +255,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `242` // Estimated: `11704` - // Minimum execution time: 17_710_000 picoseconds. - Weight::from_parts(18_110_000, 11704) + // Minimum execution time: 20_820_000 picoseconds. + Weight::from_parts(21_470_000, 11704) .saturating_add(RocksDbWeight::get().reads(6_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -276,10 +277,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `61` // Estimated: `1546 + n * (10714 ±0)` - // Minimum execution time: 2_270_374_000 picoseconds. - Weight::from_parts(10_871_225, 1546) - // Standard Error: 2_740_399 - .saturating_add(Weight::from_parts(2_290_830_855, 0).saturating_mul(n.into())) + // Minimum execution time: 2_535_745_000 picoseconds. + Weight::from_parts(51_613_689, 1546) + // Standard Error: 1_581_789 + .saturating_add(Weight::from_parts(2_558_352_793, 0).saturating_mul(n.into())) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().reads((2_u64).saturating_mul(n.into()))) .saturating_add(RocksDbWeight::get().writes(2_u64)) From 291cc2620d71d7e925b12063e9663810428a2792 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Sun, 5 Jul 2026 22:44:52 -0400 Subject: [PATCH 17/18] chore(runtime): bump spec_version to 3 for shielded-pool hardening --- template/runtime/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/runtime/src/lib.rs b/template/runtime/src/lib.rs index 576ea349..2dc939b7 100644 --- a/template/runtime/src/lib.rs +++ b/template/runtime/src/lib.rs @@ -197,7 +197,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: Cow::Borrowed("orbinum"), impl_name: Cow::Borrowed("orbinum"), authoring_version: 1, - spec_version: 2, + spec_version: 3, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, From b69ce9413f6d75adc39cc3f8739b7f73f581bb52 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Mon, 6 Jul 2026 00:08:07 -0400 Subject: [PATCH 18/18] fix(shielded-pool): use real pallet-relayer in the test mock --- frame/shielded-pool/Cargo.toml | 2 + frame/shielded-pool/src/mock.rs | 153 +++++++++++--------------------- 2 files changed, 53 insertions(+), 102 deletions(-) diff --git a/frame/shielded-pool/Cargo.toml b/frame/shielded-pool/Cargo.toml index a2e71595..e2041d9a 100644 --- a/frame/shielded-pool/Cargo.toml +++ b/frame/shielded-pool/Cargo.toml @@ -75,10 +75,12 @@ runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", + "pallet-relayer/runtime-benchmarks", "pallet-zk-verifier/runtime-benchmarks", ] skip-proof-verification = ["pallet-zk-verifier/skip-proof-verification"] try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", + "pallet-relayer/try-runtime", ] diff --git a/frame/shielded-pool/src/mock.rs b/frame/shielded-pool/src/mock.rs index f692b77b..b78228ad 100644 --- a/frame/shielded-pool/src/mock.rs +++ b/frame/shielded-pool/src/mock.rs @@ -1,7 +1,11 @@ //! Mock runtime for testing pallet-shielded-pool use crate as pallet_shielded_pool; -use frame_support::{PalletId, derive_impl, parameter_types, traits::ConstU128}; +use frame_support::{ + PalletId, derive_impl, parameter_types, + traits::{ConstU32, ConstU128}, +}; +use frame_system::EnsureRoot; use pallet_zk_verifier::ZkVerifierPort; use sp_runtime::{AccountId32, BuildStorage, traits::IdentityLookup}; @@ -19,6 +23,7 @@ frame_support::construct_runtime!( System: frame_system, Balances: pallet_balances, ZkVerifier: pallet_zk_verifier, + Relayer: pallet_relayer, ShieldedPool: pallet_shielded_pool, } ); @@ -126,6 +131,30 @@ impl ZkVerifierPort for MockZkVerifier { } } +/// Block-author provider for the real `pallet-relayer` in tests. +/// +/// Returns `None` when the raw `b"mock:no_author"` flag is set (see +/// `mock_clear_block_author`), otherwise `Some(acc(1))` (Alice) — preserving the +/// behavior the previous `MockRelayer` exposed. +pub struct MockBlockAuthor; +impl frame_support::traits::Get> for MockBlockAuthor { + fn get() -> Option { + if sp_io::storage::get(b"mock:no_author").is_some() { + None + } else { + Some(acc(1)) + } + } +} + +impl pallet_relayer::Config for Test { + type BlockAuthor = MockBlockAuthor; + type DefaultMinRelayFee = ConstU128<0>; + type ManageOrigin = EnsureRoot; + type MaxAllowedSelectors = ConstU32<16>; + type WeightInfo = (); +} + impl pallet_shielded_pool::Config for Test { type Currency = Balances; type ZkVerifier = MockZkVerifier; @@ -134,130 +163,50 @@ impl pallet_shielded_pool::Config for Test { type MaxHistoricRoots = MaxHistoricRoots; type MinShieldAmount = MinShieldAmount; type WeightInfo = (); - type Relayer = MockRelayer; + type Relayer = pallet_relayer::Pallet; } -/// Mock implementation of `RelayerInterface` for unit tests. -/// -/// - `min_relay_fee()` → 0 (no minimum in tests; set higher when testing fee enforcement) -/// - `block_author()` → `Some(acc(1))` (Alice) -/// - Fee tracking is backed by raw `TestExternalities` storage (auto-reset per test). -pub struct MockRelayer; +// ── Relayer test helpers ───────────────────────────────────────────────────── +// +// These drive the REAL `pallet-relayer` storage so `mock::Test` exercises the +// same code path as the runtime. Signatures are unchanged from the previous +// `MockRelayer` helpers, so callers stay the same. -/// Read a pending-fee balance from raw test storage. +/// Read a pending-fee balance from `pallet-relayer`. pub fn mock_pending_fees_get(who: AccountId, asset_id: u32) -> u128 { - use parity_scale_codec::{Decode, Encode}; - let key = [ - b"mock:fees:".as_ref(), - who.encode().as_slice(), - asset_id.encode().as_slice(), - ] - .concat(); - sp_io::storage::get(&key) - .and_then(|v| u128::decode(&mut &v[..]).ok()) - .unwrap_or(0) + pallet_relayer::PendingRelayerFees::::get(who, asset_id) } -/// Write a pending-fee balance to raw test storage. +/// Write a pending-fee balance into `pallet-relayer`. pub fn mock_pending_fees_set(who: AccountId, asset_id: u32, amount: u128) { - use parity_scale_codec::Encode; - let key = [ - b"mock:fees:".as_ref(), - who.encode().as_slice(), - asset_id.encode().as_slice(), - ] - .concat(); - sp_io::storage::set(&key, &amount.encode()); + pallet_relayer::PendingRelayerFees::::insert(who, asset_id, amount); } -/// Read the registered EVM address for an account from raw test storage. +/// Read the registered EVM address for an account from `pallet-relayer`'s +/// reverse index. +#[allow(dead_code)] pub fn mock_evm_address_get(who: AccountId) -> Option { - use parity_scale_codec::{Decode, Encode}; - let key = [b"mock:evm:".as_ref(), who.encode().as_slice()].concat(); - sp_io::storage::get(&key) - .and_then(|v| <[u8; 20]>::decode(&mut &v[..]).ok()) - .map(sp_core::H160::from) + pallet_relayer::RelayerByAccount::::get(who) } -/// Write a minimum relay fee to raw test storage. -/// By default `MockRelayer::min_relay_fee()` returns 0; call this to raise the floor. +/// Set the minimum relay fee in `pallet-relayer`. +/// Defaults to 0 (`DefaultMinRelayFee = ConstU128<0>`); call this to raise the floor. pub fn mock_set_min_relay_fee(fee: u128) { - use parity_scale_codec::Encode; - sp_io::storage::set(b"mock:min_relay_fee", &fee.encode()); + pallet_relayer::MinRelayFee::::put(fee); } /// Register an EVM address → account mapping so `resolve_relayer` returns `Some`. -/// Mirrors the governance-gated registry in `pallet-relayer` for tests. +/// Writes directly into `pallet-relayer`'s registry for tests. pub fn mock_register_relayer(who: AccountId, addr: sp_core::H160) { - use parity_scale_codec::Encode; - let key = [b"mock:resolve:".as_ref(), addr.as_bytes()].concat(); - sp_io::storage::set(&key, &who.encode()); + pallet_relayer::RelayerRegistry::::insert(addr, who); } -/// Force `block_author()` to return `None` (default is `Some(acc(1))`), to test -/// the no-fee-recipient path. +/// Force the relayer's `block_author()` to return `None` (default is `Some(acc(1))`), +/// to test the no-fee-recipient path. Read by `MockBlockAuthor`. pub fn mock_clear_block_author() { sp_io::storage::set(b"mock:no_author", &[1u8]); } -impl pallet_relayer::RelayerInterface for MockRelayer { - type AccountId = AccountId; - - fn resolve_relayer(evm_address: &sp_core::H160) -> Option { - // Reads the test registry seeded by `mock_register_relayer`; unregistered - // addresses return None so fees fall back to block_author. - use parity_scale_codec::Decode; - let key = [b"mock:resolve:".as_ref(), evm_address.as_bytes()].concat(); - sp_io::storage::get(&key).and_then(|v| AccountId::decode(&mut &v[..]).ok()) - } - - fn min_relay_fee() -> u128 { - use parity_scale_codec::Decode; - sp_io::storage::get(b"mock:min_relay_fee") - .and_then(|v| u128::decode(&mut &v[..]).ok()) - .unwrap_or(0) - } - - fn allowed_selectors() -> sp_std::vec::Vec<[u8; 4]> { - sp_std::vec![] - } - - fn block_author() -> Option { - if sp_io::storage::get(b"mock:no_author").is_some() { - None - } else { - Some(acc(1)) - } - } - - fn accumulate_relay_fee(author: &AccountId, asset_id: u32, amount: u128) { - let current = mock_pending_fees_get(author.clone(), asset_id); - mock_pending_fees_set(author.clone(), asset_id, current.saturating_add(amount)); - } - - fn pending_relay_fees(who: &AccountId, asset_id: u32) -> u128 { - mock_pending_fees_get(who.clone(), asset_id) - } - - fn consume_relay_fee( - who: &AccountId, - asset_id: u32, - amount: u128, - ) -> frame_support::dispatch::DispatchResult { - let balance = mock_pending_fees_get(who.clone(), asset_id); - if balance >= amount { - mock_pending_fees_set(who.clone(), asset_id, balance - amount); - Ok(()) - } else { - Err(sp_runtime::DispatchError::Other("InsufficientPendingFees")) - } - } - - fn registered_evm_address(who: &AccountId) -> Option { - mock_evm_address_get(who.clone()) - } -} - /// Build genesis storage for testing pub fn new_test_ext() -> sp_io::TestExternalities { let mut t = frame_system::GenesisConfig::::default()