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",
|
"tower",
|
||||||
"tracing",
|
"tracing",
|
||||||
"zcash_address",
|
"zcash_address",
|
||||||
|
"zcash_primitives 0.13.0",
|
||||||
"zebra-chain",
|
"zebra-chain",
|
||||||
"zebra-consensus",
|
"zebra-consensus",
|
||||||
"zebra-network",
|
"zebra-network",
|
||||||
|
|
|
@ -15,7 +15,6 @@ use std::{
|
||||||
fmt,
|
fmt,
|
||||||
hash::{Hash, Hasher},
|
hash::{Hash, Hasher},
|
||||||
io,
|
io,
|
||||||
sync::Arc,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use bitvec::prelude::*;
|
use bitvec::prelude::*;
|
||||||
|
@ -25,7 +24,7 @@ use hex::ToHex;
|
||||||
use incrementalmerkletree::Hashable;
|
use incrementalmerkletree::Hashable;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use zcash_primitives::merkle_tree::{write_commitment_tree, HashSer};
|
use zcash_primitives::merkle_tree::HashSer;
|
||||||
|
|
||||||
use super::sinsemilla::*;
|
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
|
/// Zebra stores Orchard note commitment trees as [`Frontier`][1]s while the
|
||||||
/// [`z_gettreestate`][2] RPC requires [`CommitmentTree`][3]s. Implementing
|
/// [`z_gettreestate`][2] RPC requires [`CommitmentTree`][3]s. Implementing
|
||||||
|
@ -633,7 +632,21 @@ impl NoteCommitmentTree {
|
||||||
assert_eq!(self.inner, other.inner);
|
assert_eq!(self.inner, other.inner);
|
||||||
|
|
||||||
// Check the RPC serialization format (not the same as the Zebra database format)
|
// 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
|
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,
|
fmt,
|
||||||
hash::{Hash, Hasher},
|
hash::{Hash, Hasher},
|
||||||
io,
|
io,
|
||||||
sync::Arc,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use bitvec::prelude::*;
|
use bitvec::prelude::*;
|
||||||
|
@ -25,7 +24,6 @@ use incrementalmerkletree::{frontier::Frontier, Hashable};
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use zcash_encoding::{Optional, Vector};
|
|
||||||
use zcash_primitives::merkle_tree::HashSer;
|
use zcash_primitives::merkle_tree::HashSer;
|
||||||
|
|
||||||
use super::commitment::pedersen_hashes::pedersen_hash;
|
use super::commitment::pedersen_hashes::pedersen_hash;
|
||||||
|
@ -38,7 +36,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod legacy;
|
pub mod legacy;
|
||||||
use legacy::{LegacyLeaf, LegacyNoteCommitmentTree};
|
use legacy::LegacyNoteCommitmentTree;
|
||||||
|
|
||||||
/// The type that is used to update the note commitment tree.
|
/// 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
|
/// Zebra stores Sapling note commitment trees as [`Frontier`]s while the
|
||||||
/// [`z_gettreestate`][1] RPC requires [`CommitmentTree`][2]s. Implementing
|
/// [`z_gettreestate`][1] RPC requires [`CommitmentTree`][2]s. Implementing
|
||||||
|
@ -614,7 +612,21 @@ impl NoteCommitmentTree {
|
||||||
assert_eq!(self.inner, other.inner);
|
assert_eq!(self.inner, other.inner);
|
||||||
|
|
||||||
// Check the RPC serialization format (not the same as the Zebra database format)
|
// 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
|
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"] }
|
hex = { version = "0.4.3", features = ["serde"] }
|
||||||
serde = { version = "1.0.202", features = ["serde_derive"] }
|
serde = { version = "1.0.202", features = ["serde_derive"] }
|
||||||
|
|
||||||
|
zcash_primitives = { version = "0.13.0" }
|
||||||
|
|
||||||
# Experimental feature getblocktemplate-rpcs
|
# Experimental feature getblocktemplate-rpcs
|
||||||
rand = { version = "0.8.5", optional = true }
|
rand = { version = "0.8.5", optional = true }
|
||||||
# ECC deps used by getblocktemplate-rpcs feature
|
# ECC deps used by getblocktemplate-rpcs feature
|
||||||
|
|
|
@ -18,13 +18,12 @@ use tokio::{sync::broadcast, task::JoinHandle};
|
||||||
use tower::{Service, ServiceExt};
|
use tower::{Service, ServiceExt};
|
||||||
use tracing::Instrument;
|
use tracing::Instrument;
|
||||||
|
|
||||||
|
use zcash_primitives::consensus::Parameters;
|
||||||
use zebra_chain::{
|
use zebra_chain::{
|
||||||
block::{self, Height, SerializedBlock},
|
block::{self, Height, SerializedBlock},
|
||||||
chain_tip::ChainTip,
|
chain_tip::ChainTip,
|
||||||
orchard,
|
|
||||||
parameters::{ConsensusBranchId, Network, NetworkUpgrade},
|
parameters::{ConsensusBranchId, Network, NetworkUpgrade},
|
||||||
sapling,
|
serialization::ZcashDeserialize,
|
||||||
serialization::{SerializationError, ZcashDeserialize},
|
|
||||||
subtree::NoteCommitmentSubtreeIndex,
|
subtree::NoteCommitmentSubtreeIndex,
|
||||||
transaction::{self, SerializedTransaction, Transaction, UnminedTx},
|
transaction::{self, SerializedTransaction, Transaction, UnminedTx},
|
||||||
transparent::{self, Address},
|
transparent::{self, Address},
|
||||||
|
@ -34,7 +33,7 @@ use zebra_state::{HashOrHeight, MinedTx, OutputIndex, OutputLocation, Transactio
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
constants::{INVALID_PARAMETERS_ERROR_CODE, MISSING_BLOCK_ERROR_CODE},
|
constants::{INVALID_PARAMETERS_ERROR_CODE, MISSING_BLOCK_ERROR_CODE},
|
||||||
methods::trees::{GetSubtrees, SubtreeRpcData},
|
methods::trees::{GetSubtrees, GetTreestate, SubtreeRpcData},
|
||||||
queue::Queue,
|
queue::Queue,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -504,30 +503,18 @@ where
|
||||||
let (tip_height, tip_hash) = self
|
let (tip_height, tip_hash) = self
|
||||||
.latest_chain_tip
|
.latest_chain_tip
|
||||||
.best_tip_height_and_hash()
|
.best_tip_height_and_hash()
|
||||||
.ok_or_else(|| Error {
|
.ok_or_server_error("No Chain tip available yet")?;
|
||||||
code: ErrorCode::ServerError(0),
|
|
||||||
message: "No Chain tip available yet".to_string(),
|
|
||||||
data: None,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// `estimated_height` field
|
// `estimated_height` field
|
||||||
let current_block_time =
|
let current_block_time = self
|
||||||
self.latest_chain_tip
|
.latest_chain_tip
|
||||||
.best_tip_block_time()
|
.best_tip_block_time()
|
||||||
.ok_or_else(|| Error {
|
.ok_or_server_error("No Chain tip available yet")?;
|
||||||
code: ErrorCode::ServerError(0),
|
|
||||||
message: "No Chain tip available yet".to_string(),
|
|
||||||
data: None,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let zebra_estimated_height = self
|
let zebra_estimated_height = self
|
||||||
.latest_chain_tip
|
.latest_chain_tip
|
||||||
.estimate_network_chain_tip_height(network, Utc::now())
|
.estimate_network_chain_tip_height(network, Utc::now())
|
||||||
.ok_or_else(|| Error {
|
.ok_or_server_error("No Chain tip available yet")?;
|
||||||
code: ErrorCode::ServerError(0),
|
|
||||||
message: "No Chain tip available yet".to_string(),
|
|
||||||
data: None,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let mut estimated_height =
|
let mut estimated_height =
|
||||||
if current_block_time > Utc::now() || zebra_estimated_height < tip_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 valid_addresses = address_strings.valid_addresses()?;
|
||||||
|
|
||||||
let request = zebra_state::ReadRequest::AddressBalance(valid_addresses);
|
let request = zebra_state::ReadRequest::AddressBalance(valid_addresses);
|
||||||
let response = state.oneshot(request).await.map_err(|error| Error {
|
let response = state.oneshot(request).await.map_server_error()?;
|
||||||
code: ErrorCode::ServerError(0),
|
|
||||||
message: error.to_string(),
|
|
||||||
data: None,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
zebra_state::ReadResponse::AddressBalance(balance) => Ok(AddressBalance {
|
zebra_state::ReadResponse::AddressBalance(balance) => Ok(AddressBalance {
|
||||||
|
@ -647,11 +630,7 @@ where
|
||||||
let transaction_parameter = mempool::Gossip::Tx(raw_transaction.into());
|
let transaction_parameter = mempool::Gossip::Tx(raw_transaction.into());
|
||||||
let request = mempool::Request::Queue(vec![transaction_parameter]);
|
let request = mempool::Request::Queue(vec![transaction_parameter]);
|
||||||
|
|
||||||
let response = mempool.oneshot(request).await.map_err(|error| Error {
|
let response = mempool.oneshot(request).await.map_server_error()?;
|
||||||
code: ErrorCode::ServerError(0),
|
|
||||||
message: error.to_string(),
|
|
||||||
data: None,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let queue_results = match response {
|
let queue_results = match response {
|
||||||
mempool::Response::Queued(results) => results,
|
mempool::Response::Queued(results) => results,
|
||||||
|
@ -666,14 +645,10 @@ where
|
||||||
|
|
||||||
tracing::debug!("sent transaction to mempool: {:?}", &queue_results[0]);
|
tracing::debug!("sent transaction to mempool: {:?}", &queue_results[0]);
|
||||||
|
|
||||||
match &queue_results[0] {
|
queue_results[0]
|
||||||
Ok(()) => Ok(SentTransactionHash(transaction_hash)),
|
.as_ref()
|
||||||
Err(error) => Err(Error {
|
.map(|_| SentTransactionHash(transaction_hash))
|
||||||
code: ErrorCode::ServerError(0),
|
.map_server_error()
|
||||||
message: error.to_string(),
|
|
||||||
data: None,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
@ -681,7 +656,6 @@ where
|
||||||
// TODO:
|
// TODO:
|
||||||
// - use `height_from_signed_int()` to handle negative heights
|
// - use `height_from_signed_int()` to handle negative heights
|
||||||
// (this might be better in the state request, because it needs the state height)
|
// (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(
|
fn get_block(
|
||||||
&self,
|
&self,
|
||||||
hash_or_height: String,
|
hash_or_height: String,
|
||||||
|
@ -694,14 +668,7 @@ where
|
||||||
let verbosity = verbosity.unwrap_or(DEFAULT_GETBLOCK_VERBOSITY);
|
let verbosity = verbosity.unwrap_or(DEFAULT_GETBLOCK_VERBOSITY);
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
let hash_or_height: HashOrHeight =
|
let hash_or_height: HashOrHeight = hash_or_height.parse().map_server_error()?;
|
||||||
hash_or_height
|
|
||||||
.parse()
|
|
||||||
.map_err(|error: SerializationError| Error {
|
|
||||||
code: ErrorCode::ServerError(0),
|
|
||||||
message: error.to_string(),
|
|
||||||
data: None,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if verbosity == 0 {
|
if verbosity == 0 {
|
||||||
// # Performance
|
// # Performance
|
||||||
|
@ -713,11 +680,7 @@ where
|
||||||
.ready()
|
.ready()
|
||||||
.and_then(|service| service.call(request))
|
.and_then(|service| service.call(request))
|
||||||
.await
|
.await
|
||||||
.map_err(|error| Error {
|
.map_server_error()?;
|
||||||
code: ErrorCode::ServerError(0),
|
|
||||||
message: error.to_string(),
|
|
||||||
data: None,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
zebra_state::ReadResponse::Block(Some(block)) => {
|
zebra_state::ReadResponse::Block(Some(block)) => {
|
||||||
|
@ -761,11 +724,7 @@ where
|
||||||
.ready()
|
.ready()
|
||||||
.and_then(|service| service.call(request))
|
.and_then(|service| service.call(request))
|
||||||
.await
|
.await
|
||||||
.map_err(|error| Error {
|
.map_server_error()?;
|
||||||
code: ErrorCode::ServerError(0),
|
|
||||||
message: error.to_string(),
|
|
||||||
data: None,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
zebra_state::ReadResponse::BlockHash(Some(hash)) => hash,
|
zebra_state::ReadResponse::BlockHash(Some(hash)) => hash,
|
||||||
|
@ -913,11 +872,7 @@ where
|
||||||
self.latest_chain_tip
|
self.latest_chain_tip
|
||||||
.best_tip_hash()
|
.best_tip_hash()
|
||||||
.map(GetBlockHash)
|
.map(GetBlockHash)
|
||||||
.ok_or(Error {
|
.ok_or_server_error("No blocks in state")
|
||||||
code: ErrorCode::ServerError(0),
|
|
||||||
message: "No blocks in state".to_string(),
|
|
||||||
data: None,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: use a generic error constructor (#5548)
|
// TODO: use a generic error constructor (#5548)
|
||||||
|
@ -947,11 +902,7 @@ where
|
||||||
.ready()
|
.ready()
|
||||||
.and_then(|service| service.call(request))
|
.and_then(|service| service.call(request))
|
||||||
.await
|
.await
|
||||||
.map_err(|error| Error {
|
.map_server_error()?;
|
||||||
code: ErrorCode::ServerError(0),
|
|
||||||
message: error.to_string(),
|
|
||||||
data: None,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
#[cfg(feature = "getblocktemplate-rpcs")]
|
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||||
|
@ -1030,11 +981,7 @@ where
|
||||||
.ready()
|
.ready()
|
||||||
.and_then(|service| service.call(request))
|
.and_then(|service| service.call(request))
|
||||||
.await
|
.await
|
||||||
.map_err(|error| Error {
|
.map_server_error()?;
|
||||||
code: ErrorCode::ServerError(0),
|
|
||||||
message: error.to_string(),
|
|
||||||
data: None,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
mempool::Response::Transactions(unmined_transactions) => {
|
mempool::Response::Transactions(unmined_transactions) => {
|
||||||
|
@ -1052,11 +999,7 @@ where
|
||||||
.ready()
|
.ready()
|
||||||
.and_then(|service| service.call(request))
|
.and_then(|service| service.call(request))
|
||||||
.await
|
.await
|
||||||
.map_err(|error| Error {
|
.map_server_error()?;
|
||||||
code: ErrorCode::ServerError(0),
|
|
||||||
message: error.to_string(),
|
|
||||||
data: None,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
zebra_state::ReadResponse::Transaction(Some(MinedTx {
|
zebra_state::ReadResponse::Transaction(Some(MinedTx {
|
||||||
|
@ -1069,11 +1012,9 @@ where
|
||||||
confirmations,
|
confirmations,
|
||||||
verbose,
|
verbose,
|
||||||
)),
|
)),
|
||||||
zebra_state::ReadResponse::Transaction(None) => Err(Error {
|
zebra_state::ReadResponse::Transaction(None) => {
|
||||||
code: ErrorCode::ServerError(0),
|
Err("Transaction not found").map_server_error()
|
||||||
message: "Transaction not found".to_string(),
|
}
|
||||||
data: None,
|
|
||||||
}),
|
|
||||||
_ => unreachable!("unmatched response to a transaction request"),
|
_ => unreachable!("unmatched response to a transaction request"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1081,47 +1022,30 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
// - use a generic error constructor (#5548)
|
|
||||||
// - use `height_from_signed_int()` to handle negative heights
|
// - use `height_from_signed_int()` to handle negative heights
|
||||||
// (this might be better in the state request, because it needs the state height)
|
// (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>> {
|
fn z_get_treestate(&self, hash_or_height: String) -> BoxFuture<Result<GetTreestate>> {
|
||||||
let mut state = self.state.clone();
|
let mut state = self.state.clone();
|
||||||
|
let network = self.network.clone();
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
// Convert the [`hash_or_height`] string into an actual hash or height.
|
// Convert the [`hash_or_height`] string into an actual hash or height.
|
||||||
let hash_or_height = hash_or_height
|
let hash_or_height = hash_or_height.parse().map_server_error()?;
|
||||||
.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.
|
|
||||||
|
|
||||||
// Fetch the block referenced by [`hash_or_height`] from the state.
|
// 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.
|
// # Concurrency
|
||||||
let block_request = zebra_state::ReadRequest::Block(hash_or_height);
|
//
|
||||||
let block_response = state
|
// 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()
|
.ready()
|
||||||
.and_then(|service| service.call(block_request))
|
.and_then(|service| service.call(zebra_state::ReadRequest::Block(hash_or_height)))
|
||||||
.await
|
.await
|
||||||
.map_err(|error| Error {
|
.map_server_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 {
|
|
||||||
zebra_state::ReadResponse::Block(Some(block)) => block,
|
zebra_state::ReadResponse::Block(Some(block)) => block,
|
||||||
zebra_state::ReadResponse::Block(None) => {
|
zebra_state::ReadResponse::Block(None) => {
|
||||||
return Err(Error {
|
return Err(Error {
|
||||||
|
@ -1133,73 +1057,54 @@ where
|
||||||
_ => unreachable!("unmatched response to a block request"),
|
_ => unreachable!("unmatched response to a block request"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let hash = hash_or_height.hash().unwrap_or_else(|| block.hash());
|
let hash = hash_or_height
|
||||||
let hash_or_height = hash.into();
|
.hash_or_else(|_| Some(block.hash()))
|
||||||
|
.expect("block hash");
|
||||||
|
|
||||||
// Fetch the Sapling & Orchard treestates referenced by
|
let height = hash_or_height
|
||||||
// [`hash_or_height`] from the state.
|
.height_or_else(|_| block.coinbase_height())
|
||||||
|
.expect("verified blocks have a coinbase height");
|
||||||
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 time = u32::try_from(block.header.time.timestamp())
|
let time = u32::try_from(block.header.time.timestamp())
|
||||||
.expect("Timestamps of valid blocks always fit into u32.");
|
.expect("Timestamps of valid blocks always fit into u32.");
|
||||||
|
|
||||||
let sapling_tree = match sapling_response {
|
let sapling_nu = zcash_primitives::consensus::NetworkUpgrade::Sapling;
|
||||||
zebra_state::ReadResponse::SaplingTree(maybe_tree) => {
|
let sapling = if network.is_nu_active(sapling_nu, height.into()) {
|
||||||
sapling::tree::SerializedTree::from(maybe_tree)
|
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 {
|
let orchard_nu = zcash_primitives::consensus::NetworkUpgrade::Nu5;
|
||||||
zebra_state::ReadResponse::OrchardTree(maybe_tree) => {
|
let orchard = if network.is_nu_active(orchard_nu, height.into()) {
|
||||||
orchard::tree::SerializedTree::from(maybe_tree)
|
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 {
|
Ok(GetTreestate::from_parts(
|
||||||
hash,
|
hash, height, time, sapling, orchard,
|
||||||
height,
|
))
|
||||||
time,
|
|
||||||
sapling: Treestate {
|
|
||||||
commitments: Commitments {
|
|
||||||
final_state: sapling_tree,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
orchard: Treestate {
|
|
||||||
commitments: Commitments {
|
|
||||||
final_state: orchard_tree,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
@ -1221,11 +1126,7 @@ where
|
||||||
.ready()
|
.ready()
|
||||||
.and_then(|service| service.call(request))
|
.and_then(|service| service.call(request))
|
||||||
.await
|
.await
|
||||||
.map_err(|error| Error {
|
.map_server_error()?;
|
||||||
code: ErrorCode::ServerError(0),
|
|
||||||
message: error.to_string(),
|
|
||||||
data: None,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let subtrees = match response {
|
let subtrees = match response {
|
||||||
zebra_state::ReadResponse::SaplingSubtrees(subtrees) => subtrees,
|
zebra_state::ReadResponse::SaplingSubtrees(subtrees) => subtrees,
|
||||||
|
@ -1251,11 +1152,7 @@ where
|
||||||
.ready()
|
.ready()
|
||||||
.and_then(|service| service.call(request))
|
.and_then(|service| service.call(request))
|
||||||
.await
|
.await
|
||||||
.map_err(|error| Error {
|
.map_server_error()?;
|
||||||
code: ErrorCode::ServerError(0),
|
|
||||||
message: error.to_string(),
|
|
||||||
data: None,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let subtrees = match response {
|
let subtrees = match response {
|
||||||
zebra_state::ReadResponse::OrchardSubtrees(subtrees) => subtrees,
|
zebra_state::ReadResponse::OrchardSubtrees(subtrees) => subtrees,
|
||||||
|
@ -1316,11 +1213,7 @@ where
|
||||||
.ready()
|
.ready()
|
||||||
.and_then(|service| service.call(request))
|
.and_then(|service| service.call(request))
|
||||||
.await
|
.await
|
||||||
.map_err(|error| Error {
|
.map_server_error()?;
|
||||||
code: ErrorCode::ServerError(0),
|
|
||||||
message: error.to_string(),
|
|
||||||
data: None,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let hashes = match response {
|
let hashes = match response {
|
||||||
zebra_state::ReadResponse::AddressesTransactionIds(hashes) => {
|
zebra_state::ReadResponse::AddressesTransactionIds(hashes) => {
|
||||||
|
@ -1368,11 +1261,7 @@ where
|
||||||
.ready()
|
.ready()
|
||||||
.and_then(|service| service.call(request))
|
.and_then(|service| service.call(request))
|
||||||
.await
|
.await
|
||||||
.map_err(|error| Error {
|
.map_server_error()?;
|
||||||
code: ErrorCode::ServerError(0),
|
|
||||||
message: error.to_string(),
|
|
||||||
data: None,
|
|
||||||
})?;
|
|
||||||
let utxos = match response {
|
let utxos = match response {
|
||||||
zebra_state::ReadResponse::AddressUtxos(utxos) => utxos,
|
zebra_state::ReadResponse::AddressUtxos(utxos) => utxos,
|
||||||
_ => unreachable!("unmatched response to a UtxosByAddresses request"),
|
_ => 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
|
where
|
||||||
Tip: ChainTip + Clone + Send + Sync + 'static,
|
Tip: ChainTip + Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
latest_chain_tip.best_tip_height().ok_or(Error {
|
latest_chain_tip
|
||||||
code: ErrorCode::ServerError(0),
|
.best_tip_height()
|
||||||
message: "No blocks in state".to_string(),
|
.ok_or_server_error("No blocks in state")
|
||||||
data: None,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response to a `getinfo` RPC request.
|
/// 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.
|
/// Response to a `getrawtransaction` RPC request.
|
||||||
///
|
///
|
||||||
/// See the notes for the [`Rpc::get_raw_transaction` method].
|
/// See the notes for the [`Rpc::get_raw_transaction` method].
|
||||||
|
|
|
@ -11,9 +11,18 @@ use insta::dynamic_redaction;
|
||||||
use tower::buffer::Buffer;
|
use tower::buffer::Buffer;
|
||||||
|
|
||||||
use zebra_chain::{
|
use zebra_chain::{
|
||||||
block::Block, chain_tip::mock::MockChainTip, parameters::Network::Mainnet,
|
block::Block,
|
||||||
serialization::ZcashDeserializeInto, subtree::NoteCommitmentSubtreeData,
|
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_state::{ReadRequest, ReadResponse, MAX_ON_DISK_HEIGHT};
|
||||||
use zebra_test::mock_service::MockService;
|
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) {
|
async fn test_rpc_response_data_for_network(network: &Network) {
|
||||||
// Create a continuous chain of mainnet and testnet blocks from genesis
|
// Create a continuous chain of mainnet and testnet blocks from genesis
|
||||||
let block_data = network.blockchain_map();
|
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);
|
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
|
// `getrawtransaction` verbosity=0
|
||||||
//
|
//
|
||||||
// - similar to `getrawmempool` described above, a mempool request will be made to get the requested
|
// - 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));
|
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.
|
/// Snapshot `getrawtransaction` response, using `cargo insta` and JSON serialization.
|
||||||
fn snapshot_rpc_getrawtransaction(
|
fn snapshot_rpc_getrawtransaction(
|
||||||
variant: &'static str,
|
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
|
source: zebra-rpc/src/methods/tests/snapshot.rs
|
||||||
expression: tree_state
|
expression: treestate
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
"Err": {
|
"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
|
source: zebra-rpc/src/methods/tests/snapshot.rs
|
||||||
expression: tree_state
|
expression: treestate
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
"Err": {
|
"Err": {
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
source: zebra-rpc/src/methods/tests/snapshot.rs
|
source: zebra-rpc/src/methods/tests/snapshot.rs
|
||||||
expression: tree_state
|
expression: treestate
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
"hash": "025579869bcf52a989337342f5f57a84f3a28b968f7d6a8307902b065a668d23",
|
"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.
|
//! 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.
|
/// A subtree data type that can hold Sapling or Orchard subtree roots.
|
||||||
pub type SubtreeRpcData = NoteCommitmentSubtreeData<String>;
|
pub type SubtreeRpcData = NoteCommitmentSubtreeData<String>;
|
||||||
|
@ -29,3 +31,104 @@ pub struct GetSubtrees {
|
||||||
//#[serde(skip_serializing_if = "Vec::is_empty")]
|
//#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
pub subtrees: Vec<SubtreeRpcData>,
|
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 serde::Serialize;
|
||||||
use syn::LitStr;
|
use syn::LitStr;
|
||||||
|
|
||||||
use zebra_rpc::methods::*;
|
use zebra_rpc::methods::{trees::GetTreestate, *};
|
||||||
|
|
||||||
// The API server
|
// The API server
|
||||||
const SERVER: &str = "http://localhost:8232";
|
const SERVER: &str = "http://localhost:8232";
|
||||||
|
|
Loading…
Reference in New Issue