Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions client/rpc-v2/src/privacy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ pub struct AssetBalanceResponse {
pub struct PoolStatsResponse {
pub merkle_root: String,
pub commitment_count: u32,
pub nullifier_count: u64,
pub total_balance: u128,
pub asset_balances: Vec<AssetBalanceResponse>,
pub tree_depth: u32,
Expand Down Expand Up @@ -429,9 +430,18 @@ where
}
}

// Nullifier count — O(1) read of TotalNullifiersSpent counter.
// Defaults to 0 if the storage item is absent (pool with no spent notes yet).
let nullifier_count =
match read_storage(&*self.client, best_hash, value_key(b"TotalNullifiersSpent"))? {
Some(raw) => u64::decode(&mut &raw[..]).unwrap_or(0),
None => 0,
};

Ok(PoolStatsResponse {
merkle_root: format!("0x{}", hex::encode(root.as_bytes())),
commitment_count,
nullifier_count,
total_balance,
asset_balances,
tree_depth: DEFAULT_TREE_DEPTH as u32,
Expand Down Expand Up @@ -671,6 +681,7 @@ mod tests {
let resp = PoolStatsResponse {
merkle_root: "0x01".to_string(),
commitment_count: 5,
nullifier_count: 3,
total_balance: 1_000,
asset_balances: vec![
AssetBalanceResponse {
Expand All @@ -686,6 +697,7 @@ mod tests {
};
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["commitment_count"], 5u64);
assert_eq!(json["nullifier_count"], 3u64);
assert_eq!(json["total_balance"].as_u64().unwrap(), 1_000u64);
let ab = json["asset_balances"].as_array().unwrap();
assert_eq!(ab.len(), 2);
Expand All @@ -698,6 +710,7 @@ mod tests {
let orig = PoolStatsResponse {
merkle_root: "0xff".to_string(),
commitment_count: 1,
nullifier_count: 0,
total_balance: 42,
asset_balances: vec![AssetBalanceResponse {
asset_id: 0,
Expand All @@ -709,6 +722,25 @@ mod tests {
serde_json::from_str(&serde_json::to_string(&orig).unwrap()).unwrap();
assert_eq!(orig, back);
}

#[test]
fn pool_stats_response_nullifier_count_field_present() {
let resp = PoolStatsResponse {
merkle_root: "0xab".to_string(),
commitment_count: 10,
nullifier_count: 4,
total_balance: 0,
asset_balances: vec![],
tree_depth: 20,
};
let json = serde_json::to_value(&resp).unwrap();
// nullifier_count must be present and correctly serialised
assert_eq!(json["nullifier_count"], 4u64);
// active notes estimate: commitment_count - nullifier_count
let active = json["commitment_count"].as_u64().unwrap()
- json["nullifier_count"].as_u64().unwrap();
assert_eq!(active, 6);
}
}

// -------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion frame/shielded-pool/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pallet-shielded-pool"
version = "0.5.1"
version = "0.5.2"
description = "Shielded pool pallet for private transactions using ZK proofs"
authors = ["Orbinum Team"]
license = "GPL-3.0-or-later"
Expand Down
1 change: 0 additions & 1 deletion frame/shielded-pool/rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ license = "GPL-3.0-or-later"
[dependencies]
hex = "0.4"
jsonrpsee = { version = "0.24.9", features = ["server", "macros", "client"] }
log = "0.4"
pallet-shielded-pool = { path = ".." }
pallet-shielded-pool-runtime-api = { path = "../runtime-api" }
parity-scale-codec = { version = "3.6", features = ["derive"] }
Expand Down
47 changes: 0 additions & 47 deletions frame/shielded-pool/rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,46 +20,13 @@ pub struct MerkleProof {
pub siblings: Vec<String>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ShieldedEvent {
pub block_number: u64,
pub extrinsic_index: u32,
pub event_type: ShieldedEventType,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum ShieldedEventType {
Shield {
depositor: String,
amount: u128,
commitment: String,
leaf_index: u32,
encrypted_memo: Option<String>,
},
PrivateTransfer {
nullifiers: Vec<String>,
commitments: Vec<String>,
leaf_indices: Vec<u32>,
encrypted_memos: Option<Vec<String>>,
},
Unshield {
nullifier: String,
amount: u128,
recipient: String,
},
}

#[rpc(client, server)]
pub trait ShieldedPoolApi<BlockHash> {
#[method(name = "shieldedPool_getMerkleTreeInfo")]
fn get_merkle_tree_info(&self) -> RpcResult<MerkleTreeInfo>;

#[method(name = "shieldedPool_getMerkleProof")]
fn get_merkle_proof(&self, commitment: String) -> RpcResult<MerkleProof>;

#[method(name = "shieldedPool_scanEvents")]
fn scan_events(&self, from_block: u64, to_block: u64) -> RpcResult<Vec<ShieldedEvent>>;
}

pub struct ShieldedPool<C, B> {
Expand Down Expand Up @@ -137,18 +104,4 @@ where
.collect(),
})
}

fn scan_events(&self, _from_block: u64, _to_block: u64) -> RpcResult<Vec<ShieldedEvent>> {
// Event scanning is not implemented via runtime API
// This functionality should be implemented by:
// 1. Indexing events in an off-chain database (recommended)
// 2. Using Substrate's archive node with state queries
// 3. Implementing a custom indexer service
//
// Returning empty list as placeholder.
// TODO: Implement proper event indexing strategy

log::warn!("scan_events called but not implemented - use event indexer instead");
Ok(Vec::new())
}
}
25 changes: 25 additions & 0 deletions frame/shielded-pool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,15 @@ pub mod pallet {
#[pallet::storage]
pub type MerkleLeaves<T> = StorageMap<_, Blake2_128Concat, u32, Commitment, OptionQuery>;

/// Reverse index: commitment -> leaf index.
///
/// Populated on every `insert_leaf`. Enables O(1) lookup for Merkle proof
/// generation and duplicate-commitment checks, replacing the former O(n)
/// linear scan over `MerkleLeaves`.
#[pallet::storage]
pub type CommitmentToLeafIndex<T> =
StorageMap<_, Blake2_128Concat, Commitment, u32, OptionQuery>;

/// Set of used nullifiers (nullifier -> block number when used)
#[pallet::storage]
pub type NullifierSet<T: Config> =
Expand Down Expand Up @@ -348,6 +357,22 @@ pub mod pallet {
ValueQuery,
>;

/// Total number of commitments ever inserted into the Merkle tree.
///
/// Monotonically increasing counter. Incremented once per successful
/// `insert_leaf` (shield, private_transfer output, claim_shielded_fees).
/// Enables O(1) pool stats without scanning `MerkleLeaves` key prefixes.
#[pallet::storage]
pub type TotalCommitmentsInserted<T> = StorageValue<_, u64, ValueQuery>;

/// Total number of nullifiers ever spent (notes consumed).
///
/// Monotonically increasing counter. Incremented once per
/// `NullifierRepository::mark_as_used` (unshield, private_transfer input).
/// Enables O(1) pool stats without scanning `NullifierSet` key prefixes.
#[pallet::storage]
pub type TotalNullifiersSpent<T> = StorageValue<_, u64, ValueQuery>;

// ========================================================================
// Genesis Config
// ========================================================================
Expand Down
57 changes: 56 additions & 1 deletion frame/shielded-pool/src/merkle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use crate::{
pallet::{CommitmentMemos, Config, Error, Event, Pallet},
storage::MerkleRepository,
storage::{MerkleRepository, PoolStatsRepository},
types::{Commitment, DefaultMerklePath, Hash, MerklePath},
};
use alloc::boxed::Box;
Expand Down Expand Up @@ -344,7 +344,9 @@ impl MerkleTreeService {
let old_poseidon_root = MerkleRepository::get_poseidon_root::<T>();

MerkleRepository::insert_leaf::<T>(index, commitment);
MerkleRepository::set_commitment_leaf_index::<T>(commitment, index);
MerkleRepository::set_tree_size::<T>(index.saturating_add(1));
PoolStatsRepository::increment_commitments_inserted::<T>();
MerkleRepository::set_frontier::<T>(frontier);
MerkleRepository::set_poseidon_root::<T>(new_poseidon_root);
Self::add_poseidon_historic_root::<T>(new_poseidon_root);
Expand Down Expand Up @@ -734,6 +736,59 @@ mod tests {
});
}

#[test]
fn insert_leaf_populates_commitment_to_leaf_index() {
use crate::storage::MerkleRepository;
new_test_ext().execute_with(|| {
let c0 = Commitment::new([0xD0u8; 32]);
let c1 = Commitment::new([0xD1u8; 32]);
let c2 = Commitment::new([0xD2u8; 32]);
MerkleTreeService::insert_leaf::<Test>(c0).unwrap();
MerkleTreeService::insert_leaf::<Test>(c1).unwrap();
MerkleTreeService::insert_leaf::<Test>(c2).unwrap();
// Reverse index must be populated for every inserted commitment
assert_eq!(
MerkleRepository::get_commitment_leaf_index::<Test>(&c0),
Some(0)
);
assert_eq!(
MerkleRepository::get_commitment_leaf_index::<Test>(&c1),
Some(1)
);
assert_eq!(
MerkleRepository::get_commitment_leaf_index::<Test>(&c2),
Some(2)
);
// Unknown commitment returns None
assert_eq!(
MerkleRepository::get_commitment_leaf_index::<Test>(&Commitment::new([0xFFu8; 32])),
None
);
});
}

#[test]
fn insert_leaf_increments_total_commitments_counter() {
use crate::storage::PoolStatsRepository;
new_test_ext().execute_with(|| {
assert_eq!(
PoolStatsRepository::get_total_commitments_inserted::<Test>(),
0
);
MerkleTreeService::insert_leaf::<Test>(Commitment::new([0xF0u8; 32])).unwrap();
assert_eq!(
PoolStatsRepository::get_total_commitments_inserted::<Test>(),
1
);
MerkleTreeService::insert_leaf::<Test>(Commitment::new([0xF1u8; 32])).unwrap();
MerkleTreeService::insert_leaf::<Test>(Commitment::new([0xF2u8; 32])).unwrap();
assert_eq!(
PoolStatsRepository::get_total_commitments_inserted::<Test>(),
3
);
});
}

// ── Incremental frontier vs batch consistency ────────────────────────────

#[test]
Expand Down
24 changes: 21 additions & 3 deletions frame/shielded-pool/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,11 @@ impl ZkVerifierPort for MockZkVerifier {
"Invalid public signals length",
));
}
// Always return true for testing (bypass ZK verification)
// Sentinel: a proof whose first byte is 0x00 is treated as cryptographically
// rejected (simulates Groth16 returning false). All other non-empty proofs pass.
if proof[0] == 0x00 {
return Ok(false);
}
Ok(true)
}

Expand All @@ -116,7 +120,11 @@ impl ZkVerifierPort for MockZkVerifier {
if proofs.len() != public_signals.len() {
return Err(sp_runtime::DispatchError::Other("Mismatched array lengths"));
}
// Always return true for testing (bypass ZK verification)
// Sentinel: any proof in the batch starting with 0x00 causes the whole batch
// to return Ok(false), simulating a failed cryptographic batch verification.
if proofs.iter().any(|p| p.first() == Some(&0x00)) {
return Ok(false);
}
Ok(true)
}

Expand Down Expand Up @@ -197,6 +205,13 @@ pub fn mock_evm_address_set(who: u64, addr: sp_core::H160) {
sp_io::storage::set(&key, &addr.as_fixed_bytes().encode());
}

/// Write a minimum relay fee to raw test storage.
/// By default `MockRelayer::min_relay_fee()` returns 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());
}

impl pallet_relayer::RelayerInterface for MockRelayer {
type AccountId = u64;

Expand All @@ -206,7 +221,10 @@ impl pallet_relayer::RelayerInterface for MockRelayer {
}

fn min_relay_fee() -> u128 {
0
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]> {
Expand Down
Loading
Loading