diff --git a/Cargo.lock b/Cargo.lock index 0bb206f9..dabcd5dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/zebra-chain/Cargo.toml b/zebra-chain/Cargo.toml index 19d5e2b3..4c8fb164 100644 --- a/zebra-chain/Cargo.toml +++ b/zebra-chain/Cargo.toml @@ -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 } diff --git a/zebra-chain/src/block.rs b/zebra-chain/src/block.rs index 394880ca..584173c8 100644 --- a/zebra-chain/src/block.rs +++ b/zebra-chain/src/block.rs @@ -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 { self.transactions .get(0) diff --git a/zebra-chain/src/block/header.rs b/zebra-chain/src/block/header.rs index 50e95233..53b133d7 100644 --- a/zebra-chain/src/block/header.rs +++ b/zebra-chain/src/block/header.rs @@ -1,3 +1,5 @@ +//! The block header. + use std::usize; use chrono::{DateTime, Duration, Utc}; diff --git a/zebra-chain/src/orchard/tree.rs b/zebra-chain/src/orchard/tree.rs index 636ce1d2..28463a6d 100644 --- a/zebra-chain/src/orchard/tree.rs +++ b/zebra-chain/src/orchard/tree.rs @@ -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(mut reader: R) -> io::Result { + let mut repr = [0u8; 32]; + reader.read_exact(&mut repr)?; + let maybe_node = pallas::Base::from_repr(repr).map(Self); + + >::from(maybe_node).ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + "Non-canonical encoding of Pallas base field value.", + ) + }) + } + + fn 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> 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); + +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>> for SerializedTree { + fn from(maybe_tree: Option>) -> Self { + match maybe_tree { + Some(tree) => tree.as_ref().into(), + None => Self(Vec::new()), + } + } +} + +impl AsRef<[u8]> for SerializedTree { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} diff --git a/zebra-chain/src/sapling/tree.rs b/zebra-chain/src/sapling/tree.rs index a1a1321a..76a524b5 100644 --- a/zebra-chain/src/sapling/tree.rs +++ b/zebra-chain/src/sapling/tree.rs @@ -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(mut reader: R) -> io::Result { + let mut node = [0u8; 32]; + reader.read_exact(&mut node)?; + Ok(Self(node)) + } + + fn 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. /// /// /// @@ -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>, } @@ -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> 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) -> Self { let mut tree = Self::default(); @@ -376,3 +423,131 @@ impl From> 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); + +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]: + 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>> for SerializedTree { + fn from(maybe_tree: Option>) -> Self { + match maybe_tree { + Some(tree) => tree.as_ref().into(), + None => Self(vec![]), + } + } +} + +impl AsRef<[u8]> for SerializedTree { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} diff --git a/zebra-rpc/src/methods.rs b/zebra-rpc/src/methods.rs index b458ca6f..9e5e7314 100644 --- a/zebra-rpc/src/methods.rs +++ b/zebra-rpc/src/methods.rs @@ -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>>; + /// 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>; + /// 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> { + 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, + + /// A treestate containing an Orchard note commitment tree, hex-encoded. + #[serde(skip_serializing_if = "Treestate::is_empty")] + orchard: Treestate, +} + +/// A treestate that is included in the [`z_gettreestate`][1] RPC response. +/// +/// [1]: https://zcash.github.io/rpc/z_gettreestate.html +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] +struct Treestate> { + /// Contains an Orchard or Sapling serialized note commitment tree, + /// hex-encoded. + commitments: Commitments, +} + +/// A wrapper that contains either an Orchard or Sapling note commitment tree. +/// +/// Note that in the original [`z_gettreestate`][1] RPC, [`Commitments`] also +/// contains the field `finalRoot`. Zebra does *not* use this field. +/// +/// [1]: https://zcash.github.io/rpc/z_gettreestate.html +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] +struct Commitments> { + /// Orchard or Sapling serialized note commitment tree, hex-encoded. + #[serde(with = "hex")] + #[serde(rename = "finalState")] + final_state: Tree, +} + +impl> Treestate { + /// Returns `true` if there's no serialized commitment tree. + fn is_empty(&self) -> bool { + self.commitments.final_state.as_ref().is_empty() + } +} + /// Response to a `getrawtransaction` RPC request. /// /// See the notes for the [`Rpc::get_raw_transaction` method]. diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 33563fde..ad7162a2 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -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 for HashOrHeight { } } +impl std::str::FromStr for HashOrHeight { + type Err = SerializationError; + + fn from_str(s: &str) -> Result { + 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), - /// 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))`](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))`](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 diff --git a/zebra-state/src/response.rs b/zebra-state/src/response.rs index dc2eee5e..a2607108 100644 --- a/zebra-state/src/response.rs +++ b/zebra-state/src/response.rs @@ -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>), - /// Response to [`ReadRequest::Transaction`] with the specified transaction. + /// Response to + /// [`ReadRequest::Transaction`](crate::ReadRequest::Transaction) with the + /// specified transaction. Transaction(Option<(Arc, block::Height)>), + /// Response to + /// [`ReadRequest::SaplingTree`](crate::ReadRequest::SaplingTree) with the + /// specified Sapling note commitment tree. + SaplingTree(Option>), + + /// Response to + /// [`ReadRequest::OrchardTree`](crate::ReadRequest::OrchardTree) with the + /// specified Orchard note commitment tree. + OrchardTree(Option>), + /// Response to [`ReadRequest::AddressBalance`] with the total balance of the addresses. AddressBalance(Amount), diff --git a/zebra-state/src/service.rs b/zebra-state/src/service.rs index 059cb6f7..78ef6597 100644 --- a/zebra-state/src/service.rs +++ b/zebra-state/src/service.rs @@ -940,15 +940,6 @@ impl Service 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 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, diff --git a/zebra-state/src/service/finalized_state/zebra_db/block.rs b/zebra-state/src/service/finalized_state/zebra_db/block.rs index 33addbb2..766dd661 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/block.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/block.rs @@ -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> { + 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> { + 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. diff --git a/zebra-state/src/service/non_finalized_state/chain.rs b/zebra-state/src/service/non_finalized_state/chain.rs index d0959728..085f11ce 100644 --- a/zebra-state/src/service/non_finalized_state/chain.rs +++ b/zebra-state/src/service/non_finalized_state/chain.rs @@ -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 diff --git a/zebra-state/src/service/read.rs b/zebra-state/src/service/read.rs index 76b058bf..c0fc5c06 100644 --- a/zebra-state/src/service/read.rs +++ b/zebra-state/src/service/read.rs @@ -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(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( chain: Option, 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( chain: Option, 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( + chain: Option, + db: &ZebraDb, + hash_or_height: HashOrHeight, +) -> Option> +where + C: AsRef, +{ + // # 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( + chain: Option, + db: &ZebraDb, + hash_or_height: HashOrHeight, +) -> Option> +where + C: AsRef, +{ + // # 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.