feat(rpc): Implement `z_gettreestate` RPC (#3990)

* Impl the elementary structure of the `z_gettreestate` RPC

* Fix merging bugs

* Fix a merge bug

* Fix a merge bug

* Move a derive attribute

Co-authored-by: teor <teor@riseup.net>

* Clarify the support of negative heights

* Add Orchard note commitment trees to the response

* Add the time to the response

* Finalize the `z_gettreestate` RPC

* Add a note that verified blocks have coinbase height

* Refactor `from_str` for `HashOrHeight`

* Fix a mistake in the docs

Co-authored-by: teor <teor@riseup.net>

* Clarify request types

Co-authored-by: teor <teor@riseup.net>

* Simplify `hash_or_height` conversion to height

Co-authored-by: teor <teor@riseup.net>

* Add a TODO about optimization

Co-authored-by: teor <teor@riseup.net>

* Add a doc comment

* Make sure Sapling & Orchard trees don't get mixed up

* Serialize Sapling commitment trees

* Refactor some comments

* Serialize Orchard commitment trees

* Serialize block heights

* Simplify the serialization of commitment trees

* Remove the block time from the RPC response

* Simplify the serialization of block heights

* Put Sapling & Orchard requests together

* Remove a redundant TODO

* Add block times to the RPC response

* Derive `Clone, Debug, Eq, PartialEq` for `GetTreestate`

Co-authored-by: teor <teor@riseup.net>

* Derive `Clone`, `Debug`, `Eq` and `PartialEq` for `SerializedTree`

* Document the fields of `GetTreestate`

* Skip the serialization of empty trees

This ensures compatibility with `zcashd` in the `z_gettreestate` RPC.

* Document the `impl` of `merkle_tree::Hashable` for nodes

* Make the structure of the JSON response consistent with `zcashd`

* Derive `Eq` for nodes

Co-authored-by: teor <teor@riseup.net>

* Convert Sapling commitment trees to a format compatible with zcashd

* Refactor the conversion of Sapling commitment trees

* Refactor some comments

* Refactor comments

* Add a description of the conversion

Co-authored-by: Conrado Gouvea <conrado@zfnd.org>

* Fix comment indenting

* Document the conversion between the dense and sparse formats

Co-authored-by: teor <teor@riseup.net>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Conrado Gouvea <conrado@zfnd.org>
This commit is contained in:
Marek 2022-05-12 09:00:12 +02:00 committed by GitHub
parent fd7f49fb0c
commit 7c726b246d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 795 additions and 51 deletions

68
Cargo.lock generated
View File

@ -1485,6 +1485,15 @@ dependencies = [
"byteorder",
]
[[package]]
name = "equihash"
version = "0.1.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=d5c5f04#d5c5f048947d211c2fbef23ce986b329b91d1aa5"
dependencies = [
"blake2b_simd 1.0.0",
"byteorder",
]
[[package]]
name = "eyre"
version = "0.6.7"
@ -5908,6 +5917,15 @@ dependencies = [
"nonempty",
]
[[package]]
name = "zcash_encoding"
version = "0.0.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=d5c5f04#d5c5f048947d211c2fbef23ce986b329b91d1aa5"
dependencies = [
"byteorder",
"nonempty",
]
[[package]]
name = "zcash_history"
version = "0.2.0"
@ -5940,6 +5958,17 @@ dependencies = [
"subtle",
]
[[package]]
name = "zcash_note_encryption"
version = "0.1.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=d5c5f04#d5c5f048947d211c2fbef23ce986b329b91d1aa5"
dependencies = [
"chacha20",
"chacha20poly1305",
"rand_core 0.6.3",
"subtle",
]
[[package]]
name = "zcash_primitives"
version = "0.5.0"
@ -6008,6 +6037,42 @@ dependencies = [
"zcash_note_encryption 0.1.0 (git+https://github.com/zcash/librustzcash.git?rev=d14e7a707ce01cefcbc82651dad48f002185dded)",
]
[[package]]
name = "zcash_primitives"
version = "0.5.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=d5c5f04#d5c5f048947d211c2fbef23ce986b329b91d1aa5"
dependencies = [
"aes",
"bip0039",
"bitvec",
"blake2b_simd 1.0.0",
"blake2s_simd 1.0.0",
"bls12_381",
"bs58",
"byteorder",
"chacha20poly1305",
"equihash 0.1.0 (git+https://github.com/zcash/librustzcash.git?rev=d5c5f04)",
"ff",
"fpe",
"group",
"hdwallet",
"hex",
"incrementalmerkletree",
"jubjub",
"lazy_static",
"memuse",
"nonempty",
"orchard",
"rand 0.8.5",
"rand_core 0.6.3",
"ripemd",
"secp256k1",
"sha2",
"subtle",
"zcash_encoding 0.0.0 (git+https://github.com/zcash/librustzcash.git?rev=d5c5f04)",
"zcash_note_encryption 0.1.0 (git+https://github.com/zcash/librustzcash.git?rev=d5c5f04)",
]
[[package]]
name = "zcash_proofs"
version = "0.5.0"
@ -6095,9 +6160,10 @@ dependencies = [
"tracing",
"uint",
"x25519-dalek",
"zcash_encoding 0.0.0 (git+https://github.com/zcash/librustzcash.git?rev=d5c5f04)",
"zcash_history",
"zcash_note_encryption 0.1.0 (git+https://github.com/zcash/librustzcash.git?rev=d14e7a707ce01cefcbc82651dad48f002185dded)",
"zcash_primitives 0.5.0 (git+https://github.com/zcash/librustzcash.git?rev=d14e7a707ce01cefcbc82651dad48f002185dded)",
"zcash_primitives 0.5.0 (git+https://github.com/zcash/librustzcash.git?rev=d5c5f04)",
"zebra-test",
]

View File

@ -53,7 +53,8 @@ orchard = "=0.1.0-beta.3"
equihash = "0.1.0"
zcash_note_encryption = "0.1"
zcash_primitives = { version = "0.5", features = ["transparent-inputs"] }
zcash_primitives = { git = "https://github.com/zcash/librustzcash.git", rev = "d5c5f04", features = ["transparent-inputs"] }
zcash_encoding = { git = "https://github.com/zcash/librustzcash.git", rev = "d5c5f04" }
zcash_history = { git = "https://github.com/ZcashFoundation/librustzcash.git", tag = "0.5.1-zebra-v1.0.0-beta.4" }
proptest = { version = "0.10.1", optional = true }

View File

@ -67,6 +67,10 @@ impl fmt::Display for Block {
impl Block {
/// Return the block height reported in the coinbase transaction, if any.
///
/// Note
///
/// Verified blocks have a valid height.
pub fn coinbase_height(&self) -> Option<Height> {
self.transactions
.get(0)

View File

@ -1,3 +1,5 @@
//! The block header.
use std::usize;
use chrono::{DateTime, Duration, Utc};

View File

@ -18,6 +18,7 @@ use std::{
hash::{Hash, Hasher},
io,
ops::Deref,
sync::Arc,
};
use bitvec::prelude::*;
@ -25,6 +26,7 @@ use halo2::pasta::{group::ff::PrimeField, pallas};
use incrementalmerkletree::{bridgetree, Frontier};
use lazy_static::lazy_static;
use thiserror::Error;
use zcash_primitives::merkle_tree::{self, CommitmentTree};
use super::sinsemilla::*;
@ -153,9 +155,52 @@ impl ZcashDeserialize for Root {
}
/// A node of the Orchard Incremental Note Commitment Tree.
#[derive(Clone, Debug)]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
struct Node(pallas::Base);
/// Required to convert [`NoteCommitmentTree`] into [`SerializedTree`].
///
/// Zebra stores Orchard note commitment trees as [`Frontier`][1]s while the
/// [`z_gettreestate`][2] RPC requires [`CommitmentTree`][3]s. Implementing
/// [`merkle_tree::Hashable`] for [`Node`]s allows the conversion.
///
/// [1]: bridgetree::Frontier
/// [2]: https://zcash.github.io/rpc/z_gettreestate.html
/// [3]: merkle_tree::CommitmentTree
impl merkle_tree::Hashable for Node {
fn read<R: io::Read>(mut reader: R) -> io::Result<Self> {
let mut repr = [0u8; 32];
reader.read_exact(&mut repr)?;
let maybe_node = pallas::Base::from_repr(repr).map(Self);
<Option<_>>::from(maybe_node).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Non-canonical encoding of Pallas base field value.",
)
})
}
fn write<W: io::Write>(&self, mut writer: W) -> io::Result<()> {
writer.write_all(&self.0.to_repr())
}
fn combine(level: usize, a: &Self, b: &Self) -> Self {
let level = u8::try_from(level).expect("level must fit into u8");
let layer = (MERKLE_DEPTH - 1) as u8 - level;
Self(merkle_crh_orchard(layer, a.0, b.0))
}
fn blank() -> Self {
Self(NoteCommitmentTree::uncommitted())
}
fn empty_root(level: usize) -> Self {
let layer_below: usize = MERKLE_DEPTH - level;
Self(EMPTY_ROOTS[layer_below])
}
}
impl incrementalmerkletree::Hashable for Node {
fn empty_leaf() -> Self {
Self(NoteCommitmentTree::uncommitted())
@ -372,3 +417,67 @@ impl From<Vec<pallas::Base>> for NoteCommitmentTree {
tree
}
}
/// A serialized Orchard note commitment tree.
///
/// The format of the serialized data is compatible with
/// [`CommitmentTree`](merkle_tree::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`](merkle_tree::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, 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 = CommitmentTree::from_frontier(&tree.inner);
tree.write(&mut serialized_tree)
.expect("note commitment tree should be serializable");
Self(serialized_tree)
}
}
impl From<Option<Arc<NoteCommitmentTree>>> for SerializedTree {
fn from(maybe_tree: Option<Arc<NoteCommitmentTree>>) -> Self {
match maybe_tree {
Some(tree) => tree.as_ref().into(),
None => Self(Vec::new()),
}
}
}
impl AsRef<[u8]> for SerializedTree {
fn as_ref(&self) -> &[u8] {
&self.0
}
}

