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:
parent
b06a1221cb
commit
eade2a85a8
|
@ -6171,6 +6171,7 @@ dependencies = [
|
|||
"tower",
|
||||
"tracing",
|
||||
"zcash_address",
|
||||
"zcash_primitives 0.13.0",
|
||||
"zebra-chain",
|
||||
"zebra-consensus",
|
||||
"zebra-network",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
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,
|
||||
})?;
|
||||
.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)
|
||||
}
|
||||
_ => unreachable!("unmatched response to a sapling tree request"),
|
||||
};
|
||||
|
||||
let orchard_tree = match orchard_response {
|
||||
zebra_state::ReadResponse::OrchardTree(maybe_tree) => {
|
||||
orchard::tree::SerializedTree::from(maybe_tree)
|
||||
}
|
||||
_ => unreachable!("unmatched response to an orchard tree request"),
|
||||
};
|
||||
|
||||
Ok(GetTreestate {
|
||||
hash,
|
||||
height,
|
||||
time,
|
||||
sapling: Treestate {
|
||||
commitments: Commitments {
|
||||
final_state: sapling_tree,
|
||||
},
|
||||
},
|
||||
orchard: Treestate {
|
||||
commitments: Commitments {
|
||||
final_state: orchard_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"),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
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"),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
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].
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
source: zebra-rpc/src/methods/tests/snapshot.rs
|
||||
expression: treestate
|
||||
---
|
||||
{
|
||||
"hash": "05a60a92d99d85997cce3b87616c089f6124d7342af37106edc76126334a2c38",
|
||||
"height": 0,
|
||||
"time": 1477648033
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
source: zebra-rpc/src/methods/tests/snapshot.rs
|
||||
expression: tree_state
|
||||
expression: treestate
|
||||
---
|
||||
{
|
||||
"Err": {
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
source: zebra-rpc/src/methods/tests/snapshot.rs
|
||||
expression: treestate
|
||||
---
|
||||
{
|
||||
"hash": "00f1a49e54553ac3ef735f2eb1d8247c9a87c22a47dbd7823ae70adcd6c21a18",
|
||||
"height": 2,
|
||||
"time": 1477676169,
|
||||
"sapling": {
|
||||
"commitments": {
|
||||
"finalState": "000000"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
source: zebra-rpc/src/methods/tests/snapshot.rs
|
||||
expression: tree_state
|
||||
expression: treestate
|
||||
---
|
||||
{
|
||||
"Err": {
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
source: zebra-rpc/src/methods/tests/snapshot.rs
|
||||
expression: tree_state
|
||||
expression: treestate
|
||||
---
|
||||
{
|
||||
"hash": "025579869bcf52a989337342f5f57a84f3a28b968f7d6a8307902b065a668d23",
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
source: zebra-rpc/src/methods/tests/snapshot.rs
|
||||
expression: tree_state
|
||||
---
|
||||
{
|
||||
"hash": "0007bc227e1c57a4a70e237cad00e7b7ce565155ab49166bc57397a26d339283",
|
||||
"height": 1,
|
||||
"time": 1477671596
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
Loading…
Reference in New Issue