diff --git a/client/rpc-v2/src/orbinum/application/dto/merkle_proof_response.rs b/client/rpc-v2/src/orbinum/application/dto/merkle_proof_response.rs index e5f66696..1e62e6dd 100644 --- a/client/rpc-v2/src/orbinum/application/dto/merkle_proof_response.rs +++ b/client/rpc-v2/src/orbinum/application/dto/merkle_proof_response.rs @@ -8,6 +8,8 @@ use serde::{Deserialize, Serialize}; /// It maps from `domain::MerkleProofPath`. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct MerkleProofResponse { + /// Current Merkle root (0x-prefixed hex), read from the same block as the proof. + pub root: String, /// Sibling hash path (hex strings). pub path: Vec, /// Leaf index. @@ -18,8 +20,9 @@ pub struct MerkleProofResponse { impl MerkleProofResponse { /// Creates a new `MerkleProofResponse`. - pub fn new(path: Vec, leaf_index: u32, tree_depth: u32) -> Self { + pub fn new(root: String, path: Vec, leaf_index: u32, tree_depth: u32) -> Self { Self { + root, path, leaf_index, tree_depth, @@ -33,9 +36,14 @@ mod tests { #[test] fn should_create_merkle_proof_response() { - let response = - MerkleProofResponse::new(vec!["0xaaaa".to_string(), "0xbbbb".to_string()], 7, 20); + let response = MerkleProofResponse::new( + "0x0000".to_string(), + vec!["0xaaaa".to_string(), "0xbbbb".to_string()], + 7, + 20, + ); + assert_eq!(response.root, "0x0000"); assert_eq!(response.path, vec!["0xaaaa", "0xbbbb"]); assert_eq!(response.leaf_index, 7); assert_eq!(response.tree_depth, 20); diff --git a/client/rpc-v2/src/orbinum/application/services/merkle_proof_service.rs b/client/rpc-v2/src/orbinum/application/services/merkle_proof_service.rs index 29fe9b10..8f32a4ea 100644 --- a/client/rpc-v2/src/orbinum/application/services/merkle_proof_service.rs +++ b/client/rpc-v2/src/orbinum/application/services/merkle_proof_service.rs @@ -166,6 +166,110 @@ where Ok((root, size, depth)) } + + /// Generates a Merkle proof by leaf index and returns the Merkle root, both + /// read under the **same `best_block`** (atomic — no risk of root/path mismatch). + /// + /// # Errors + /// - `InvalidLeafIndex`: If `leaf_index >= tree_size` + /// - `TreeNotInitialized`: If the tree is empty + pub fn generate_proof_with_root( + &self, + leaf_index: u32, + ) -> ApplicationResult<(MerkleProofPath, Commitment)> { + let block_hash = self.query.best_hash()?; + let tree_size = self.query.get_tree_size(block_hash)?; + + if tree_size.value() == 0 { + return Err(ApplicationError::TreeNotInitialized); + } + if leaf_index >= tree_size.value() { + return Err(ApplicationError::InvalidLeafIndex { + index: leaf_index, + tree_size: tree_size.value(), + }); + } + + let root = self.query.get_merkle_root(block_hash)?; + let path = self.collect_sibling_path(block_hash, leaf_index)?; + let tree_depth = TreeDepth::new(20); + + Ok((MerkleProofPath::new(path, leaf_index, tree_depth), root)) + } + + /// Generates a Merkle proof by commitment and returns the Merkle root, both + /// read under the **same `best_block`** (atomic — no risk of root/path mismatch). + /// + /// # Errors + /// - `CalculationError`: If the commitment is not found in the tree + /// - `TreeNotInitialized`: If the tree is empty + pub fn generate_proof_by_commitment_with_root( + &self, + target: Commitment, + ) -> ApplicationResult<(MerkleProofPath, Commitment)> { + let block_hash = self.query.best_hash()?; + let tree_size = self.query.get_tree_size(block_hash)?; + + if tree_size.value() == 0 { + return Err(ApplicationError::TreeNotInitialized); + } + + let mut found_index: Option = None; + for i in 0..tree_size.value() { + if self.query.get_leaf(block_hash, i)? == target { + found_index = Some(i); + break; + } + } + + let leaf_index = found_index.ok_or_else(|| { + ApplicationError::CalculationError("Commitment not found in Merkle tree".to_string()) + })?; + + let root = self.query.get_merkle_root(block_hash)?; + let path = self.collect_sibling_path(block_hash, leaf_index)?; + let tree_depth = TreeDepth::new(20); + + Ok((MerkleProofPath::new(path, leaf_index, tree_depth), root)) + } + + /// Generates a Merkle proof by scanning for a matching commitment in the tree leaves. + /// + /// # Parameters + /// - `target`: Commitment to search for + /// + /// # Returns + /// - `MerkleProofPath`: Sibling path with leaf index + /// + /// # Errors + /// - `InvalidLeafIndex`: If the commitment is not found in the tree + /// - `TreeNotInitialized`: If the tree is empty + /// - `Domain`: Storage query errors + pub fn generate_proof_by_commitment( + &self, + target: Commitment, + ) -> ApplicationResult { + let block_hash = self.query.best_hash()?; + let tree_size = self.query.get_tree_size(block_hash)?; + + if tree_size.value() == 0 { + return Err(ApplicationError::TreeNotInitialized); + } + + let mut found_index: Option = None; + for i in 0..tree_size.value() { + if self.query.get_leaf(block_hash, i)? == target { + found_index = Some(i); + break; + } + } + + let leaf_index = found_index.ok_or_else(|| { + ApplicationError::CalculationError("Commitment not found in Merkle tree".to_string()) + })?; + + self.generate_proof(leaf_index) + } } #[cfg(test)] diff --git a/client/rpc-v2/src/orbinum/presentation/api.rs b/client/rpc-v2/src/orbinum/presentation/api.rs index 029bcc5b..47291714 100644 --- a/client/rpc-v2/src/orbinum/presentation/api.rs +++ b/client/rpc-v2/src/orbinum/presentation/api.rs @@ -77,6 +77,26 @@ pub trait PrivacyApi { #[method(name = "privacy_getMerkleProof")] fn get_merkle_proof(&self, leaf_index: u32) -> RpcResult; + /// Returns a Merkle proof for a leaf identified by its commitment hash. + /// + /// # Parameters + /// - `commitment`: Commitment hash as 0x-prefixed hex string (64 hex chars) + /// + /// # Returns + /// - `MerkleProofResponse`: Proof with path, leaf index, and tree depth + /// + /// # Example + /// ```json + /// { + /// "jsonrpc": "2.0", + /// "method": "privacy_getMerkleProofByCommitment", + /// "params": ["0xabcd...1234"], + /// "id": 1 + /// } + /// ``` + #[method(name = "privacy_getMerkleProofByCommitment")] + fn get_merkle_proof_by_commitment(&self, commitment: String) -> RpcResult; + /// Checks whether a nullifier has been spent. /// /// # Parameters diff --git a/client/rpc-v2/src/orbinum/presentation/handlers/merkle_proof_handler.rs b/client/rpc-v2/src/orbinum/presentation/handlers/merkle_proof_handler.rs index b248119f..445d21cd 100644 --- a/client/rpc-v2/src/orbinum/presentation/handlers/merkle_proof_handler.rs +++ b/client/rpc-v2/src/orbinum/presentation/handlers/merkle_proof_handler.rs @@ -10,7 +10,7 @@ use crate::orbinum::{ presentation::validation::{RequestValidator, RpcError}, }; -/// Handler for `privacy_getMerkleProof`. +/// Handler for `privacy_getMerkleProof` and `privacy_getMerkleProofByCommitment`. pub struct MerkleProofHandler { merkle_service: Arc>, } @@ -24,7 +24,7 @@ where Self { merkle_service } } - /// Handles request to generate a Merkle proof. + /// Handles request to generate a Merkle proof by leaf index. /// /// # Parameters /// - `leaf_index`: Leaf index (0-indexed) @@ -39,23 +39,63 @@ where // 1. Validate input RequestValidator::validate_leaf_index(leaf_index)?; - // 2. Generate proof from service - let proof_path = self + // 2. Generate proof + root atomically from the same best_block + let (proof_path, root) = self .merkle_service - .generate_proof(leaf_index) + .generate_proof_with_root(leaf_index) .map_err(RpcError::from_application_error)?; - // 3. Map domain entity to DTO + // 3. Map domain entities to DTO let (path, leaf_idx, tree_depth) = proof_path.into_parts(); let path_hex: Vec = path .into_iter() .map(CommitmentMapper::to_hex_string) .collect(); + let root_hex = CommitmentMapper::to_hex_string(root); - let response = MerkleProofResponse::new(path_hex, leaf_idx, tree_depth.value()); + let response = MerkleProofResponse::new(root_hex, path_hex, leaf_idx, tree_depth.value()); Ok(response) } + + /// Handles request to generate a Merkle proof by commitment hex. + /// + /// # Parameters + /// - `commitment_hex`: Commitment as 0x-prefixed hex string + /// + /// # Returns + /// - `MerkleProofResponse`: DTO with path, leaf index, and tree depth + /// + /// # Errors + /// - `InvalidCommitment`: If the hex string is malformed + /// - `CalculationError`: If the commitment is not found in the tree + /// - `MerkleTreeNotInitialized`: If tree is not initialized + pub fn handle_by_commitment(&self, commitment_hex: String) -> RpcResult { + // 1. Parse commitment hex + let commitment = CommitmentMapper::from_hex_string(&commitment_hex) + .map_err(RpcError::invalid_commitment)?; + + // 2. Generate proof + root atomically from the same best_block + let (proof_path, root) = self + .merkle_service + .generate_proof_by_commitment_with_root(commitment) + .map_err(RpcError::from_application_error)?; + + // 3. Map domain entities to DTO + let (path, leaf_idx, tree_depth) = proof_path.into_parts(); + let path_hex: Vec = path + .into_iter() + .map(CommitmentMapper::to_hex_string) + .collect(); + let root_hex = CommitmentMapper::to_hex_string(root); + + Ok(MerkleProofResponse::new( + root_hex, + path_hex, + leaf_idx, + tree_depth.value(), + )) + } } #[cfg(test)] diff --git a/client/rpc-v2/src/orbinum/presentation/server.rs b/client/rpc-v2/src/orbinum/presentation/server.rs index b62c0f96..6cecfe84 100644 --- a/client/rpc-v2/src/orbinum/presentation/server.rs +++ b/client/rpc-v2/src/orbinum/presentation/server.rs @@ -105,6 +105,10 @@ where self.merkle_proof_handler.handle(leaf_index) } + fn get_merkle_proof_by_commitment(&self, commitment: String) -> RpcResult { + self.merkle_proof_handler.handle_by_commitment(commitment) + } + fn get_nullifier_status(&self, nullifier: String) -> RpcResult { self.nullifier_handler.handle(nullifier) }