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:
parent
fd7f49fb0c
commit
7c726b246d
|
@ -1485,6 +1485,15 @@ dependencies = [
|
||||||
"byteorder",
|
"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]]
|
[[package]]
|
||||||
name = "eyre"
|
name = "eyre"
|
||||||
version = "0.6.7"
|
version = "0.6.7"
|
||||||
|
@ -5908,6 +5917,15 @@ dependencies = [
|
||||||
"nonempty",
|
"nonempty",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zcash_encoding"
|
||||||
|
version = "0.0.0"
|
||||||
|
source = "git+https://github.com/zcash/librustzcash.git?rev=d5c5f04#d5c5f048947d211c2fbef23ce986b329b91d1aa5"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"nonempty",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zcash_history"
|
name = "zcash_history"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
@ -5940,6 +5958,17 @@ dependencies = [
|
||||||
"subtle",
|
"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]]
|
[[package]]
|
||||||
name = "zcash_primitives"
|
name = "zcash_primitives"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
@ -6008,6 +6037,42 @@ dependencies = [
|
||||||
"zcash_note_encryption 0.1.0 (git+https://github.com/zcash/librustzcash.git?rev=d14e7a707ce01cefcbc82651dad48f002185dded)",
|
"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]]
|
[[package]]
|
||||||
name = "zcash_proofs"
|
name = "zcash_proofs"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
@ -6095,9 +6160,10 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
"uint",
|
"uint",
|
||||||
"x25519-dalek",
|
"x25519-dalek",
|
||||||
|
"zcash_encoding 0.0.0 (git+https://github.com/zcash/librustzcash.git?rev=d5c5f04)",
|
||||||
"zcash_history",
|
"zcash_history",
|
||||||
"zcash_note_encryption 0.1.0 (git+https://github.com/zcash/librustzcash.git?rev=d14e7a707ce01cefcbc82651dad48f002185dded)",
|
"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",
|
"zebra-test",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,8 @@ orchard = "=0.1.0-beta.3"
|
||||||
|
|
||||||
equihash = "0.1.0"
|
equihash = "0.1.0"
|
||||||
zcash_note_encryption = "0.1"
|
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" }
|
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 }
|
proptest = { version = "0.10.1", optional = true }
|
||||||
|
|
|
@ -67,6 +67,10 @@ impl fmt::Display for Block {
|
||||||
|
|
||||||
impl Block {
|
impl Block {
|
||||||
/// Return the block height reported in the coinbase transaction, if any.
|
/// 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> {
|
pub fn coinbase_height(&self) -> Option<Height> {
|
||||||
self.transactions
|
self.transactions
|
||||||
.get(0)
|
.get(0)
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
//! The block header.
|
||||||
|
|
||||||
use std::usize;
|
use std::usize;
|
||||||
|
|
||||||
use chrono::{DateTime, Duration, Utc};
|
use chrono::{DateTime, Duration, Utc};
|
||||||
|
|
|
@ -18,6 +18,7 @@ use std::{
|
||||||
hash::{Hash, Hasher},
|
hash::{Hash, Hasher},
|
||||||
io,
|
io,
|
||||||
ops::Deref,
|
ops::Deref,
|
||||||
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use bitvec::prelude::*;
|
use bitvec::prelude::*;
|
||||||
|
@ -25,6 +26,7 @@ use halo2::pasta::{group::ff::PrimeField, pallas};
|
||||||
use incrementalmerkletree::{bridgetree, Frontier};
|
use incrementalmerkletree::{bridgetree, Frontier};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use zcash_primitives::merkle_tree::{self, CommitmentTree};
|
||||||
|
|
||||||
use super::sinsemilla::*;
|
use super::sinsemilla::*;
|
||||||
|
|
||||||
|
@ -153,9 +155,52 @@ impl ZcashDeserialize for Root {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A node of the Orchard Incremental Note Commitment Tree.
|
/// A node of the Orchard Incremental Note Commitment Tree.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
struct Node(pallas::Base);
|
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 {
|
impl incrementalmerkletree::Hashable for Node {
|
||||||
fn empty_leaf() -> Self {
|
fn empty_leaf() -> Self {
|
||||||
Self(NoteCommitmentTree::uncommitted())
|
Self(NoteCommitmentTree::uncommitted())
|
||||||
|
@ -372,3 +417,67 @@ 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`](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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -17,18 +17,27 @@ use std::{
|
||||||
hash::{Hash, Hasher},
|
hash::{Hash, Hasher},
|
||||||
io,
|
io,
|
||||||
ops::Deref,
|
ops::Deref,
|
||||||
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use bitvec::prelude::*;
|
use bitvec::prelude::*;
|
||||||
use incrementalmerkletree::{bridgetree, Frontier};
|
|
||||||
|
use incrementalmerkletree::{
|
||||||
|
bridgetree::{self, Leaf},
|
||||||
|
Frontier,
|
||||||
|
};
|
||||||
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::{self, Hashable};
|
||||||
|
|
||||||
use super::commitment::pedersen_hashes::pedersen_hash;
|
use super::commitment::pedersen_hashes::pedersen_hash;
|
||||||
|
|
||||||
use crate::serialization::{
|
use crate::serialization::{
|
||||||
serde_helpers, ReadZcashExt, SerializationError, ZcashDeserialize, ZcashSerialize,
|
serde_helpers, ReadZcashExt, SerializationError, ZcashDeserialize, ZcashSerialize,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(super) const MERKLE_DEPTH: usize = 32;
|
pub(super) const MERKLE_DEPTH: usize = 32;
|
||||||
|
|
||||||
/// MerkleCRH^Sapling Hash Function
|
/// 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)
|
/// 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.
|
/// 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]);
|
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 {
|
impl incrementalmerkletree::Hashable for Node {
|
||||||
fn empty_leaf() -> Self {
|
fn empty_leaf() -> Self {
|
||||||
Self(NoteCommitmentTree::uncommitted())
|
Self(NoteCommitmentTree::uncommitted())
|
||||||
|
@ -217,7 +262,7 @@ pub enum NoteCommitmentTreeError {
|
||||||
/// Sapling Incremental Note Commitment Tree.
|
/// Sapling Incremental Note Commitment Tree.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct NoteCommitmentTree {
|
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.
|
/// 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
|
/// It consists of nodes along the rightmost (newer) branch of the tree that
|
||||||
|
@ -226,8 +271,9 @@ pub struct NoteCommitmentTree {
|
||||||
///
|
///
|
||||||
/// # Consensus
|
/// # Consensus
|
||||||
///
|
///
|
||||||
/// > [Sapling onward] A block MUST NOT add Sapling note commitments that would result in the Sapling note
|
/// > [Sapling onward] A block MUST NOT add Sapling note commitments that
|
||||||
/// > commitment tree exceeding its capacity of 2^(MerkleDepth^Sapling) leaf nodes.
|
/// > 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>
|
/// <https://zips.z.cash/protocol/protocol.pdf#merkletree>
|
||||||
///
|
///
|
||||||
|
@ -236,18 +282,19 @@ pub struct NoteCommitmentTree {
|
||||||
|
|
||||||
/// A cached root of the tree.
|
/// A cached root of the tree.
|
||||||
///
|
///
|
||||||
/// Every time the root is computed by [`Self::root`] it is cached here,
|
/// Every time the root is computed by [`Self::root`] it is cached here, and
|
||||||
/// and the cached value will be returned by [`Self::root`] until the tree is
|
/// the cached value will be returned by [`Self::root`] until the tree is
|
||||||
/// changed by [`Self::append`]. This greatly increases performance
|
/// changed by [`Self::append`]. This greatly increases performance because
|
||||||
/// because it avoids recomputing the root when the tree does not change
|
/// it avoids recomputing the root when the tree does not change between
|
||||||
/// between blocks. In the finalized state, the tree is read from
|
/// blocks. In the finalized state, the tree is read from disk for every
|
||||||
/// disk for every block processed, which would also require recomputing
|
/// block processed, which would also require recomputing the root even if
|
||||||
/// the root even if it has not changed (note that the cached root is
|
/// it has not changed (note that the cached root is serialized with the
|
||||||
/// serialized with the tree). This is particularly important since we decided
|
/// tree). This is particularly important since we decided to instantiate
|
||||||
/// to instantiate the trees from the genesis block, for simplicity.
|
/// the trees from the genesis block, for simplicity.
|
||||||
///
|
///
|
||||||
/// We use a [`RwLock`] for this cache, because it is only written once per tree update.
|
/// We use a [`RwLock`] for this cache, because it is only written once per
|
||||||
/// Each tree has its own cached root, a new lock is created for each clone.
|
/// tree update. Each tree has its own cached root, a new lock is created
|
||||||
|
/// for each clone.
|
||||||
cached_root: std::sync::RwLock<Option<Root>>,
|
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.
|
/// note commitments.
|
||||||
pub fn hash(&self) -> [u8; 32] {
|
pub fn hash(&self) -> [u8; 32] {
|
||||||
self.root().into()
|
self.root().into()
|
||||||
|
@ -320,7 +367,7 @@ impl NoteCommitmentTree {
|
||||||
jubjub::Fq::one().to_bytes()
|
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.
|
/// For Sapling, the tree is capped at 2^32.
|
||||||
pub fn count(&self) -> u64 {
|
pub fn count(&self) -> u64 {
|
||||||
|
@ -361,7 +408,7 @@ impl PartialEq for NoteCommitmentTree {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Vec<jubjub::Fq>> 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 {
|
fn from(values: Vec<jubjub::Fq>) -> Self {
|
||||||
let mut tree = Self::default();
|
let mut tree = Self::default();
|
||||||
|
|
||||||
|
@ -376,3 +423,131 @@ 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`](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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -21,7 +21,9 @@ use tracing::Instrument;
|
||||||
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::{SerializationError, ZcashDeserialize},
|
serialization::{SerializationError, ZcashDeserialize},
|
||||||
transaction::{self, SerializedTransaction, Transaction, UnminedTx},
|
transaction::{self, SerializedTransaction, Transaction, UnminedTx},
|
||||||
transparent::{self, Address},
|
transparent::{self, Address},
|
||||||
|
@ -149,6 +151,23 @@ pub trait Rpc {
|
||||||
#[rpc(name = "getrawmempool")]
|
#[rpc(name = "getrawmempool")]
|
||||||
fn get_raw_mempool(&self) -> BoxFuture<Result<Vec<String>>>;
|
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.
|
/// Returns the raw transaction data, as a [`GetRawTransaction`] JSON string or structure.
|
||||||
///
|
///
|
||||||
/// zcashd reference: [`getrawtransaction`](https://zcash.github.io/rpc/getrawtransaction.html)
|
/// zcashd reference: [`getrawtransaction`](https://zcash.github.io/rpc/getrawtransaction.html)
|
||||||
|
@ -660,6 +679,120 @@ where
|
||||||
.boxed()
|
.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(
|
fn get_address_tx_ids(
|
||||||
&self,
|
&self,
|
||||||
request: GetAddressTxIdsRequest,
|
request: GetAddressTxIdsRequest,
|
||||||
|
@ -947,6 +1080,65 @@ pub struct GetBlock(#[serde(with = "hex")] SerializedBlock);
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
pub struct GetBestBlockHash(#[serde(with = "hex")] block::Hash);
|
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.
|
/// 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].
|
||||||
|
|
|
@ -9,6 +9,7 @@ use std::{
|
||||||
use zebra_chain::{
|
use zebra_chain::{
|
||||||
amount::NegativeAllowed,
|
amount::NegativeAllowed,
|
||||||
block::{self, Block},
|
block::{self, Block},
|
||||||
|
serialization::SerializationError,
|
||||||
transaction,
|
transaction,
|
||||||
transparent::{self, utxos_from_ordered_utxos},
|
transparent::{self, utxos_from_ordered_utxos},
|
||||||
value_balance::{ValueBalance, ValueBalanceError},
|
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
|
/// A block which has undergone semantic validation and has been prepared for
|
||||||
/// contextual validation.
|
/// contextual validation.
|
||||||
///
|
///
|
||||||
|
@ -444,7 +458,25 @@ pub enum ReadRequest {
|
||||||
/// Returns an [`Amount`] with the total balance of the set of addresses.
|
/// Returns an [`Amount`] with the total balance of the set of addresses.
|
||||||
AddressBalance(HashSet<transparent::Address>),
|
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.
|
/// in an inclusive blockchain height range.
|
||||||
///
|
///
|
||||||
/// Returns
|
/// Returns
|
||||||
|
|
|
@ -5,6 +5,7 @@ use std::{collections::BTreeMap, sync::Arc};
|
||||||
use zebra_chain::{
|
use zebra_chain::{
|
||||||
amount::{Amount, NonNegative},
|
amount::{Amount, NonNegative},
|
||||||
block::{self, Block},
|
block::{self, Block},
|
||||||
|
orchard, sapling,
|
||||||
transaction::{self, Transaction},
|
transaction::{self, Transaction},
|
||||||
transparent,
|
transparent,
|
||||||
};
|
};
|
||||||
|
@ -49,14 +50,28 @@ pub enum Response {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[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 {
|
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>>),
|
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)>),
|
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.
|
/// Response to [`ReadRequest::AddressBalance`] with the total balance of the addresses.
|
||||||
AddressBalance(Amount<NonNegative>),
|
AddressBalance(Amount<NonNegative>),
|
||||||
|
|
||||||
|
|
|
@ -940,15 +940,6 @@ impl Service<ReadRequest> for ReadStateService {
|
||||||
#[instrument(name = "read_state", skip(self))]
|
#[instrument(name = "read_state", skip(self))]
|
||||||
fn call(&mut self, req: ReadRequest) -> Self::Future {
|
fn call(&mut self, req: ReadRequest) -> Self::Future {
|
||||||
match req {
|
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.
|
// Used by get_block RPC.
|
||||||
ReadRequest::Block(hash_or_height) => {
|
ReadRequest::Block(hash_or_height) => {
|
||||||
metrics::counter!(
|
metrics::counter!(
|
||||||
|
@ -992,6 +983,46 @@ impl Service<ReadRequest> for ReadStateService {
|
||||||
.boxed()
|
.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.
|
// For the get_address_tx_ids RPC.
|
||||||
ReadRequest::TransactionIdsByAddresses {
|
ReadRequest::TransactionIdsByAddresses {
|
||||||
addresses,
|
addresses,
|
||||||
|
|
|
@ -20,7 +20,9 @@ use zebra_chain::{
|
||||||
amount::NonNegative,
|
amount::NonNegative,
|
||||||
block::{self, Block, Height},
|
block::{self, Block, Height},
|
||||||
history_tree::HistoryTree,
|
history_tree::HistoryTree,
|
||||||
|
orchard,
|
||||||
parameters::{Network, GENESIS_PREVIOUS_BLOCK_HASH},
|
parameters::{Network, GENESIS_PREVIOUS_BLOCK_HASH},
|
||||||
|
sapling,
|
||||||
serialization::TrustedPreallocate,
|
serialization::TrustedPreallocate,
|
||||||
transaction::{self, Transaction},
|
transaction::{self, Transaction},
|
||||||
transparent,
|
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
|
// Read tip block methods
|
||||||
|
|
||||||
/// Returns the hash of the current finalized tip block.
|
/// Returns the hash of the current finalized tip block.
|
||||||
|
|
|
@ -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.
|
/// Returns the block hash of the tip block.
|
||||||
pub fn non_finalized_tip_hash(&self) -> block::Hash {
|
pub fn non_finalized_tip_hash(&self) -> block::Hash {
|
||||||
self.blocks
|
self.blocks
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
//! Shared state reading code.
|
//! Shared state reading code.
|
||||||
//!
|
//!
|
||||||
//! Used by [`StateService`] and [`ReadStateService`]
|
//! Used by [`StateService`](crate::StateService) and
|
||||||
//! to read from the best [`Chain`] in the [`NonFinalizedState`],
|
//! [`ReadStateService`](crate::ReadStateService) to read from the best
|
||||||
//! and the database in the [`FinalizedState`].
|
//! [`Chain`] in the
|
||||||
|
//! [`NonFinalizedState`](crate::service::non_finalized_state::NonFinalizedState),
|
||||||
|
//! and the database in the
|
||||||
|
//! [`FinalizedState`](crate::service::finalized_state::FinalizedState).
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeMap, BTreeSet, HashSet},
|
collections::{BTreeMap, BTreeSet, HashSet},
|
||||||
|
@ -13,7 +16,9 @@ use std::{
|
||||||
use zebra_chain::{
|
use zebra_chain::{
|
||||||
amount::{self, Amount, NegativeAllowed, NonNegative},
|
amount::{self, Amount, NegativeAllowed, NonNegative},
|
||||||
block::{self, Block, Height},
|
block::{self, Block, Height},
|
||||||
|
orchard,
|
||||||
parameters::Network,
|
parameters::Network,
|
||||||
|
sapling,
|
||||||
transaction::{self, Transaction},
|
transaction::{self, Transaction},
|
||||||
transparent,
|
transparent,
|
||||||
};
|
};
|
||||||
|
@ -44,8 +49,8 @@ const FINALIZED_ADDRESS_INDEX_RETRIES: usize = 3;
|
||||||
pub const ADDRESS_HEIGHTS_FULL_RANGE: RangeInclusive<Height> = Height(1)..=Height::MAX;
|
pub const ADDRESS_HEIGHTS_FULL_RANGE: RangeInclusive<Height> = Height(1)..=Height::MAX;
|
||||||
|
|
||||||
/// Returns the [`Block`] with [`block::Hash`](zebra_chain::block::Hash) or
|
/// Returns the [`Block`] with [`block::Hash`](zebra_chain::block::Hash) or
|
||||||
/// [`Height`](zebra_chain::block::Height),
|
/// [`Height`](zebra_chain::block::Height), if it exists in the non-finalized
|
||||||
/// if it exists in the non-finalized `chain` or finalized `db`.
|
/// `chain` or finalized `db`.
|
||||||
pub(crate) fn block<C>(
|
pub(crate) fn block<C>(
|
||||||
chain: Option<C>,
|
chain: Option<C>,
|
||||||
db: &ZebraDb,
|
db: &ZebraDb,
|
||||||
|
@ -56,12 +61,13 @@ where
|
||||||
{
|
{
|
||||||
// # Correctness
|
// # Correctness
|
||||||
//
|
//
|
||||||
// The StateService commits blocks to the finalized state before updating the latest chain,
|
// The StateService commits blocks to the finalized state before updating
|
||||||
// and it can commit additional blocks after we've cloned this `chain` variable.
|
// 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,
|
// Since blocks are the same in the finalized and non-finalized state, we
|
||||||
// we check the most efficient alternative first.
|
// check the most efficient alternative first. (`chain` is always in memory,
|
||||||
// (`chain` is always in memory, but `db` stores blocks on disk, with a memory cache.)
|
// but `db` stores blocks on disk, with a memory cache.)
|
||||||
chain
|
chain
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|chain| chain.as_ref().block(hash_or_height))
|
.and_then(|chain| chain.as_ref().block(hash_or_height))
|
||||||
|
@ -69,8 +75,8 @@ where
|
||||||
.or_else(|| db.block(hash_or_height))
|
.or_else(|| db.block(hash_or_height))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the [`Transaction`] with [`transaction::Hash`],
|
/// Returns the [`Transaction`] with [`transaction::Hash`], if it exists in the
|
||||||
/// if it exists in the non-finalized `chain` or finalized `db`.
|
/// non-finalized `chain` or finalized `db`.
|
||||||
pub(crate) fn transaction<C>(
|
pub(crate) fn transaction<C>(
|
||||||
chain: Option<C>,
|
chain: Option<C>,
|
||||||
db: &ZebraDb,
|
db: &ZebraDb,
|
||||||
|
@ -81,12 +87,13 @@ where
|
||||||
{
|
{
|
||||||
// # Correctness
|
// # Correctness
|
||||||
//
|
//
|
||||||
// The StateService commits blocks to the finalized state before updating the latest chain,
|
// The StateService commits blocks to the finalized state before updating
|
||||||
// and it can commit additional blocks after we've cloned this `chain` variable.
|
// 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,
|
// Since transactions are the same in the finalized and non-finalized state,
|
||||||
// we check the most efficient alternative first.
|
// we check the most efficient alternative first. (`chain` is always in
|
||||||
// (`chain` is always in memory, but `db` stores transactions on disk, with a memory cache.)
|
// memory, but `db` stores transactions on disk, with a memory cache.)
|
||||||
chain
|
chain
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|chain| {
|
.and_then(|chain| {
|
||||||
|
@ -98,6 +105,60 @@ where
|
||||||
.or_else(|| db.transaction(hash))
|
.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.
|
/// 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.
|
/// If the addresses do not exist in the non-finalized `chain` or finalized `db`, returns zero.
|
||||||
|
|
Loading…
Reference in New Issue