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
4 changes: 2 additions & 2 deletions Cargo.lock

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

65 changes: 64 additions & 1 deletion client/rpc-v2/src/privacy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use pallet_shielded_pool_runtime_api::ShieldedPoolRuntimeApi;
use sc_client_api::StorageProvider as ScStorageProvider;
use scale_codec::Decode;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use sp_api::ProvideRuntimeApi;
use sp_api::{ApiExt, ProvideRuntimeApi};

/// `u128` serde helper: serialises as a decimal string so JavaScript clients
/// can parse values larger than `Number.MAX_SAFE_INTEGER` without precision loss.
Expand Down Expand Up @@ -73,6 +73,26 @@ fn map_key(item: &[u8], k: &[u8]) -> Vec<u8> {
// Response types
// ============================================================================

/// Level-6 subtree roots of one tree (`privacy_getSubtreeRoots`).
///
/// Every root but the last of an active tree is final. Past the last block the
/// nodes are the zero hash; the response omits them.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SubtreeRootsResponse {
pub tree_id: u32,
/// Level of the roots: each covers `2^level` leaves.
pub level: u8,
/// Leaves in the tree: the capacity once sealed.
pub tree_leaves: u32,
pub sealed: bool,
/// Root the tree anchors to at the same state (`0x`-prefixed hex).
pub root: String,
/// Index of `roots[0]` within the level.
pub start: u32,
/// `0x`-prefixed hex, little-endian field elements.
pub roots: Vec<String>,
}

/// Response for `privacy_getMerkleProof`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MerkleProofResponse {
Expand Down Expand Up @@ -144,6 +164,17 @@ pub trait PrivacyApi {
#[method(name = "privacy_getMerkleProofByCommitment", blocking)]
fn get_merkle_proof_by_commitment(&self, commitment: String) -> RpcResult<MerkleProofResponse>;

/// Up to `count` (at most 4096) level-6 subtree roots of tree `tree_id` from
/// `start`, with the root the tree anchors to, so a wallet can build Merkle
/// paths itself. Needs runtime API v4.
#[method(name = "privacy_getSubtreeRoots", blocking)]
fn get_subtree_roots(
&self,
tree_id: u32,
start: u32,
count: u32,
) -> RpcResult<SubtreeRootsResponse>;

/// Returns whether the nullifier (`0x`-prefixed hex, 32 bytes) has been spent.
#[method(name = "privacy_getNullifierStatus")]
fn get_nullifier_status(&self, nullifier: String) -> RpcResult<NullifierStatusResponse>;
Expand Down Expand Up @@ -461,6 +492,38 @@ where
})
}

fn get_subtree_roots(
&self,
tree_id: u32,
start: u32,
count: u32,
) -> RpcResult<SubtreeRootsResponse> {
let _permit = self.proofs.enter().ok_or_else(busy)?;
let best_hash = self.client.info().best_hash;
let api = self.client.runtime_api();
let version = api
.api_version::<dyn ShieldedPoolRuntimeApi<B>>(best_hash)
.map_err(internal_error)?
.unwrap_or(0);
if version < 4 {
return Err(internal_error("subtree roots need runtime API v4"));
}
let found = api
.get_subtree_roots(best_hash, tree_id, start, count)
.map_err(internal_error)?
.ok_or_else(|| invalid_params(format!("tree {tree_id} does not exist")))?;
let hex32 = |h: [u8; 32]| format!("0x{}", hex::encode(h));
Ok(SubtreeRootsResponse {
tree_id: found.tree_id,
level: pallet_shielded_pool::SUBTREE_LEVEL,
tree_leaves: found.tree_leaves,
sealed: found.sealed,
root: hex32(found.root),
start: found.start,
roots: found.roots.into_iter().map(hex32).collect(),
})
}

