diff --git a/Cargo.lock b/Cargo.lock index 4efe3de46..71af23352 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6171,6 +6171,7 @@ dependencies = [ "tower", "tracing", "zcash_address", + "zcash_primitives 0.13.0", "zebra-chain", "zebra-consensus", "zebra-network", diff --git a/zebra-chain/src/orchard/tree.rs b/zebra-chain/src/orchard/tree.rs index 25cde25d9..b3433a99c 100644 --- a/zebra-chain/src/orchard/tree.rs +++ b/zebra-chain/src/orchard/tree.rs @@ -15,7 +15,6 @@ use std::{ fmt, hash::{Hash, Hasher}, io, - sync::Arc, }; use bitvec::prelude::*; @@ -25,7 +24,7 @@ use hex::ToHex; use incrementalmerkletree::Hashable; use lazy_static::lazy_static; use thiserror::Error; -use zcash_primitives::merkle_tree::{write_commitment_tree, HashSer}; +use zcash_primitives::merkle_tree::HashSer; use super::sinsemilla::*; @@ -243,7 +242,7 @@ impl ToHex for Node { } } -/// Required to convert [`NoteCommitmentTree`] into [`SerializedTree`]. +/// Required to serialize [`NoteCommitmentTree`]s in a format compatible with `zcashd`. /// /// Zebra stores Orchard note commitment trees as [`Frontier`][1]s while the /// [`z_gettreestate`][2] RPC requires [`CommitmentTree`][3]s. Implementing @@ -633,7 +632,21 @@ impl NoteCommitmentTree { assert_eq!(self.inner, other.inner); // Check the RPC serialization format (not the same as the Zebra database format) - assert_eq!(SerializedTree::from(self), SerializedTree::from(other)); + assert_eq!(self.to_rpc_bytes(), other.to_rpc_bytes()); + } + + /// Serializes [`Self`] to a format compatible with `zcashd`'s RPCs. + pub fn to_rpc_bytes(&self) -> Vec { + // Convert the tree from [`Frontier`](bridgetree::Frontier) to + // [`CommitmentTree`](merkle_tree::CommitmentTree). + let tree = incrementalmerkletree::frontier::CommitmentTree::from_frontier(&self.inner); + + let mut rpc_bytes = vec![]; + + zcash_primitives::merkle_tree::write_commitment_tree(&tree, &mut rpc_bytes) + .expect("serializable tree"); + + rpc_bytes } } @@ -688,68 +701,3 @@ impl From> for NoteCommitmentTree { tree } } - -/// A serialized Orchard note commitment tree. -/// -/// The format of the serialized data is compatible with -/// [`CommitmentTree`](incrementalmerkletree::frontier::CommitmentTree) from `librustzcash` and not -/// with [`Frontier`](bridgetree::Frontier) from the crate -/// [`incrementalmerkletree`]. Zebra follows the former format in order to stay -/// consistent with `zcashd` in RPCs. Note that [`NoteCommitmentTree`] itself is -/// represented as [`Frontier`](bridgetree::Frontier). -/// -/// The formats are semantically equivalent. The primary difference between them -/// is that in [`Frontier`](bridgetree::Frontier), the vector of parents is -/// dense (we know where the gaps are from the position of the leaf in the -/// overall tree); whereas in [`CommitmentTree`](incrementalmerkletree::frontier::CommitmentTree), -/// the vector of parent hashes is sparse with [`None`] values in the gaps. -/// -/// The sparse format, used in this implementation, allows representing invalid -/// commitment trees while the dense format allows representing only valid -/// commitment trees. -/// -/// It is likely that the dense format will be used in future RPCs, in which -/// case the current implementation will have to change and use the format -/// compatible with [`Frontier`](bridgetree::Frontier) instead. -#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize)] -pub struct SerializedTree(Vec); - -impl From<&NoteCommitmentTree> for SerializedTree { - fn from(tree: &NoteCommitmentTree) -> Self { - let mut serialized_tree = vec![]; - - // Skip the serialization of empty trees. - // - // Note: This ensures compatibility with `zcashd` in the - // [`z_gettreestate`][1] RPC. - // - // [1]: https://zcash.github.io/rpc/z_gettreestate.html - if tree.inner == bridgetree::Frontier::empty() { - return Self(serialized_tree); - } - - // Convert the note commitment tree from - // [`Frontier`](bridgetree::Frontier) to - // [`CommitmentTree`](merkle_tree::CommitmentTree). - let tree = incrementalmerkletree::frontier::CommitmentTree::from_frontier(&tree.inner); - - write_commitment_tree(&tree, &mut serialized_tree) - .expect("note commitment tree should be serializable"); - Self(serialized_tree) - } -} - -impl From>> for SerializedTree { - fn from(maybe_tree: Option>) -> Self { - match maybe_tree { - Some(tree) => tree.as_ref().into(), - None => Self(Vec::new()), - } - } -} - -impl AsRef<[u8]> for SerializedTree { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} diff --git a/zebra-chain/src/sapling/tree.rs b/zebra-chain/src/sapling/tree.rs index 7b137422b..519d7deee 100644 --- a/zebra-chain/src/sapling/tree.rs +++ b/zebra-chain/src/sapling/tree.rs @@ -15,7 +15,6 @@ use std::{ fmt, hash::{Hash, Hasher}, io, - sync::Arc, }; use bitvec::prelude::*; @@ -25,7 +24,6 @@ use incrementalmerkletree::{frontier::Frontier, Hashable}; use lazy_static::lazy_static; use thiserror::Error; -use zcash_encoding::{Optional, Vector}; use zcash_primitives::merkle_tree::HashSer; use super::commitment::pedersen_hashes::pedersen_hash; @@ -38,7 +36,7 @@ use crate::{ }; pub mod legacy; -use legacy::{LegacyLeaf, LegacyNoteCommitmentTree}; +use legacy::LegacyNoteCommitmentTree; /// The type that is used to update the note commitment tree. /// @@ -219,7 +217,7 @@ impl ToHex for Node { } } -/// Required to convert [`NoteCommitmentTree`] into [`SerializedTree`]. +/// Required to serialize [`NoteCommitmentTree`]s in a format matching `zcashd`. /// /// Zebra stores Sapling note commitment trees as [`Frontier`]s while the /// [`z_gettreestate`][1] RPC requires [`CommitmentTree`][2]s. Implementing @@ -614,7 +612,21 @@ impl NoteCommitmentTree { assert_eq!(self.inner, other.inner); // Check the RPC serialization format (not the same as the Zebra database format) - assert_eq!(SerializedTree::from(self), SerializedTree::from(other)); + assert_eq!(self.to_rpc_bytes(), other.to_rpc_bytes()); + } + + /// Serializes [`Self`] to a format matching `zcashd`'s RPCs. + pub fn to_rpc_bytes(&self) -> Vec { + // Convert the tree from [`Frontier`](bridgetree::Frontier) to + // [`CommitmentTree`](merkle_tree::CommitmentTree). + let tree = incrementalmerkletree::frontier::CommitmentTree::from_frontier(&self.inner); + + let mut rpc_bytes = vec![]; + + zcash_primitives::merkle_tree::write_commitment_tree(&tree, &mut rpc_bytes) + .expect("serializable tree"); + + rpc_bytes } } @@ -670,135 +682,3 @@ impl From> for NoteCommitmentTree { tree } } - -/// A serialized Sapling note commitment tree. -/// -/// The format of the serialized data is compatible with -/// [`CommitmentTree`](incrementalmerkletree::frontier::CommitmentTree) from `librustzcash` and not -/// with [`Frontier`] from the crate -/// [`incrementalmerkletree`]. Zebra follows the former format in order to stay -/// consistent with `zcashd` in RPCs. Note that [`NoteCommitmentTree`] itself is -/// represented as [`Frontier`]. -/// -/// The formats are semantically equivalent. The primary difference between them -/// is that in [`Frontier`], the vector of parents is -/// dense (we know where the gaps are from the position of the leaf in the -/// overall tree); whereas in [`CommitmentTree`](incrementalmerkletree::frontier::CommitmentTree), -/// the vector of parent hashes is sparse with [`None`] values in the gaps. -/// -/// The sparse format, used in this implementation, allows representing invalid -/// commitment trees while the dense format allows representing only valid -/// commitment trees. -/// -/// It is likely that the dense format will be used in future RPCs, in which -/// case the current implementation will have to change and use the format -/// compatible with [`Frontier`] instead. -#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize)] -pub struct SerializedTree(Vec); - -impl From<&NoteCommitmentTree> for SerializedTree { - fn from(tree: &NoteCommitmentTree) -> Self { - let mut serialized_tree = vec![]; - - // - let legacy_tree = LegacyNoteCommitmentTree::from(tree.clone()); - - // Convert the note commitment tree represented as a frontier into the - // format compatible with `zcashd`. - // - // `librustzcash` has a function [`from_frontier()`][1], which returns a - // commitment tree in the sparse format. However, the returned tree - // always contains [`MERKLE_DEPTH`] parent nodes, even though some - // trailing parents are empty. Such trees are incompatible with Sapling - // commitment trees returned by `zcashd` because `zcashd` returns - // Sapling commitment trees without empty trailing parents. For this - // reason, Zebra implements its own conversion between the dense and - // sparse formats for Sapling. - // - // [1]: - if let Some(frontier) = legacy_tree.inner.frontier { - let (left_leaf, right_leaf) = match frontier.leaf { - LegacyLeaf::Left(left_value) => (Some(left_value), None), - LegacyLeaf::Right(left_value, right_value) => (Some(left_value), Some(right_value)), - }; - - // Ommers are siblings of parent nodes along the branch from the - // most recent leaf to the root of the tree. - let mut ommers_iter = frontier.ommers.iter(); - - // Set bits in the binary representation of the position indicate - // the presence of ommers along the branch from the most recent leaf - // node to the root of the tree, except for the lowest bit. - let mut position: u64 = (frontier.position.0) - .try_into() - .expect("old usize position always fit in u64"); - - // The lowest bit does not indicate the presence of any ommers. We - // clear it so that we can test if there are no set bits left in - // [`position`]. - position &= !1; - - // Run through the bits of [`position`], and push an ommer for each - // set bit, or `None` otherwise. In contrast to the 'zcashd' code - // linked above, we want to skip any trailing `None` parents at the - // top of the tree. To do that, we clear the bits as we go through - // them, and break early if the remaining bits are all zero (i.e. - // [`position`] is zero). - let mut parents = vec![]; - for i in 1..MERKLE_DEPTH { - // Test each bit in [`position`] individually. Don't test the - // lowest bit since it doesn't actually indicate the position of - // any ommer. - let bit_mask = 1 << i; - - if position & bit_mask == 0 { - parents.push(None); - } else { - parents.push(ommers_iter.next()); - // Clear the set bit so that we can test if there are no set - // bits left. - position &= !bit_mask; - // If there are no set bits left, exit early so that there - // are no empty trailing parent nodes in the serialized - // tree. - if position == 0 { - break; - } - } - } - - // Serialize the converted note commitment tree. - Optional::write(&mut serialized_tree, left_leaf, |tree, leaf| { - leaf.write(tree) - }) - .expect("A leaf in a note commitment tree should be serializable"); - - Optional::write(&mut serialized_tree, right_leaf, |tree, leaf| { - leaf.write(tree) - }) - .expect("A leaf in a note commitment tree should be serializable"); - - Vector::write(&mut serialized_tree, &parents, |tree, parent| { - Optional::write(tree, *parent, |tree, parent| parent.write(tree)) - }) - .expect("Parent nodes in a note commitment tree should be serializable"); - } - - Self(serialized_tree) - } -} - -impl From>> for SerializedTree { - fn from(maybe_tree: Option>) -> Self { - match maybe_tree { - Some(tree) => tree.as_ref().into(), - None => Self(vec![]), - } - } -} - -impl AsRef<[u8]> for SerializedTree { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} diff --git a/zebra-rpc/Cargo.toml b/zebra-rpc/Cargo.toml index 59762cd68..2f8288fe4 100644 --- a/zebra-rpc/Cargo.toml +++ b/zebra-rpc/Cargo.toml @@ -64,6 +64,8 @@ tracing = "0.1.39" hex = { version = "0.4.3", features = ["serde"] } serde = { version = "1.0.202", features = ["serde_derive"] } +zcash_primitives = { version = "0.13.0" } + # Experimental feature getblocktemplate-rpcs rand = { version = "0.8.5", optional = true } # ECC deps used by getblocktemplate-rpcs feature diff --git a/zebra-rpc/src/methods.rs b/zebra-rpc/src/methods.rs index 92c7aa22a..0ff129a64 100644 --- a/zebra-rpc/src/methods.rs +++ b/zebra-rpc/src/methods.rs @@ -18,13 +18,12 @@ use tokio::{sync::broadcast, task::JoinHandle}; use tower::{Service, ServiceExt}; use tracing::Instrument; +use zcash_primitives::consensus::Parameters; use zebra_chain::{ block::{self, Height, SerializedBlock}, chain_tip::ChainTip, - orchard, parameters::{ConsensusBranchId, Network, NetworkUpgrade}, - sapling, - serialization::{SerializationError, ZcashDeserialize}, + serialization::ZcashDeserialize, subtree::NoteCommitmentSubtreeIndex, transaction::{self, SerializedTransaction, Transaction, UnminedTx}, transparent::{self, Address}, @@ -34,7 +33,7 @@ use zebra_state::{HashOrHeight, MinedTx, OutputIndex, OutputLocation, Transactio use crate::{ constants::{INVALID_PARAMETERS_ERROR_CODE, MISSING_BLOCK_ERROR_CODE}, - methods::trees::{GetSubtrees, SubtreeRpcData}, + methods::trees::{GetSubtrees, GetTreestate, SubtreeRpcData}, queue::Queue, }; @@ -504,30 +503,18 @@ where let (tip_height, tip_hash) = self .latest_chain_tip .best_tip_height_and_hash() - .ok_or_else(|| Error { - code: ErrorCode::ServerError(0), - message: "No Chain tip available yet".to_string(), - data: None, - })?; + .ok_or_server_error("No Chain tip available yet")?; // `estimated_height` field - let current_block_time = - self.latest_chain_tip - .best_tip_block_time() - .ok_or_else(|| Error { - code: ErrorCode::ServerError(0), - message: "No Chain tip available yet".to_string(), - data: None, - })?; + let current_block_time = self + .latest_chain_tip + .best_tip_block_time() + .ok_or_server_error("No Chain tip available yet")?; let zebra_estimated_height = self .latest_chain_tip .estimate_network_chain_tip_height(network, Utc::now()) - .ok_or_else(|| Error { - code: ErrorCode::ServerError(0), - message: "No Chain tip available yet".to_string(), - data: None, - })?; + .ok_or_server_error("No Chain tip available yet")?; let mut estimated_height = if current_block_time > Utc::now() || zebra_estimated_height < tip_height { @@ -606,11 +593,7 @@ where let valid_addresses = address_strings.valid_addresses()?; let request = zebra_state::ReadRequest::AddressBalance(valid_addresses); - let response = state.oneshot(request).await.map_err(|error| Error { - code: ErrorCode::ServerError(0), - message: error.to_string(), - data: None, - })?; + let response = state.oneshot(request).await.map_server_error()?; match response { zebra_state::ReadResponse::AddressBalance(balance) => Ok(AddressBalance { @@ -647,11 +630,7 @@ where let transaction_parameter = mempool::Gossip::Tx(raw_transaction.into()); let request = mempool::Request::Queue(vec![transaction_parameter]); - let response = mempool.oneshot(request).await.map_err(|error| Error { - code: ErrorCode::ServerError(0), - message: error.to_string(), - data: None, - })?; + let response = mempool.oneshot(request).await.map_server_error()?; let queue_results = match response { mempool::Response::Queued(results) => results, @@ -666,14 +645,10 @@ where tracing::debug!("sent transaction to mempool: {:?}", &queue_results[0]); - match &queue_results[0] { - Ok(()) => Ok(SentTransactionHash(transaction_hash)), - Err(error) => Err(Error { - code: ErrorCode::ServerError(0), - message: error.to_string(), - data: None, - }), - } + queue_results[0] + .as_ref() + .map(|_| SentTransactionHash(transaction_hash)) + .map_server_error() } .boxed() } @@ -681,7 +656,6 @@ where // TODO: // - use `height_from_signed_int()` to handle negative heights // (this might be better in the state request, because it needs the state height) - // - create a function that handles block hashes or heights, and use it in `z_get_treestate()` fn get_block( &self, hash_or_height: String, @@ -694,14 +668,7 @@ where let verbosity = verbosity.unwrap_or(DEFAULT_GETBLOCK_VERBOSITY); async move { - let hash_or_height: HashOrHeight = - hash_or_height - .parse() - .map_err(|error: SerializationError| Error { - code: ErrorCode::ServerError(0), - message: error.to_string(), - data: None, - })?; + let hash_or_height: HashOrHeight = hash_or_height.parse().map_server_error()?; if verbosity == 0 { // # Performance @@ -713,11 +680,7 @@ where .ready() .and_then(|service| service.call(request)) .await - .map_err(|error| Error { - code: ErrorCode::ServerError(0), - message: error.to_string(), - data: None, - })?; + .map_server_error()?; match response { zebra_state::ReadResponse::Block(Some(block)) => { @@ -761,11 +724,7 @@ where .ready() .and_then(|service| service.call(request)) .await - .map_err(|error| Error { - code: ErrorCode::ServerError(0), - message: error.to_string(), - data: None, - })?; + .map_server_error()?; match response { zebra_state::ReadResponse::BlockHash(Some(hash)) => hash, @@ -913,11 +872,7 @@ where self.latest_chain_tip .best_tip_hash() .map(GetBlockHash) - .ok_or(Error { - code: ErrorCode::ServerError(0), - message: "No blocks in state".to_string(), - data: None, - }) + .ok_or_server_error("No blocks in state") } // TODO: use a generic error constructor (#5548) @@ -947,11 +902,7 @@ where .ready() .and_then(|service| service.call(request)) .await - .map_err(|error| Error { - code: ErrorCode::ServerError(0), - message: error.to_string(), - data: None, - })?; + .map_server_error()?; match response { #[cfg(feature = "getblocktemplate-rpcs")] @@ -1030,11 +981,7 @@ where .ready() .and_then(|service| service.call(request)) .await - .map_err(|error| Error { - code: ErrorCode::ServerError(0), - message: error.to_string(), - data: None, - })?; + .map_server_error()?; match response { mempool::Response::Transactions(unmined_transactions) => { @@ -1052,11 +999,7 @@ where .ready() .and_then(|service| service.call(request)) .await - .map_err(|error| Error { - code: ErrorCode::ServerError(0), - message: error.to_string(), - data: None, - })?; + .map_server_error()?; match response { zebra_state::ReadResponse::Transaction(Some(MinedTx { @@ -1069,11 +1012,9 @@ where confirmations, verbose, )), - zebra_state::ReadResponse::Transaction(None) => Err(Error { - code: ErrorCode::ServerError(0), - message: "Transaction not found".to_string(), - data: None, - }), + zebra_state::ReadResponse::Transaction(None) => { + Err("Transaction not found").map_server_error() + } _ => unreachable!("unmatched response to a transaction request"), } } @@ -1081,47 +1022,30 @@ where } // TODO: - // - use a generic error constructor (#5548) // - use `height_from_signed_int()` to handle negative heights // (this might be better in the state request, because it needs the state height) - // - create a function that handles block hashes or heights, and use it in `get_block()` fn z_get_treestate(&self, hash_or_height: String) -> BoxFuture> { let mut state = self.state.clone(); + let network = self.network.clone(); async move { // Convert the [`hash_or_height`] string into an actual hash or height. - let hash_or_height = hash_or_height - .parse() - .map_err(|error: SerializationError| Error { - code: ErrorCode::ServerError(0), - message: error.to_string(), - data: None, - })?; - - // # Concurrency - // - // For consistency, this lookup must be performed first, then all the other - // lookups must be based on the hash. + let hash_or_height = hash_or_height.parse().map_server_error()?; // Fetch the block referenced by [`hash_or_height`] from the state. - // TODO: If this RPC is called a lot, just get the block header, - // rather than the whole block. - let block_request = zebra_state::ReadRequest::Block(hash_or_height); - let block_response = state + // + // # Concurrency + // + // For consistency, this lookup must be performed first, then all the other lookups must + // be based on the hash. + // + // TODO: If this RPC is called a lot, just get the block header, rather than the whole block. + let block = match state .ready() - .and_then(|service| service.call(block_request)) + .and_then(|service| service.call(zebra_state::ReadRequest::Block(hash_or_height))) .await - .map_err(|error| Error { - code: ErrorCode::ServerError(0), - message: error.to_string(), - data: None, - })?; - - // The block hash, height, and time are all required fields in the - // RPC response. For this reason, we throw an error early if the - // state didn't return the requested block so that we prevent - // further state queries. - let block = match block_response { + .map_server_error()? + { zebra_state::ReadResponse::Block(Some(block)) => block, zebra_state::ReadResponse::Block(None) => { return Err(Error { @@ -1133,73 +1057,54 @@ where _ => unreachable!("unmatched response to a block request"), }; - let hash = hash_or_height.hash().unwrap_or_else(|| block.hash()); - let hash_or_height = hash.into(); + let hash = hash_or_height + .hash_or_else(|_| Some(block.hash())) + .expect("block hash"); - // Fetch the Sapling & Orchard treestates referenced by - // [`hash_or_height`] from the state. - - let sapling_request = zebra_state::ReadRequest::SaplingTree(hash_or_height); - let sapling_response = state - .ready() - .and_then(|service| service.call(sapling_request)) - .await - .map_err(|error| Error { - code: ErrorCode::ServerError(0), - message: error.to_string(), - data: None, - })?; - - let orchard_request = zebra_state::ReadRequest::OrchardTree(hash_or_height); - let orchard_response = state - .ready() - .and_then(|service| service.call(orchard_request)) - .await - .map_err(|error| Error { - code: ErrorCode::ServerError(0), - message: error.to_string(), - data: None, - })?; - - // We've got all the data we need for the RPC response, so we - // assemble the response. - - let height = block - .coinbase_height() - .expect("verified blocks have a valid height"); + let height = hash_or_height + .height_or_else(|_| block.coinbase_height()) + .expect("verified blocks have a coinbase height"); let time = u32::try_from(block.header.time.timestamp()) .expect("Timestamps of valid blocks always fit into u32."); - let sapling_tree = match sapling_response { - zebra_state::ReadResponse::SaplingTree(maybe_tree) => { - sapling::tree::SerializedTree::from(maybe_tree) + let sapling_nu = zcash_primitives::consensus::NetworkUpgrade::Sapling; + let sapling = if network.is_nu_active(sapling_nu, height.into()) { + match state + .ready() + .and_then(|service| { + service.call(zebra_state::ReadRequest::SaplingTree(hash.into())) + }) + .await + .map_server_error()? + { + zebra_state::ReadResponse::SaplingTree(tree) => tree.map(|t| t.to_rpc_bytes()), + _ => unreachable!("unmatched response to a Sapling tree request"), } - _ => unreachable!("unmatched response to a sapling tree request"), + } else { + None }; - let orchard_tree = match orchard_response { - zebra_state::ReadResponse::OrchardTree(maybe_tree) => { - orchard::tree::SerializedTree::from(maybe_tree) + let orchard_nu = zcash_primitives::consensus::NetworkUpgrade::Nu5; + let orchard = if network.is_nu_active(orchard_nu, height.into()) { + match state + .ready() + .and_then(|service| { + service.call(zebra_state::ReadRequest::OrchardTree(hash.into())) + }) + .await + .map_server_error()? + { + zebra_state::ReadResponse::OrchardTree(tree) => tree.map(|t| t.to_rpc_bytes()), + _ => unreachable!("unmatched response to an Orchard tree request"), } - _ => unreachable!("unmatched response to an orchard tree request"), + } else { + None }; - Ok(GetTreestate { - hash, - height, - time, - sapling: Treestate { - commitments: Commitments { - final_state: sapling_tree, - }, - }, - orchard: Treestate { - commitments: Commitments { - final_state: orchard_tree, - }, - }, - }) + Ok(GetTreestate::from_parts( + hash, height, time, sapling, orchard, + )) } .boxed() } @@ -1221,11 +1126,7 @@ where .ready() .and_then(|service| service.call(request)) .await - .map_err(|error| Error { - code: ErrorCode::ServerError(0), - message: error.to_string(), - data: None, - })?; + .map_server_error()?; let subtrees = match response { zebra_state::ReadResponse::SaplingSubtrees(subtrees) => subtrees, @@ -1251,11 +1152,7 @@ where .ready() .and_then(|service| service.call(request)) .await - .map_err(|error| Error { - code: ErrorCode::ServerError(0), - message: error.to_string(), - data: None, - })?; + .map_server_error()?; let subtrees = match response { zebra_state::ReadResponse::OrchardSubtrees(subtrees) => subtrees, @@ -1316,11 +1213,7 @@ where .ready() .and_then(|service| service.call(request)) .await - .map_err(|error| Error { - code: ErrorCode::ServerError(0), - message: error.to_string(), - data: None, - })?; + .map_server_error()?; let hashes = match response { zebra_state::ReadResponse::AddressesTransactionIds(hashes) => { @@ -1368,11 +1261,7 @@ where .ready() .and_then(|service| service.call(request)) .await - .map_err(|error| Error { - code: ErrorCode::ServerError(0), - message: error.to_string(), - data: None, - })?; + .map_server_error()?; let utxos = match response { zebra_state::ReadResponse::AddressUtxos(utxos) => utxos, _ => unreachable!("unmatched response to a UtxosByAddresses request"), @@ -1422,11 +1311,9 @@ pub fn best_chain_tip_height(latest_chain_tip: &Tip) -> Result where Tip: ChainTip + Clone + Send + Sync + 'static, { - latest_chain_tip.best_tip_height().ok_or(Error { - code: ErrorCode::ServerError(0), - message: "No blocks in state".to_string(), - data: None, - }) + latest_chain_tip + .best_tip_height() + .ok_or_server_error("No blocks in state") } /// Response to a `getinfo` RPC request. @@ -1660,85 +1547,6 @@ impl Default for GetBlockHash { } } -/// Response to a `z_gettreestate` RPC request. -/// -/// Contains the hex-encoded Sapling & Orchard note commitment trees, and their -/// corresponding [`block::Hash`], [`Height`], and block time. -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] -pub struct GetTreestate { - /// The block hash corresponding to the treestate, hex-encoded. - #[serde(with = "hex")] - hash: block::Hash, - - /// The block height corresponding to the treestate, numeric. - height: Height, - - /// Unix time when the block corresponding to the treestate was mined, - /// numeric. - /// - /// UTC seconds since the Unix 1970-01-01 epoch. - time: u32, - - /// A treestate containing a Sapling note commitment tree, hex-encoded. - #[serde(skip_serializing_if = "Treestate::is_empty")] - sapling: Treestate, - - /// A treestate containing an Orchard note commitment tree, hex-encoded. - #[serde(skip_serializing_if = "Treestate::is_empty")] - orchard: Treestate, -} - -impl Default for GetTreestate { - fn default() -> Self { - GetTreestate { - hash: block::Hash([0; 32]), - height: Height(0), - time: 0, - sapling: Treestate { - commitments: Commitments { - final_state: sapling::tree::SerializedTree::default(), - }, - }, - orchard: Treestate { - commitments: Commitments { - final_state: orchard::tree::SerializedTree::default(), - }, - }, - } - } -} - -/// A treestate that is included in the [`z_gettreestate`][1] RPC response. -/// -/// [1]: https://zcash.github.io/rpc/z_gettreestate.html -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] -struct Treestate> { - /// Contains an Orchard or Sapling serialized note commitment tree, - /// hex-encoded. - commitments: Commitments, -} - -/// A wrapper that contains either an Orchard or Sapling note commitment tree. -/// -/// Note that in the original [`z_gettreestate`][1] RPC, [`Commitments`] also -/// contains the field `finalRoot`. Zebra does *not* use this field. -/// -/// [1]: https://zcash.github.io/rpc/z_gettreestate.html -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] -struct Commitments> { - /// Orchard or Sapling serialized note commitment tree, hex-encoded. - #[serde(with = "hex")] - #[serde(rename = "finalState")] - final_state: Tree, -} - -impl> Treestate { - /// Returns `true` if there's no serialized commitment tree. - fn is_empty(&self) -> bool { - self.commitments.final_state.as_ref().is_empty() - } -} - /// Response to a `getrawtransaction` RPC request. /// /// See the notes for the [`Rpc::get_raw_transaction` method]. diff --git a/zebra-rpc/src/methods/tests/snapshot.rs b/zebra-rpc/src/methods/tests/snapshot.rs index b97880532..eed914c78 100644 --- a/zebra-rpc/src/methods/tests/snapshot.rs +++ b/zebra-rpc/src/methods/tests/snapshot.rs @@ -11,9 +11,18 @@ use insta::dynamic_redaction; use tower::buffer::Buffer; use zebra_chain::{ - block::Block, chain_tip::mock::MockChainTip, parameters::Network::Mainnet, - serialization::ZcashDeserializeInto, subtree::NoteCommitmentSubtreeData, + block::Block, + chain_tip::mock::MockChainTip, + orchard, + parameters::{ + testnet::{ConfiguredActivationHeights, Parameters}, + Network::Mainnet, + }, + sapling, + serialization::ZcashDeserializeInto, + subtree::NoteCommitmentSubtreeData, }; +use zebra_node_services::BoxError; use zebra_state::{ReadRequest, ReadResponse, MAX_ON_DISK_HEIGHT}; use zebra_test::mock_service::MockService; @@ -40,6 +49,110 @@ async fn test_rpc_response_data() { ); } +/// Checks the output of the [`z_get_treestate`] RPC. +/// +/// TODO: +/// 1. Check a non-empty Sapling treestate. +/// 2. Check an empty Orchard treestate at NU5 activation height. +/// 3. Check a non-empty Orchard treestate. +/// +/// To implement the todos above, we need to: +/// +/// 1. Have a block containing Sapling note commitmnets in the state. +/// 2. Activate NU5 at a height for which we have a block in the state. +/// 3. Have a block containing Orchard note commitments in the state. +#[tokio::test] +async fn test_z_get_treestate() { + let _init_guard = zebra_test::init(); + const SAPLING_ACTIVATION_HEIGHT: u32 = 2; + + let testnet = Parameters::build() + .with_activation_heights(ConfiguredActivationHeights { + sapling: Some(SAPLING_ACTIVATION_HEIGHT), + // We need to set the NU5 activation height higher than the height of the last block for + // this test because we currently have only the first 10 blocks from the public Testnet, + // none of which are compatible with NU5 due to the following consensus rule: + // + // > [NU5 onward] hashBlockCommitments MUST be set to the value of + // > hashBlockCommitments for this block, as specified in [ZIP-244]. + // + // Activating NU5 at a lower height and using the 10 blocks causes a failure in + // [`zebra_state::populated_state`]. + nu5: Some(10), + ..Default::default() + }) + .with_network_name("custom_testnet") + .to_network(); + + // Initiate the snapshots of the RPC responses. + let mut settings = insta::Settings::clone_current(); + settings.set_snapshot_suffix(network_string(&testnet).to_string()); + + let blocks: Vec<_> = testnet + .blockchain_iter() + .map(|(_, block_bytes)| block_bytes.zcash_deserialize_into().unwrap()) + .collect(); + + let (_, state, tip, _) = zebra_state::populated_state(blocks.clone(), &testnet).await; + + let (rpc, _) = RpcImpl::new( + "", + "", + testnet, + false, + true, + Buffer::new(MockService::build().for_unit_tests::<_, _, BoxError>(), 1), + state, + tip, + ); + + // Request the treestate by a hash. + let treestate = rpc + .z_get_treestate(blocks[0].hash().to_string()) + .await + .expect("genesis treestate = no treestate"); + settings.bind(|| insta::assert_json_snapshot!("z_get_treestate_by_hash", treestate)); + + // Request the treestate by a hash for a block which is not in the state. + let treestate = rpc.z_get_treestate(block::Hash([0; 32]).to_string()).await; + settings + .bind(|| insta::assert_json_snapshot!("z_get_treestate_by_non_existent_hash", treestate)); + + // Request the treestate before Sapling activation. + let treestate = rpc + .z_get_treestate((SAPLING_ACTIVATION_HEIGHT - 1).to_string()) + .await + .expect("no Sapling treestate and no Orchard treestate"); + settings.bind(|| insta::assert_json_snapshot!("z_get_treestate_no_treestate", treestate)); + + // Request the treestate at Sapling activation. + let treestate = rpc + .z_get_treestate(SAPLING_ACTIVATION_HEIGHT.to_string()) + .await + .expect("empty Sapling treestate and no Orchard treestate"); + settings.bind(|| { + insta::assert_json_snapshot!("z_get_treestate_empty_Sapling_treestate", treestate) + }); + + // Request the treestate for an invalid height. + let treestate = rpc + .z_get_treestate(EXCESSIVE_BLOCK_HEIGHT.to_string()) + .await; + settings + .bind(|| insta::assert_json_snapshot!("z_get_treestate_excessive_block_height", treestate)); + + // Request the treestate for an unparsable hash or height. + let treestate = rpc.z_get_treestate("Do you even shield?".to_string()).await; + settings.bind(|| { + insta::assert_json_snapshot!("z_get_treestate_unparsable_hash_or_height", treestate) + }); + + // TODO: + // 1. Request a non-empty Sapling treestate. + // 2. Request an empty Orchard treestate at an NU5 activation height. + // 3. Request a non-empty Orchard treestate. +} + async fn test_rpc_response_data_for_network(network: &Network) { // Create a continuous chain of mainnet and testnet blocks from genesis let block_data = network.blockchain_map(); @@ -241,18 +354,6 @@ async fn test_rpc_response_data_for_network(network: &Network) { snapshot_rpc_getrawmempool(get_raw_mempool, &settings); - // `z_gettreestate` - let tree_state = rpc - .z_get_treestate(BLOCK_HEIGHT.to_string()) - .await - .expect("We should have a GetTreestate struct"); - snapshot_rpc_z_gettreestate_valid(tree_state, &settings); - - let tree_state = rpc - .z_get_treestate(EXCESSIVE_BLOCK_HEIGHT.to_string()) - .await; - snapshot_rpc_z_gettreestate_invalid("excessive_height", tree_state, &settings); - // `getrawtransaction` verbosity=0 // // - similar to `getrawmempool` described above, a mempool request will be made to get the requested @@ -501,22 +602,6 @@ fn snapshot_rpc_getrawmempool(raw_mempool: Vec, settings: &insta::Settin settings.bind(|| insta::assert_json_snapshot!("get_raw_mempool", raw_mempool)); } -/// Snapshot a valid `z_gettreestate` response, using `cargo insta` and JSON serialization. -fn snapshot_rpc_z_gettreestate_valid(tree_state: GetTreestate, settings: &insta::Settings) { - settings.bind(|| insta::assert_json_snapshot!(format!("z_get_treestate_valid"), tree_state)); -} - -/// Snapshot an invalid `z_gettreestate` response, using `cargo insta` and JSON serialization. -fn snapshot_rpc_z_gettreestate_invalid( - variant: &'static str, - tree_state: Result, - settings: &insta::Settings, -) { - settings.bind(|| { - insta::assert_json_snapshot!(format!("z_get_treestate_invalid_{variant}"), tree_state) - }); -} - /// Snapshot `getrawtransaction` response, using `cargo insta` and JSON serialization. fn snapshot_rpc_getrawtransaction( variant: &'static str, diff --git a/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_by_hash@custom_testnet.snap b/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_by_hash@custom_testnet.snap new file mode 100644 index 000000000..c262f2045 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_by_hash@custom_testnet.snap @@ -0,0 +1,9 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: treestate +--- +{ + "hash": "05a60a92d99d85997cce3b87616c089f6124d7342af37106edc76126334a2c38", + "height": 0, + "time": 1477648033 +} diff --git a/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_invalid_excessive_height@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_by_non_existent_hash@custom_testnet.snap similarity index 86% rename from zebra-rpc/src/methods/tests/snapshots/z_get_treestate_invalid_excessive_height@mainnet_10.snap rename to zebra-rpc/src/methods/tests/snapshots/z_get_treestate_by_non_existent_hash@custom_testnet.snap index fd6c362fd..d0013994a 100644 --- a/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_invalid_excessive_height@mainnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_by_non_existent_hash@custom_testnet.snap @@ -1,6 +1,6 @@ --- source: zebra-rpc/src/methods/tests/snapshot.rs -expression: tree_state +expression: treestate --- { "Err": { diff --git a/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_empty_Sapling_treestate@custom_testnet.snap b/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_empty_Sapling_treestate@custom_testnet.snap new file mode 100644 index 000000000..3ba356fe5 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_empty_Sapling_treestate@custom_testnet.snap @@ -0,0 +1,14 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: treestate +--- +{ + "hash": "00f1a49e54553ac3ef735f2eb1d8247c9a87c22a47dbd7823ae70adcd6c21a18", + "height": 2, + "time": 1477676169, + "sapling": { + "commitments": { + "finalState": "000000" + } + } +} diff --git a/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_invalid_excessive_height@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_excessive_block_height@custom_testnet.snap similarity index 86% rename from zebra-rpc/src/methods/tests/snapshots/z_get_treestate_invalid_excessive_height@testnet_10.snap rename to zebra-rpc/src/methods/tests/snapshots/z_get_treestate_excessive_block_height@custom_testnet.snap index fd6c362fd..d0013994a 100644 --- a/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_invalid_excessive_height@testnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_excessive_block_height@custom_testnet.snap @@ -1,6 +1,6 @@ --- source: zebra-rpc/src/methods/tests/snapshot.rs -expression: tree_state +expression: treestate --- { "Err": { diff --git a/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_valid@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_no_treestate@custom_testnet.snap similarity index 88% rename from zebra-rpc/src/methods/tests/snapshots/z_get_treestate_valid@testnet_10.snap rename to zebra-rpc/src/methods/tests/snapshots/z_get_treestate_no_treestate@custom_testnet.snap index 474b781d3..7c77e4c3a 100644 --- a/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_valid@testnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_no_treestate@custom_testnet.snap @@ -1,6 +1,6 @@ --- source: zebra-rpc/src/methods/tests/snapshot.rs -expression: tree_state +expression: treestate --- { "hash": "025579869bcf52a989337342f5f57a84f3a28b968f7d6a8307902b065a668d23", diff --git a/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_unparsable_hash_or_height@custom_testnet.snap b/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_unparsable_hash_or_height@custom_testnet.snap new file mode 100644 index 000000000..a45d7e298 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_unparsable_hash_or_height@custom_testnet.snap @@ -0,0 +1,10 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: treestate +--- +{ + "Err": { + "code": 0, + "message": "parse error: could not convert the input string to a hash or height" + } +} diff --git a/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_valid@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_valid@mainnet_10.snap deleted file mode 100644 index 845d918b2..000000000 --- a/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_valid@mainnet_10.snap +++ /dev/null @@ -1,9 +0,0 @@ ---- -source: zebra-rpc/src/methods/tests/snapshot.rs -expression: tree_state ---- -{ - "hash": "0007bc227e1c57a4a70e237cad00e7b7ce565155ab49166bc57397a26d339283", - "height": 1, - "time": 1477671596 -} diff --git a/zebra-rpc/src/methods/trees.rs b/zebra-rpc/src/methods/trees.rs index 43389c43d..81b87227b 100644 --- a/zebra-rpc/src/methods/trees.rs +++ b/zebra-rpc/src/methods/trees.rs @@ -1,8 +1,10 @@ //! Types and functions for note commitment tree RPCs. -// -// TODO: move the *Tree and *Commitment types into this module. -use zebra_chain::subtree::{NoteCommitmentSubtreeData, NoteCommitmentSubtreeIndex}; +use zebra_chain::{ + block::Hash, + block::Height, + subtree::{NoteCommitmentSubtreeData, NoteCommitmentSubtreeIndex}, +}; /// A subtree data type that can hold Sapling or Orchard subtree roots. pub type SubtreeRpcData = NoteCommitmentSubtreeData; @@ -29,3 +31,104 @@ pub struct GetSubtrees { //#[serde(skip_serializing_if = "Vec::is_empty")] pub subtrees: Vec, } + +/// Response to a `z_gettreestate` RPC request. +/// +/// Contains hex-encoded Sapling & Orchard note commitment trees and their corresponding +/// [`struct@Hash`], [`Height`], and block time. +/// +/// The format of the serialized trees represents `CommitmentTree`s from the crate +/// `incrementalmerkletree` and not `Frontier`s from the same crate, even though `zebrad`'s +/// `NoteCommitmentTree`s are implemented using `Frontier`s. Zebra follows the former format to stay +/// consistent with `zcashd`'s RPCs. +/// +/// The formats are semantically equivalent. The difference is that in `Frontier`s, the vector of +/// ommers is dense (we know where the gaps are from the position of the leaf in the overall tree); +/// whereas in `CommitmentTree`, the vector of ommers is sparse with [`None`] values in the gaps. +/// +/// The dense format might be used in future RPCs. +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] +pub struct GetTreestate { + /// The block hash corresponding to the treestate, hex-encoded. + #[serde(with = "hex")] + hash: Hash, + + /// The block height corresponding to the treestate, numeric. + height: Height, + + /// Unix time when the block corresponding to the treestate was mined, + /// numeric. + /// + /// UTC seconds since the Unix 1970-01-01 epoch. + time: u32, + + /// A treestate containing a Sapling note commitment tree, hex-encoded. + #[serde(skip_serializing_if = "Option::is_none")] + sapling: Option>>, + + /// A treestate containing an Orchard note commitment tree, hex-encoded. + #[serde(skip_serializing_if = "Option::is_none")] + orchard: Option>>, +} + +impl GetTreestate { + /// Constructs [`GetTreestate`] from its constituent parts. + pub fn from_parts( + hash: Hash, + height: Height, + time: u32, + sapling: Option>, + orchard: Option>, + ) -> Self { + let sapling = sapling.map(|tree| Treestate { + commitments: Commitments { final_state: tree }, + }); + let orchard = orchard.map(|tree| Treestate { + commitments: Commitments { final_state: tree }, + }); + + Self { + hash, + height, + time, + sapling, + orchard, + } + } +} + +impl Default for GetTreestate { + fn default() -> Self { + Self { + hash: Hash([0; 32]), + height: Height::MIN, + time: Default::default(), + sapling: Default::default(), + orchard: Default::default(), + } + } +} + +/// A treestate that is included in the [`z_gettreestate`][1] RPC response. +/// +/// [1]: https://zcash.github.io/rpc/z_gettreestate.html +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] +struct Treestate> { + /// Contains an Orchard or Sapling serialized note commitment tree, + /// hex-encoded. + commitments: Commitments, +} + +/// A wrapper that contains either an Orchard or Sapling note commitment tree. +/// +/// Note that in the original [`z_gettreestate`][1] RPC, [`Commitments`] also +/// contains the field `finalRoot`. Zebra does *not* use this field. +/// +/// [1]: https://zcash.github.io/rpc/z_gettreestate.html +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] +struct Commitments> { + /// Orchard or Sapling serialized note commitment tree, hex-encoded. + #[serde(with = "hex")] + #[serde(rename = "finalState")] + final_state: Tree, +} diff --git a/zebra-utils/src/bin/openapi-generator/main.rs b/zebra-utils/src/bin/openapi-generator/main.rs index 6dc24db95..020c16653 100644 --- a/zebra-utils/src/bin/openapi-generator/main.rs +++ b/zebra-utils/src/bin/openapi-generator/main.rs @@ -6,7 +6,7 @@ use quote::ToTokens; use serde::Serialize; use syn::LitStr; -use zebra_rpc::methods::*; +use zebra_rpc::methods::{trees::GetTreestate, *}; // The API server const SERVER: &str = "http://localhost:8232";