fix(rpc): Refactor the serialization of note commitment trees (#8533)

* Remove `orchard::tree::SerializedTree`

* Move `GetTreestate` to the `trees` module

* Update comments

* Remove `sapling::tree::SerializedTree`

* Make the serialization compatible with `zcashd`

* Simplify error handling

* Derive `Default` for `GetTreestate`

* Remove old TODOs

* Simplify the `z_get_treestate` method

* Add & refactor tests

* Avoid a concurrency issue

* Fix docs

* Impl `Default` for `GetTreestate`

* Update zebra-rpc/src/methods.rs

Co-authored-by: Arya <aryasolhi@gmail.com>

* Update docs

Co-authored-by: Arya <aryasolhi@gmail.com>

* Rename `rsp` to `tree_state`

Co-authored-by: Arya <aryasolhi@gmail.com>

* Describe the serialization format of treestates

* Refactor error handling

* Refactor imports

* Apply suggestions from code review

Co-authored-by: Arya <aryasolhi@gmail.com>

* Use `treestate` in snapshots

* Use `ok_or_server_error`

* Bump  `zcash_primitives` from 0.13.0 to 0.14.0

Co-authored-by: Alfredo Garcia <oxarbitrage@gmail.com>

* Remove an outdated TODO

* Add a TODO on negative heights for treestates

* Revert "Bump `zcash_primitives` from 0.13.0 to 0.14.0"

This reverts commit 0799cb2389.

---------

Co-authored-by: Arya <aryasolhi@gmail.com>
Co-authored-by: Alfredo Garcia <oxarbitrage@gmail.com>
This commit is contained in:
Marek 2024-05-22 15:31:52 +02:00 committed by GitHub
parent b06a1221cb
commit eade2a85a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 377 additions and 526 deletions

View File

@ -6171,6 +6171,7 @@ dependencies = [
"tower",
"tracing",
"zcash_address",
"zcash_primitives 0.13.0",
"zebra-chain",
"zebra-consensus",
"zebra-network",

View File

@ -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<u8> {
// 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<Vec<pallas::Base>> 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<u8>);
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<Option<Arc<NoteCommitmentTree>>> for SerializedTree {
fn from(maybe_tree: Option<Arc<NoteCommitmentTree>>) -> 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
}
}

View File

@ -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<u8> {
// 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<Vec<jubjub::Fq>> 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<u8>);
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]: <https://github.com/zcash/librustzcash/blob/a63a37a/zcash_primitives/src/merkle_tree.rs#L125>
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<Option<Arc<NoteCommitmentTree>>> for SerializedTree {
fn from(maybe_tree: Option<Arc<NoteCommitmentTree>>) -> 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
}
}

View File

@ -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

View File

@ -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<Result<GetTreestate>> {
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<Tip>(latest_chain_tip: &Tip) -> Result<Height>
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<sapling::tree::SerializedTree>,
/// A treestate containing an Orchard note commitment tree, hex-encoded.
#[serde(skip_serializing_if = "Treestate::is_empty")]
orchard: Treestate<orchard::tree::SerializedTree>,
}
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<Tree: AsRef<[u8]>> {
/// Contains an Orchard or Sapling serialized note commitment tree,
/// hex-encoded.
commitments: Commitments<Tree>,
}
/// 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<Tree: AsRef<[u8]>> {
/// Orchard or Sapling serialized note commitment tree, hex-encoded.
#[serde(with = "hex")]
#[serde(rename = "finalState")]
final_state: Tree,
}
impl<Tree: AsRef<[u8]>> Treestate<Tree> {
/// 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].

View File

@ -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<String>, 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<GetTreestate>,
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,

View File

@ -0,0 +1,9 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
expression: treestate
---
{
"hash": "05a60a92d99d85997cce3b87616c089f6124d7342af37106edc76126334a2c38",
"height": 0,
"time": 1477648033
}

View File

@ -1,6 +1,6 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
expression: tree_state
expression: treestate
---
{
"Err": {

View File

@ -0,0 +1,14 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
expression: treestate
---
{
"hash": "00f1a49e54553ac3ef735f2eb1d8247c9a87c22a47dbd7823ae70adcd6c21a18",
"height": 2,
"time": 1477676169,
"sapling": {
"commitments": {
"finalState": "000000"
}
}
}

View File

@ -1,6 +1,6 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
expression: tree_state
expression: treestate
---
{
"Err": {

View File

@ -1,6 +1,6 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
expression: tree_state
expression: treestate
---
{
"hash": "025579869bcf52a989337342f5f57a84f3a28b968f7d6a8307902b065a668d23",

View File

@ -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"
}
}

View File

@ -1,9 +0,0 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
expression: tree_state
---
{
"hash": "0007bc227e1c57a4a70e237cad00e7b7ce565155ab49166bc57397a26d339283",
"height": 1,
"time": 1477671596
}

View File

@ -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<String>;
@ -29,3 +31,104 @@ pub struct GetSubtrees {
//#[serde(skip_serializing_if = "Vec::is_empty")]
pub subtrees: Vec<SubtreeRpcData>,
}
/// 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<Treestate<Vec<u8>>>,
/// A treestate containing an Orchard note commitment tree, hex-encoded.
#[serde(skip_serializing_if = "Option::is_none")]
orchard: Option<Treestate<Vec<u8>>>,
}
impl GetTreestate {
/// Constructs [`GetTreestate`] from its constituent parts.
pub fn from_parts(
hash: Hash,
height: Height,
time: u32,
sapling: Option<Vec<u8>>,
orchard: Option<Vec<u8>>,
) -> 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<Tree: AsRef<[u8]>> {
/// Contains an Orchard or Sapling serialized note commitment tree,
/// hex-encoded.
commitments: Commitments<Tree>,
}
/// 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<Tree: AsRef<[u8]>> {
/// Orchard or Sapling serialized note commitment tree, hex-encoded.
#[serde(with = "hex")]
#[serde(rename = "finalState")]
final_state: Tree,
}

View File

@ -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";