fn get_nullifier_status(&self, nullifier: String) -> RpcResult<NullifierStatusResponse> {
let best_hash = self.client.info().best_hash;

Expand Down
11 changes: 11 additions & 0 deletions frame/shielded-pool/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ All notable changes to `pallet-shielded-pool` will be documented in this file.

## [Unreleased]

## [0.24.0] - 2026-10-09

### Added

- `get_subtree_roots(tree_id, start, count)` and `SubtreeRoots`: up to
`MAX_SUBTREE_ROOTS` (4096) level-`SUBTREE_LEVEL` (6) roots of one tree, with
the root it anchors to, read from one state. A wallet builds its own Merkle
path from them and the 64 leaves of its note's block, so no node learns which
note is spent. A sealed tree's node pruned under an earlier, higher cut is
rebuilt from its leaves. Runtime API v4 (`ShieldedPoolRuntimeApi`).

## [0.23.0] - 2026-10-08

### Changed
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.23.0"
version = "0.24.0"
description = "Shielded pool pallet for private transactions using ZK proofs"
authors = ["Orbinum Team"]
license = "GPL-3.0-or-later"
Expand Down
2 changes: 1 addition & 1 deletion frame/shielded-pool/runtime-api/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pallet-shielded-pool-runtime-api"
version = "0.2.0"
version = "0.3.0"
authors = ["Orbinum Team"]
edition = "2024"
license = "GPL-3.0-or-later"
Expand Down
8 changes: 7 additions & 1 deletion frame/shielded-pool/runtime-api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#![cfg_attr(not(feature = "std"), no_std)]

use pallet_shielded_pool::{DefaultMerklePath, Hash};
use pallet_shielded_pool::{DefaultMerklePath, Hash, SubtreeRoots};

/// Configuration exposed by the runtime for the node-native EVM relay.
///
Expand Down Expand Up @@ -60,5 +60,11 @@ sp_api::decl_runtime_apis! {
/// calldata that is not a relayable spend.
#[api_version(3)]
fn relay_commit_hash(calldata: sp_std::vec::Vec<u8>, relayer: [u8; 20]) -> Option<[u8; 32]>;

/// Up to `count` level-6 subtree roots of `tree_id` from `start`, with the
/// root the tree anchors to, read from one state, so a wallet can build
/// its own Merkle paths. None for a tree that does not exist yet.
#[api_version(4)]
fn get_subtree_roots(tree_id: u32, start: u32, count: u32) -> Option<SubtreeRoots>;
}
}
3 changes: 2 additions & 1 deletion frame/shielded-pool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ pub use pallet::*;
pub use types::{
AssetId, AssetMetadata, Commitment, DEFAULT_TREE_DEPTH, DefaultMerklePath,
EncryptedMemo as FrameEncryptedMemo, Hash, MAX_ENCRYPTED_MEMO_SIZE, MAX_PROOF_SIZE,
MAX_TREE_DEPTH, MerklePath, Note, Nullifier, Proof,
MAX_SUBTREE_ROOTS, MAX_TREE_DEPTH, MerklePath, Note, Nullifier, Proof, SUBTREE_LEVEL,
SubtreeRoots,
};
pub use weights::WeightInfo;

Expand Down
46 changes: 46 additions & 0 deletions frame/shielded-pool/src/merkle/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,52 @@ impl MerkleTreeService {
Some(DefaultMerklePath { siblings, indices })
}

/// Up to `count` (capped at [`MAX_SUBTREE_ROOTS`]) level-[`SUBTREE_LEVEL`]
/// roots of `tree_id` from `start`, with the tree's anchoring root, all read
/// from the current state. `None` for a tree that does not exist yet.
///
/// Only blocks holding at least one leaf are returned: past them every node
/// is the zero hash, which the caller knows. A node missing from storage (a
/// sealed tree pruned under an earlier, higher cut) is rebuilt from leaves.
pub fn get_subtree_roots<T: Config>(
tree_id: u32,
start: u32,
count: u32,
) -> Option<crate::types::SubtreeRoots> {
use crate::types::{MAX_SUBTREE_ROOTS, SUBTREE_LEVEL, SubtreeRoots};
let size = MerkleRepository::get_tree_size::<T>();
let cap = T::MaxLeavesPerTree::get();
let current = size / cap;
if tree_id > current {
return None;
}
let sealed = tree_id < current;
let (tree_leaves, root) = if sealed {
(cap, MerkleRepository::get_sealed_root::<T>(tree_id)?)
} else {
(size % cap, MerkleRepository::get_poseidon_root::<T>())
};
let level = SUBTREE_LEVEL as usize;
let blocks = tree_leaves.div_ceil(1 << level);
let end = start
.saturating_add(count.min(MAX_SUBTREE_ROOTS))
.min(blocks);
let roots = (start..end)
.map(|i| {
MerkleRepository::get_node::<T>(tree_id, SUBTREE_LEVEL, i)
.unwrap_or_else(|| Self::subtree_root::<T>(tree_id, level, i, cap))
})
.collect();
Some(SubtreeRoots {
tree_id,
tree_leaves,
sealed,
root,
start,
roots,
})
}

/// Rebuild the node at `(level, node_index)` from the leaves beneath it.
///
/// Reads the `2^level` leaves the node spans and folds them pairwise. Used only
Expand Down
Loading
Loading