View File

@ -17,18 +17,27 @@ use std::{
hash::{Hash, Hasher},
io,
ops::Deref,
sync::Arc,
};
use bitvec::prelude::*;
use incrementalmerkletree::{bridgetree, Frontier};
use incrementalmerkletree::{
bridgetree::{self, Leaf},
Frontier,
};
use lazy_static::lazy_static;
use thiserror::Error;
use zcash_encoding::{Optional, Vector};
use zcash_primitives::merkle_tree::{self, Hashable};
use super::commitment::pedersen_hashes::pedersen_hash;
use crate::serialization::{
serde_helpers, ReadZcashExt, SerializationError, ZcashDeserialize, ZcashSerialize,
};
pub(super) const MERKLE_DEPTH: usize = 32;
/// MerkleCRH^Sapling Hash Function
@ -157,9 +166,45 @@ impl ZcashDeserialize for Root {
///
/// Note that it's handled as a byte buffer and not a point coordinate (jubjub::Fq)
/// because that's how the spec handles the MerkleCRH^Sapling function inputs and outputs.
#[derive(Clone, Debug)]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
struct Node([u8; 32]);
/// Required to convert [`NoteCommitmentTree`] into [`SerializedTree`].
///
/// Zebra stores Sapling note commitment trees as [`Frontier`][1]s while the
/// [`z_gettreestate`][2] RPC requires [`CommitmentTree`][3]s. Implementing
/// [`merkle_tree::Hashable`] for [`Node`]s allows the conversion.
///
/// [1]: bridgetree::Frontier
/// [2]: https://zcash.github.io/rpc/z_gettreestate.html
/// [3]: merkle_tree::CommitmentTree
impl merkle_tree::Hashable for Node {
fn read<R: io::Read>(mut reader: R) -> io::Result<Self> {
let mut node = [0u8; 32];
reader.read_exact(&mut node)?;
Ok(Self(node))
}
fn write<W: io::Write>(&self, mut writer: W) -> io::Result<()> {
writer.write_all(self.0.as_ref())
}
fn combine(level: usize, a: &Self, b: &Self) -> Self {
let level = u8::try_from(level).expect("level must fit into u8");
let layer = (MERKLE_DEPTH - 1) as u8 - level;
Self(merkle_crh_sapling(layer, a.0, b.0))
}
fn blank() -> Self {
Self(NoteCommitmentTree::uncommitted())
}
fn empty_root(level: usize) -> Self {
let layer_below = MERKLE_DEPTH - level;
Self(EMPTY_ROOTS[layer_below])
}
}
impl incrementalmerkletree::Hashable for Node {
fn empty_leaf() -> Self {
Self(NoteCommitmentTree::uncommitted())
@ -217,7 +262,7 @@ pub enum NoteCommitmentTreeError {
/// Sapling Incremental Note Commitment Tree.
#[derive(Debug, Serialize, Deserialize)]
pub struct NoteCommitmentTree {
/// The tree represented as a Frontier.
/// The tree represented as a [`Frontier`](bridgetree::Frontier).
///
/// A Frontier is a subset of the tree that allows to fully specify it.
/// It consists of nodes along the rightmost (newer) branch of the tree that
@ -226,8 +271,9 @@ pub struct NoteCommitmentTree {
///
/// # Consensus
///
/// > [Sapling onward] A block MUST NOT add Sapling note commitments that would result in the Sapling note
/// > commitment tree exceeding its capacity of 2^(MerkleDepth^Sapling) leaf nodes.
/// > [Sapling onward] A block MUST NOT add Sapling note commitments that
/// > would result in the Sapling note commitment tree exceeding its capacity
/// > of 2^(MerkleDepth^Sapling) leaf nodes.
///
/// <https://zips.z.cash/protocol/protocol.pdf#merkletree>
///
@ -236,18 +282,19 @@ pub struct NoteCommitmentTree {
/// A cached root of the tree.
///
/// Every time the root is computed by [`Self::root`] it is cached here,
/// and the cached value will be returned by [`Self::root`] until the tree is
/// changed by [`Self::append`]. This greatly increases performance
/// because it avoids recomputing the root when the tree does not change
/// between blocks. In the finalized state, the tree is read from
/// disk for every block processed, which would also require recomputing
/// the root even if it has not changed (note that the cached root is
/// serialized with the tree). This is particularly important since we decided
/// to instantiate the trees from the genesis block, for simplicity.
/// Every time the root is computed by [`Self::root`] it is cached here, and
/// the cached value will be returned by [`Self::root`] until the tree is
/// changed by [`Self::append`]. This greatly increases performance because
/// it avoids recomputing the root when the tree does not change between
/// blocks. In the finalized state, the tree is read from disk for every
/// block processed, which would also require recomputing the root even if
/// it has not changed (note that the cached root is serialized with the
/// tree). This is particularly important since we decided to instantiate
/// the trees from the genesis block, for simplicity.
///
/// We use a [`RwLock`] for this cache, because it is only written once per tree update.
/// Each tree has its own cached root, a new lock is created for each clone.
/// We use a [`RwLock`] for this cache, because it is only written once per
/// tree update. Each tree has its own cached root, a new lock is created
/// for each clone.
cached_root: std::sync::RwLock<Option<Root>>,
}
@ -305,7 +352,7 @@ impl NoteCommitmentTree {
}
}
/// Get the Jubjub-based Pedersen hash of root node of this merkle tree of
/// Gets the Jubjub-based Pedersen hash of root node of this merkle tree of
/// note commitments.
pub fn hash(&self) -> [u8; 32] {
self.root().into()
@ -320,7 +367,7 @@ impl NoteCommitmentTree {
jubjub::Fq::one().to_bytes()
}
/// Count of note commitments added to the tree.
/// Counts of note commitments added to the tree.
///
/// For Sapling, the tree is capped at 2^32.
pub fn count(&self) -> u64 {
@ -361,7 +408,7 @@ impl PartialEq for NoteCommitmentTree {
}
impl From<Vec<jubjub::Fq>> for NoteCommitmentTree {
/// Compute the tree from a whole bunch of note commitments at once.
/// Computes the tree from a whole bunch of note commitments at once.
fn from(values: Vec<jubjub::Fq>) -> Self {
let mut tree = Self::default();
@ -376,3 +423,131 @@ impl From<Vec<jubjub::Fq>> for NoteCommitmentTree {
tree
}
}
/// A serialized Sapling note commitment tree.
///
/// The format of the serialized data is compatible with
/// [`CommitmentTree`](merkle_tree::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`](merkle_tree::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, Eq, PartialEq, serde::Serialize)]
pub struct SerializedTree(Vec<u8>);
impl From<&NoteCommitmentTree> for SerializedTree {
fn from(tree: &NoteCommitmentTree) -> Self {
let mut serialized_tree = vec![];
// 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) = tree.inner.value() {
let (left_leaf, right_leaf) = match frontier.leaf() {
Leaf::Left(left_value) => (Some(left_value), None),
Leaf::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: usize = frontier.position().into();
// The lowest bit does not indicate the presence of any ommers. We
// clear it so that we can test if there are no set bits left in
// [`position`].
position &= !1;
// Run through the bits of [`position`], and push an ommer for each
// set bit, or `None` otherwise. In contrast to the 'zcashd' code
// linked above, we want to skip any trailing `None` parents at the
// top of the tree. To do that, we clear the bits as we go through
// them, and break early if the remaining bits are all zero (i.e.
// [`position`] is zero).
let mut parents = vec![];
for i in 1..MERKLE_DEPTH {
// Test each bit in [`position`] individually. Don't test the
// lowest bit since it doesn't actually indicate the position of
// any ommer.
let bit_mask = 1 << i;
if position & bit_mask == 0 {
parents.push(None);
} else {
parents.push(ommers_iter.next());
// Clear the set bit so that we can test if there are no set
// bits left.
position &= !bit_mask;
// If there are no set bits left, exit early so that there
// are no empty trailing parent nodes in the serialized
// tree.
if position == 0 {
break;
}
}
}
// Serialize the converted note commitment tree.
Optional::write(&mut serialized_tree, left_leaf, |tree, leaf| {
leaf.write(tree)
})
.expect("A leaf in a note commitment tree should be serializable");
Optional::write(&mut serialized_tree, right_leaf, |tree, leaf| {
leaf.write(tree)
})
.expect("A leaf in a note commitment tree should be serializable");
Vector::write(&mut serialized_tree, &parents, |tree, parent| {
Optional::write(tree, *parent, |tree, parent| parent.write(tree))
})
.expect("Parent nodes in a note commitment tree should be serializable");
}
Self(serialized_tree)
}
}
impl From<Option<Arc<NoteCommitmentTree>>> for SerializedTree {
fn from(maybe_tree: Option<Arc<NoteCommitmentTree>>) -> Self {
match maybe_tree {
Some(tree) => tree.as_ref().into(),
None => Self(vec![]),
}
}
}
impl AsRef<[u8]> for SerializedTree {
fn as_ref(&self) -> &[u8] {
&self.0
}
}

View File

@ -21,7 +21,9 @@ use tracing::Instrument;
use zebra_chain::{
block::{self, Height, SerializedBlock},
chain_tip::ChainTip,
orchard,
parameters::{ConsensusBranchId, Network, NetworkUpgrade},
sapling,
serialization::{SerializationError, ZcashDeserialize},
transaction::{self, SerializedTransaction, Transaction, UnminedTx},
transparent::{self, Address},
@ -149,6 +151,23 @@ pub trait Rpc {
#[rpc(name = "getrawmempool")]
fn get_raw_mempool(&self) -> BoxFuture<Result<Vec<String>>>;
/// Returns information about the given block's Sapling & Orchard tree state.
///
/// zcashd reference: [`z_gettreestate`](https://zcash.github.io/rpc/z_gettreestate.html)
///
/// # Parameters
///
/// - `hash | height`: (string, required) The block hash or height.
///
/// # Notes
///
/// The zcashd doc reference above says that the parameter "`height` can be
/// negative where -1 is the last known valid block". On the other hand,
/// `lightwalletd` only uses positive heights, so Zebra does not support
/// negative heights.
#[rpc(name = "z_gettreestate")]
fn z_get_treestate(&self, hash_or_height: String) -> BoxFuture<Result<GetTreestate>>;
/// Returns the raw transaction data, as a [`GetRawTransaction`] JSON string or structure.
///
/// zcashd reference: [`getrawtransaction`](https://zcash.github.io/rpc/getrawtransaction.html)
@ -660,6 +679,120 @@ where
.boxed()
}
fn z_get_treestate(&self, hash_or_height: String) -> BoxFuture<Result<GetTreestate>> {
let mut state = self.state.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,
})?;
// 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
.ready()
.and_then(|service| service.call(block_request))
.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 {
zebra_state::ReadResponse::Block(Some(block)) => block,
zebra_state::ReadResponse::Block(None) => {
return Err(Error {
code: ErrorCode::ServerError(0),
message: "the requested block was not found".to_string(),
data: None,
})
}
_ => unreachable!("unmatched response to a block request"),
};
// 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 hash = block.hash();
let height = block
.coinbase_height()
.expect("verified blocks have a valid 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,
},
},
})
}
.boxed()
}
fn get_address_tx_ids(
&self,
request: GetAddressTxIdsRequest,
@ -947,6 +1080,65 @@ pub struct GetBlock(#[serde(with = "hex")] SerializedBlock);
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct GetBestBlockHash(#[serde(with = "hex")] block::Hash);
/// 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>,
}
/// A treestate that is included in the [`z_gettreestate`][1] RPC response.
///
/// [1]: https://zcash.github.io/rpc/z_gettreestate.html
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
struct Treestate<Tree: AsRef<[u8]>> {
/// Contains an Orchard or Sapling serialized note commitment tree,
/// hex-encoded.
commitments: Commitments<Tree>,
}
/// A wrapper that contains either an Orchard or Sapling note commitment tree.
///
/// Note that in the original [`z_gettreestate`][1] RPC, [`Commitments`] also
/// contains the field `finalRoot`. Zebra does *not* use this field.
///
/// [1]: https://zcash.github.io/rpc/z_gettreestate.html
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
struct Commitments<Tree: AsRef<[u8]>> {
/// Orchard or Sapling serialized note commitment tree, hex-encoded.
#[serde(with = "hex")]
#[serde(rename = "finalState")]
final_state: Tree,
}
impl<Tree: AsRef<[u8]>> Treestate<Tree> {
/// Returns `true` if there's no serialized commitment tree.
fn is_empty(&self) -> bool {
self.commitments.final_state.as_ref().is_empty()
}
}
/// Response to a `getrawtransaction` RPC request.
///
/// See the notes for the [`Rpc::get_raw_transaction` method].

View File

@ -9,6 +9,7 @@ use std::{
use zebra_chain::{
amount::NegativeAllowed,
block::{self, Block},
serialization::SerializationError,
transaction,
transparent::{self, utxos_from_ordered_utxos},
value_balance::{ValueBalance, ValueBalanceError},
@ -57,6 +58,19 @@ impl From<block::Height> for HashOrHeight {
}
}
impl std::str::FromStr for HashOrHeight {
type Err = SerializationError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.parse()
.map(Self::Hash)
.or_else(|_| s.parse().map(Self::Height))
.map_err(|_| {
SerializationError::Parse("could not convert the input string to a hash or height")
})
}
}
/// A block which has undergone semantic validation and has been prepared for
/// contextual validation.
///
@ -444,7 +458,25 @@ pub enum ReadRequest {
/// Returns an [`Amount`] with the total balance of the set of addresses.
AddressBalance(HashSet<transparent::Address>),
/// Looks up transaction hashes that sent or received from addresses,
/// Looks up a Sapling note commitment tree either by a hash or height.
///
/// Returns
///
/// * [`ReadResponse::SaplingTree(Some(Arc<NoteCommitmentTree>))`](crate::ReadResponse::SaplingTree)
/// if the corresponding block contains a Sapling note commitment tree.
/// * [`ReadResponse::SaplingTree(None)`](crate::ReadResponse::SaplingTree) otherwise.
SaplingTree(HashOrHeight),
/// Looks up an Orchard note commitment tree either by a hash or height.
///
/// Returns
///
/// * [`ReadResponse::OrchardTree(Some(Arc<NoteCommitmentTree>))`](crate::ReadResponse::OrchardTree)
/// if the corresponding block contains a Sapling note commitment tree.
/// * [`ReadResponse::OrchardTree(None)`](crate::ReadResponse::OrchardTree) otherwise.
OrchardTree(HashOrHeight),
/// Looks up transaction hashes that were sent or received from addresses,
/// in an inclusive blockchain height range.
///
/// Returns

View File

@ -5,6 +5,7 @@ use std::{collections::BTreeMap, sync::Arc};
use zebra_chain::{
amount::{Amount, NonNegative},
block::{self, Block},
orchard, sapling,
transaction::{self, Transaction},
transparent,
};
@ -49,14 +50,28 @@ pub enum Response {
}
#[derive(Clone, Debug, PartialEq, Eq)]
/// A response to a read-only [`ReadStateService`] [`ReadRequest`].
/// A response to a read-only [`ReadStateService`](crate::ReadStateService)'s
/// [`ReadRequest`](crate::ReadRequest).
pub enum ReadResponse {
/// Response to [`ReadRequest::Block`] with the specified block.
/// Response to [`ReadRequest::Block`](crate::ReadRequest::Block) with the
/// specified block.
Block(Option<Arc<Block>>),
/// Response to [`ReadRequest::Transaction`] with the specified transaction.
/// Response to
/// [`ReadRequest::Transaction`](crate::ReadRequest::Transaction) with the
/// specified transaction.
Transaction(Option<(Arc<Transaction>, block::Height)>),
/// Response to
/// [`ReadRequest::SaplingTree`](crate::ReadRequest::SaplingTree) with the
/// specified Sapling note commitment tree.
SaplingTree(Option<Arc<sapling::tree::NoteCommitmentTree>>),
/// Response to
/// [`ReadRequest::OrchardTree`](crate::ReadRequest::OrchardTree) with the
/// specified Orchard note commitment tree.
OrchardTree(Option<Arc<orchard::tree::NoteCommitmentTree>>),
/// Response to [`ReadRequest::AddressBalance`] with the total balance of the addresses.
AddressBalance(Amount<NonNegative>),

View File

@ -940,15 +940,6 @@ impl Service<ReadRequest> for ReadStateService {
#[instrument(name = "read_state", skip(self))]
fn call(&mut self, req: ReadRequest) -> Self::Future {
match req {
// TODO: implement these new ReadRequests for lightwalletd, as part of these tickets
// z_get_tree_state (#3156)
// depends on transparent address indexes (#3150)
// get_address_tx_ids (#3147)
// get_address_balance (#3157)
// get_address_utxos (#3158)
// Used by get_block RPC.
ReadRequest::Block(hash_or_height) => {
metrics::counter!(
@ -992,6 +983,46 @@ impl Service<ReadRequest> for ReadStateService {
.boxed()
}
ReadRequest::SaplingTree(hash_or_height) => {
metrics::counter!(
"state.requests",
1,
"service" => "read_state",
"type" => "sapling_tree",
);
let state = self.clone();
async move {
let sapling_tree = state.best_chain_receiver.with_watch_data(|best_chain| {
read::sapling_tree(best_chain, &state.db, hash_or_height)
});
Ok(ReadResponse::SaplingTree(sapling_tree))
}
.boxed()
}
ReadRequest::OrchardTree(hash_or_height) => {
metrics::counter!(
"state.requests",
1,
"service" => "read_state",
"type" => "orchard_tree",
);
let state = self.clone();
async move {
let orchard_tree = state.best_chain_receiver.with_watch_data(|best_chain| {
read::orchard_tree(best_chain, &state.db, hash_or_height)
});
Ok(ReadResponse::OrchardTree(orchard_tree))
}
.boxed()
}
// For the get_address_tx_ids RPC.
ReadRequest::TransactionIdsByAddresses {
addresses,

View File

@ -20,7 +20,9 @@ use zebra_chain::{
amount::NonNegative,
block::{self, Block, Height},
history_tree::HistoryTree,
orchard,
parameters::{Network, GENESIS_PREVIOUS_BLOCK_HASH},
sapling,
serialization::TrustedPreallocate,
transaction::{self, Transaction},
transparent,
@ -111,6 +113,34 @@ impl ZebraDb {
}))
}
/// Returns the Sapling
/// [`NoteCommitmentTree`](sapling::tree::NoteCommitmentTree) specified by a
/// hash or height, if it exists in the finalized `db`.
pub fn sapling_tree(
&self,
hash_or_height: HashOrHeight,
) -> Option<Arc<sapling::tree::NoteCommitmentTree>> {
let height = hash_or_height.height_or_else(|hash| self.height(hash))?;
let sapling_tree_handle = self.db.cf_handle("sapling_note_commitment_tree").unwrap();
self.db.zs_get(&sapling_tree_handle, &height)
}
/// Returns the Orchard
/// [`NoteCommitmentTree`](orchard::tree::NoteCommitmentTree) specified by a
/// hash or height, if it exists in the finalized `db`.
pub fn orchard_tree(
&self,
hash_or_height: HashOrHeight,
) -> Option<Arc<orchard::tree::NoteCommitmentTree>> {
let height = hash_or_height.height_or_else(|hash| self.height(hash))?;
let orchard_tree_handle = self.db.cf_handle("orchard_note_commitment_tree").unwrap();
self.db.zs_get(&orchard_tree_handle, &height)
}
// Read tip block methods
/// Returns the hash of the current finalized tip block.

View File

@ -394,6 +394,32 @@ impl Chain {
)
}
/// Returns the Sapling
/// [`NoteCommitmentTree`](sapling::tree::NoteCommitmentTree) specified by a
/// hash or height, if it exists in the non-finalized `chain`.
pub fn sapling_tree(
&self,
hash_or_height: HashOrHeight,
) -> Option<&sapling::tree::NoteCommitmentTree> {
let height =
hash_or_height.height_or_else(|hash| self.height_by_hash.get(&hash).cloned())?;
self.sapling_trees_by_height.get(&height)
}
/// Returns the Orchard
/// [`NoteCommitmentTree`](orchard::tree::NoteCommitmentTree) specified by a
/// hash or height, if it exists in the non-finalized `chain`.
pub fn orchard_tree(
&self,
hash_or_height: HashOrHeight,
) -> Option<&orchard::tree::NoteCommitmentTree> {
let height =
hash_or_height.height_or_else(|hash| self.height_by_hash.get(&hash).cloned())?;
self.orchard_trees_by_height.get(&height)
}
/// Returns the block hash of the tip block.
pub fn non_finalized_tip_hash(&self) -> block::Hash {
self.blocks

View File

@ -1,8 +1,11 @@
//! Shared state reading code.
//!
//! Used by [`StateService`] and [`ReadStateService`]
//! to read from the best [`Chain`] in the [`NonFinalizedState`],
//! and the database in the [`FinalizedState`].
//! Used by [`StateService`](crate::StateService) and
//! [`ReadStateService`](crate::ReadStateService) to read from the best
//! [`Chain`] in the
//! [`NonFinalizedState`](crate::service::non_finalized_state::NonFinalizedState),
//! and the database in the
//! [`FinalizedState`](crate::service::finalized_state::FinalizedState).
use std::{
collections::{BTreeMap, BTreeSet, HashSet},
@ -13,7 +16,9 @@ use std::{
use zebra_chain::{
amount::{self, Amount, NegativeAllowed, NonNegative},
block::{self, Block, Height},
orchard,
parameters::Network,
sapling,
transaction::{self, Transaction},
transparent,
};
@ -44,8 +49,8 @@ const FINALIZED_ADDRESS_INDEX_RETRIES: usize = 3;
pub const ADDRESS_HEIGHTS_FULL_RANGE: RangeInclusive<Height> = Height(1)..=Height::MAX;
/// Returns the [`Block`] with [`block::Hash`](zebra_chain::block::Hash) or
/// [`Height`](zebra_chain::block::Height),
/// if it exists in the non-finalized `chain` or finalized `db`.
/// [`Height`](zebra_chain::block::Height), if it exists in the non-finalized
/// `chain` or finalized `db`.
pub(crate) fn block<C>(
chain: Option<C>,
db: &ZebraDb,
@ -56,12 +61,13 @@ where
{
// # Correctness
//
// The StateService commits blocks to the finalized state before updating the latest chain,
// and it can commit additional blocks after we've cloned this `chain` variable.
// The StateService commits blocks to the finalized state before updating
// the latest chain, and it can commit additional blocks after we've cloned
// this `chain` variable.
//
// Since blocks are the same in the finalized and non-finalized state,
// we check the most efficient alternative first.
// (`chain` is always in memory, but `db` stores blocks on disk, with a memory cache.)
// Since blocks are the same in the finalized and non-finalized state, we
// check the most efficient alternative first. (`chain` is always in memory,
// but `db` stores blocks on disk, with a memory cache.)
chain
.as_ref()
.and_then(|chain| chain.as_ref().block(hash_or_height))
@ -69,8 +75,8 @@ where
.or_else(|| db.block(hash_or_height))
}
/// Returns the [`Transaction`] with [`transaction::Hash`],
/// if it exists in the non-finalized `chain` or finalized `db`.
/// Returns the [`Transaction`] with [`transaction::Hash`], if it exists in the
/// non-finalized `chain` or finalized `db`.
pub(crate) fn transaction<C>(
chain: Option<C>,
db: &ZebraDb,
@ -81,12 +87,13 @@ where
{
// # Correctness
//
// The StateService commits blocks to the finalized state before updating the latest chain,
// and it can commit additional blocks after we've cloned this `chain` variable.
// The StateService commits blocks to the finalized state before updating
// the latest chain, and it can commit additional blocks after we've cloned
// this `chain` variable.
//
// Since transactions are the same in the finalized and non-finalized state,
// we check the most efficient alternative first.
// (`chain` is always in memory, but `db` stores transactions on disk, with a memory cache.)
// we check the most efficient alternative first. (`chain` is always in
// memory, but `db` stores transactions on disk, with a memory cache.)
chain
.as_ref()
.and_then(|chain| {
@ -98,6 +105,60 @@ where
.or_else(|| db.transaction(hash))
}
/// Returns the Sapling
/// [`NoteCommitmentTree`](sapling::tree::NoteCommitmentTree) specified by a
/// hash or height, if it exists in the non-finalized `chain` or finalized `db`.
pub(crate) fn sapling_tree<C>(
chain: Option<C>,
db: &ZebraDb,
hash_or_height: HashOrHeight,
) -> Option<Arc<sapling::tree::NoteCommitmentTree>>
where
C: AsRef<Chain>,
{
// # Correctness
//
// The StateService commits blocks to the finalized state before updating
// the latest chain, and it can commit additional blocks after we've cloned
// this `chain` variable.
//
// Since sapling treestates are the same in the finalized and non-finalized
// state, we check the most efficient alternative first. (`chain` is always
// in memory, but `db` stores blocks on disk, with a memory cache.)
chain
.as_ref()
.and_then(|chain| chain.as_ref().sapling_tree(hash_or_height).cloned())
.map(Arc::new)
.or_else(|| db.sapling_tree(hash_or_height))
}
/// Returns the Orchard
/// [`NoteCommitmentTree`](orchard::tree::NoteCommitmentTree) specified by a
/// hash or height, if it exists in the non-finalized `chain` or finalized `db`.
pub(crate) fn orchard_tree<C>(
chain: Option<C>,
db: &ZebraDb,
hash_or_height: HashOrHeight,
) -> Option<Arc<orchard::tree::NoteCommitmentTree>>
where
C: AsRef<Chain>,
{
// # Correctness
//
// The StateService commits blocks to the finalized state before updating
// the latest chain, and it can commit additional blocks after we've cloned
// this `chain` variable.
//
// Since orchard treestates are the same in the finalized and non-finalized
// state, we check the most efficient alternative first. (`chain` is always
// in memory, but `db` stores blocks on disk, with a memory cache.)
chain
.as_ref()
.and_then(|chain| chain.as_ref().orchard_tree(hash_or_height).cloned())
.map(Arc::new)
.or_else(|| db.orchard_tree(hash_or_height))
}
/// Returns the total transparent balance for the supplied [`transparent::Address`]es.
///
/// If the addresses do not exist in the non-finalized `chain` or finalized `db`, returns zero.