Merge remote-tracking branch 'origin/main' into zcb-for-zebra-scan
This commit is contained in:
commit
25cca748b4
|
@ -6281,6 +6281,7 @@ dependencies = [
|
|||
"tower",
|
||||
"tracing",
|
||||
"zcash_address",
|
||||
"zcash_primitives 0.13.0",
|
||||
"zebra-chain",
|
||||
"zebra-consensus",
|
||||
"zebra-network",
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
- [Shielded Scanning gRPC Server](user/shielded-scan-grpc-server.md)
|
||||
- [Kibana blockchain explorer](user/elasticsearch.md)
|
||||
- [Forking the Zcash Testnet with Zebra](user/fork-zebra-testnet.md)
|
||||
- [Regtest with Zebra](user/regtest.md)
|
||||
- [OpenAPI specification](user/openapi.md)
|
||||
- [Troubleshooting](user/troubleshooting.md)
|
||||
- [Developer Documentation](dev.md)
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
# Regtest with Zebra
|
||||
|
||||
The Regtest network in Zebra enables testing of custom functionalities in a private testnet environment with configurable network upgrade activation heights. It allows for starting an isolated node which won't connect to any peers and currently allows for committing blocks without validating their Proof of Work (in the future, it may use a very low target difficulty and easier Equihash parameters instead of skipping Proof of Work validation altogether).
|
||||
|
||||
Zebra always activates the Canopy network upgrade at block height 1 due to limitations on its block construction.
|
||||
|
||||
In order to use Regtest, Zebra must be configured to run on the Regtest network. The `[mining]` section is also necessary for mining blocks, and the `[rpc]` section is necessary for using the `send_raw_transaction` RPC method to mine non-coinbase transactions onto the chain.
|
||||
|
||||
Relevant parts of the configuration file:
|
||||
|
||||
```toml
|
||||
[mining]
|
||||
miner_address = 't27eWDgjFYJGVXmzrXeVjnb5J3uXDM9xH9v'
|
||||
|
||||
[network]
|
||||
network = "Regtest"
|
||||
|
||||
# This section may be omitted when testing only Canopy
|
||||
[network.testnet_parameters.activation_heights]
|
||||
# Configured activation heights must be greater than or equal to 1,
|
||||
# block height 0 is reserved for the Genesis network upgrade in Zebra
|
||||
NU5 = 1
|
||||
|
||||
# This section may be omitted if a persistent Regtest chain state is desired
|
||||
[state]
|
||||
ephemeral = true
|
||||
|
||||
# This section may be omitted if it's not necessary to send transactions to Zebra's mempool
|
||||
[rpc]
|
||||
listen_addr = "0.0.0.0:18232"
|
||||
```
|
||||
|
||||
Zebra should now include the Regtest network name in its logs, for example:
|
||||
|
||||
```console
|
||||
2024-05-15T21:33:57.044156Z INFO {zebrad="01633af" net="Regtest"}: zebrad::commands::start: initializing mempool
|
||||
```
|
||||
|
||||
There are two ways to commit blocks to Zebra's state on Regtest:
|
||||
- Using the `getblocktemplate` and `submitblock` RPC methods directly
|
||||
- Using Zebra's experimental `internal-miner` feature
|
||||
|
||||
## Using Zebra's Internal Miner
|
||||
|
||||
Zebra can mine blocks on the Regtest network when compiled with the experimental `internal-miner` compilation feature and configured to enable to internal miner.
|
||||
|
||||
Add `internal_miner = true` in the mining section of its configuration and compile Zebra with `cargo build --features "internal-miner"` (or `cargo run --features "internal-miner"` to compile and start Zebra) to use the internal miner with Regtest:
|
||||
|
||||
```toml
|
||||
[mining]
|
||||
internal_miner = true
|
||||
```
|
||||
|
||||
Zebra should now mine blocks on Regtest when it starts after a short delay (of around 30 seconds).
|
||||
|
||||
To confirm that it's working, look for `successfully mined a new block` messages in the logs, or that the tip height is increasing.
|
||||
|
||||
## Using RPC methods directly
|
||||
|
||||
Blocks could also be mined outside of Zebra and submitted via Zebra's RPC methods. This requires enabling the RPC server in the configuration by providing a `listen_addr` field:
|
||||
|
||||
```toml
|
||||
[rpc]
|
||||
listen_addr = "0.0.0.0:18232"
|
||||
```
|
||||
|
||||
With Proof of Work disabled on Regtest, block templates can be converted directly into blocks with the `proposal_block_from_template()` function in the `zebra-chain` crate, serialized, hex-encoded, and then submitted via the `submitblock` RPC method.
|
||||
|
||||
The `submitblock` RPC method should return `{ "result": null }` for successful block submissions.
|
||||
|
||||
For example:
|
||||
|
||||
```rust
|
||||
let client = RpcRequestClient::new(rpc_address);
|
||||
|
||||
let block_template: GetBlockTemplate = client
|
||||
.json_result_from_call("getblocktemplate", "[]".to_string())
|
||||
.await
|
||||
.expect("response should be success output with a serialized `GetBlockTemplate`");
|
||||
|
||||
let network_upgrade = if block_template.height < NU5_ACTIVATION_HEIGHT {
|
||||
NetworkUpgrade::Canopy
|
||||
} else {
|
||||
NetworkUpgrade::Nu5
|
||||
};
|
||||
|
||||
let block_data = hex::encode(
|
||||
proposal_block_from_template(&block_template, TimeSource::default(), network_upgrade)?
|
||||
.zcash_serialize_to_vec()?,
|
||||
);
|
||||
|
||||
let submit_block_response = client
|
||||
.text_from_call("submitblock", format!(r#"["{block_data}"]"#))
|
||||
.await?;
|
||||
|
||||
let was_submission_successful = submit_block_response.contains(r#""result":null"#);
|
||||
```
|
||||
|
||||
See the `regtest_submit_blocks()` acceptance test as a more detailed example for using Zebra's RPC methods to submit blocks on Regtest.
|
||||
|
||||
When Proof of Work validation is enabled for Regtest with a low target difficulty and easy Equihash parameters, Zebra may have a `network.testnet_parameters.disable_pow` field in its configuration so that this would continue working.
|
|
@ -15,7 +15,6 @@ use std::{
|
|||
fmt,
|
||||
hash::{Hash, Hasher},
|
||||
io,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use bitvec::prelude::*;
|
||||
|
@ -25,7 +24,7 @@ use hex::ToHex;
|
|||
use incrementalmerkletree::Hashable;
|
||||
use lazy_static::lazy_static;
|
||||
use thiserror::Error;
|
||||
use zcash_primitives::merkle_tree::{write_commitment_tree, HashSer};
|
||||
use zcash_primitives::merkle_tree::HashSer;
|
||||
|
||||
use super::sinsemilla::*;
|
||||
|
||||
|
@ -243,7 +242,7 @@ impl ToHex for Node {
|
|||
}
|
||||
}
|
||||
|
||||
/// Required to convert [`NoteCommitmentTree`] into [`SerializedTree`].
|
||||
/// Required to serialize [`NoteCommitmentTree`]s in a format compatible with `zcashd`.
|
||||
///
|
||||
/// Zebra stores Orchard note commitment trees as [`Frontier`][1]s while the
|
||||
/// [`z_gettreestate`][2] RPC requires [`CommitmentTree`][3]s. Implementing
|
||||
|
@ -633,7 +632,21 @@ impl NoteCommitmentTree {
|
|||
assert_eq!(self.inner, other.inner);
|
||||
|
||||
// Check the RPC serialization format (not the same as the Zebra database format)
|
||||
assert_eq!(SerializedTree::from(self), SerializedTree::from(other));
|
||||
assert_eq!(self.to_rpc_bytes(), other.to_rpc_bytes());
|
||||
}
|
||||
|
||||
/// Serializes [`Self`] to a format compatible with `zcashd`'s RPCs.
|
||||
pub fn to_rpc_bytes(&self) -> Vec<u8> {
|
||||
// Convert the tree from [`Frontier`](bridgetree::Frontier) to
|
||||
// [`CommitmentTree`](merkle_tree::CommitmentTree).
|
||||
let tree = incrementalmerkletree::frontier::CommitmentTree::from_frontier(&self.inner);
|
||||
|
||||
let mut rpc_bytes = vec![];
|
||||
|
||||
zcash_primitives::merkle_tree::write_commitment_tree(&tree, &mut rpc_bytes)
|
||||
.expect("serializable tree");
|
||||
|
||||
rpc_bytes
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -688,68 +701,3 @@ impl From<Vec<pallas::Base>> for NoteCommitmentTree {
|
|||
tree
|
||||
}
|
||||
}
|
||||
|
||||
/// A serialized Orchard note commitment tree.
|
||||
///
|
||||
/// The format of the serialized data is compatible with
|
||||
/// [`CommitmentTree`](incrementalmerkletree::frontier::CommitmentTree) from `librustzcash` and not
|
||||
/// with [`Frontier`](bridgetree::Frontier) from the crate
|
||||
/// [`incrementalmerkletree`]. Zebra follows the former format in order to stay
|
||||
/// consistent with `zcashd` in RPCs. Note that [`NoteCommitmentTree`] itself is
|
||||
/// represented as [`Frontier`](bridgetree::Frontier).
|
||||
///
|
||||
/// The formats are semantically equivalent. The primary difference between them
|
||||
/// is that in [`Frontier`](bridgetree::Frontier), the vector of parents is
|
||||
/// dense (we know where the gaps are from the position of the leaf in the
|
||||
/// overall tree); whereas in [`CommitmentTree`](incrementalmerkletree::frontier::CommitmentTree),
|
||||
/// the vector of parent hashes is sparse with [`None`] values in the gaps.
|
||||
///
|
||||
/// The sparse format, used in this implementation, allows representing invalid
|
||||
/// commitment trees while the dense format allows representing only valid
|
||||
/// commitment trees.
|
||||
///
|
||||
/// It is likely that the dense format will be used in future RPCs, in which
|
||||
/// case the current implementation will have to change and use the format
|
||||
/// compatible with [`Frontier`](bridgetree::Frontier) instead.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize)]
|
||||
pub struct SerializedTree(Vec<u8>);
|
||||
|
||||
impl From<&NoteCommitmentTree> for SerializedTree {
|
||||
fn from(tree: &NoteCommitmentTree) -> Self {
|
||||
let mut serialized_tree = vec![];
|
||||
|
||||
// Skip the serialization of empty trees.
|
||||
//
|
||||
// Note: This ensures compatibility with `zcashd` in the
|
||||
// [`z_gettreestate`][1] RPC.
|
||||
//
|
||||
// [1]: https://zcash.github.io/rpc/z_gettreestate.html
|
||||
if tree.inner == bridgetree::Frontier::empty() {
|
||||
return Self(serialized_tree);
|
||||
}
|
||||
|
||||
// Convert the note commitment tree from
|
||||
// [`Frontier`](bridgetree::Frontier) to
|
||||
// [`CommitmentTree`](merkle_tree::CommitmentTree).
|
||||
let tree = incrementalmerkletree::frontier::CommitmentTree::from_frontier(&tree.inner);
|
||||
|
||||
write_commitment_tree(&tree, &mut serialized_tree)
|
||||
.expect("note commitment tree should be serializable");
|
||||
Self(serialized_tree)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<Arc<NoteCommitmentTree>>> for SerializedTree {
|
||||
fn from(maybe_tree: Option<Arc<NoteCommitmentTree>>) -> Self {
|
||||
match maybe_tree {
|
||||
Some(tree) => tree.as_ref().into(),
|
||||
None => Self(Vec::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for SerializedTree {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ use std::{
|
|||
fmt,
|
||||
hash::{Hash, Hasher},
|
||||
io,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use bitvec::prelude::*;
|
||||
|
@ -25,7 +24,6 @@ use incrementalmerkletree::{frontier::Frontier, Hashable};
|
|||
|
||||
use lazy_static::lazy_static;
|
||||
use thiserror::Error;
|
||||
use zcash_encoding::{Optional, Vector};
|
||||
use zcash_primitives::merkle_tree::HashSer;
|
||||
|
||||
use super::commitment::pedersen_hashes::pedersen_hash;
|
||||
|
@ -38,7 +36,7 @@ use crate::{
|
|||
};
|
||||
|
||||
pub mod legacy;
|
||||
use legacy::{LegacyLeaf, LegacyNoteCommitmentTree};
|
||||
use legacy::LegacyNoteCommitmentTree;
|
||||
|
||||
/// The type that is used to update the note commitment tree.
|
||||
///
|
||||
|
@ -219,7 +217,7 @@ impl ToHex for Node {
|
|||
}
|
||||
}
|
||||
|
||||
/// Required to convert [`NoteCommitmentTree`] into [`SerializedTree`].
|
||||
/// Required to serialize [`NoteCommitmentTree`]s in a format matching `zcashd`.
|
||||
///
|
||||
/// Zebra stores Sapling note commitment trees as [`Frontier`]s while the
|
||||
/// [`z_gettreestate`][1] RPC requires [`CommitmentTree`][2]s. Implementing
|
||||
|
@ -614,7 +612,21 @@ impl NoteCommitmentTree {
|
|||
assert_eq!(self.inner, other.inner);
|
||||
|
||||
// Check the RPC serialization format (not the same as the Zebra database format)
|
||||
assert_eq!(SerializedTree::from(self), SerializedTree::from(other));
|
||||
assert_eq!(self.to_rpc_bytes(), other.to_rpc_bytes());
|
||||
}
|
||||
|
||||
/// Serializes [`Self`] to a format matching `zcashd`'s RPCs.
|
||||
pub fn to_rpc_bytes(&self) -> Vec<u8> {
|
||||
// Convert the tree from [`Frontier`](bridgetree::Frontier) to
|
||||
// [`CommitmentTree`](merkle_tree::CommitmentTree).
|
||||
let tree = incrementalmerkletree::frontier::CommitmentTree::from_frontier(&self.inner);
|
||||
|
||||
let mut rpc_bytes = vec![];
|
||||
|
||||
zcash_primitives::merkle_tree::write_commitment_tree(&tree, &mut rpc_bytes)
|
||||
.expect("serializable tree");
|
||||
|
||||
rpc_bytes
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -670,135 +682,3 @@ impl From<Vec<jubjub::Fq>> for NoteCommitmentTree {
|
|||
tree
|
||||
}
|
||||
}
|
||||
|
||||
/// A serialized Sapling note commitment tree.
|
||||
///
|
||||
/// The format of the serialized data is compatible with
|
||||
/// [`CommitmentTree`](incrementalmerkletree::frontier::CommitmentTree) from `librustzcash` and not
|
||||
/// with [`Frontier`] from the crate
|
||||
/// [`incrementalmerkletree`]. Zebra follows the former format in order to stay
|
||||
/// consistent with `zcashd` in RPCs. Note that [`NoteCommitmentTree`] itself is
|
||||
/// represented as [`Frontier`].
|
||||
///
|
||||
/// The formats are semantically equivalent. The primary difference between them
|
||||
/// is that in [`Frontier`], the vector of parents is
|
||||
/// dense (we know where the gaps are from the position of the leaf in the
|
||||
/// overall tree); whereas in [`CommitmentTree`](incrementalmerkletree::frontier::CommitmentTree),
|
||||
/// the vector of parent hashes is sparse with [`None`] values in the gaps.
|
||||
///
|
||||
/// The sparse format, used in this implementation, allows representing invalid
|
||||
/// commitment trees while the dense format allows representing only valid
|
||||
/// commitment trees.
|
||||
///
|
||||
/// It is likely that the dense format will be used in future RPCs, in which
|
||||
/// case the current implementation will have to change and use the format
|
||||
/// compatible with [`Frontier`] instead.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize)]
|
||||
pub struct SerializedTree(Vec<u8>);
|
||||
|
||||
impl From<&NoteCommitmentTree> for SerializedTree {
|
||||
fn from(tree: &NoteCommitmentTree) -> Self {
|
||||
let mut serialized_tree = vec![];
|
||||
|
||||
//
|
||||
let legacy_tree = LegacyNoteCommitmentTree::from(tree.clone());
|
||||
|
||||
// Convert the note commitment tree represented as a frontier into the
|
||||
// format compatible with `zcashd`.
|
||||
//
|
||||
// `librustzcash` has a function [`from_frontier()`][1], which returns a
|
||||
// commitment tree in the sparse format. However, the returned tree
|
||||
// always contains [`MERKLE_DEPTH`] parent nodes, even though some
|
||||
// trailing parents are empty. Such trees are incompatible with Sapling
|
||||
// commitment trees returned by `zcashd` because `zcashd` returns
|
||||
// Sapling commitment trees without empty trailing parents. For this
|
||||
// reason, Zebra implements its own conversion between the dense and
|
||||
// sparse formats for Sapling.
|
||||
//
|
||||
// [1]: <https://github.com/zcash/librustzcash/blob/a63a37a/zcash_primitives/src/merkle_tree.rs#L125>
|
||||
if let Some(frontier) = legacy_tree.inner.frontier {
|
||||
let (left_leaf, right_leaf) = match frontier.leaf {
|
||||
LegacyLeaf::Left(left_value) => (Some(left_value), None),
|
||||
LegacyLeaf::Right(left_value, right_value) => (Some(left_value), Some(right_value)),
|
||||
};
|
||||
|
||||
// Ommers are siblings of parent nodes along the branch from the
|
||||
// most recent leaf to the root of the tree.
|
||||
let mut ommers_iter = frontier.ommers.iter();
|
||||
|
||||
// Set bits in the binary representation of the position indicate
|
||||
// the presence of ommers along the branch from the most recent leaf
|
||||
// node to the root of the tree, except for the lowest bit.
|
||||
let mut position: u64 = (frontier.position.0)
|
||||
.try_into()
|
||||
.expect("old usize position always fit in u64");
|
||||
|
||||
// The lowest bit does not indicate the presence of any ommers. We
|
||||
// clear it so that we can test if there are no set bits left in
|
||||
// [`position`].
|
||||
position &= !1;
|
||||
|
||||
// Run through the bits of [`position`], and push an ommer for each
|
||||
// set bit, or `None` otherwise. In contrast to the 'zcashd' code
|
||||
// linked above, we want to skip any trailing `None` parents at the
|
||||
// top of the tree. To do that, we clear the bits as we go through
|
||||
// them, and break early if the remaining bits are all zero (i.e.
|
||||
// [`position`] is zero).
|
||||
let mut parents = vec![];
|
||||
for i in 1..MERKLE_DEPTH {
|
||||
// Test each bit in [`position`] individually. Don't test the
|
||||
// lowest bit since it doesn't actually indicate the position of
|
||||
// any ommer.
|
||||
let bit_mask = 1 << i;
|
||||
|
||||
if position & bit_mask == 0 {
|
||||
parents.push(None);
|
||||
} else {
|
||||
parents.push(ommers_iter.next());
|
||||
// Clear the set bit so that we can test if there are no set
|
||||
// bits left.
|
||||
position &= !bit_mask;
|
||||
// If there are no set bits left, exit early so that there
|
||||
// are no empty trailing parent nodes in the serialized
|
||||
// tree.
|
||||
if position == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize the converted note commitment tree.
|
||||
Optional::write(&mut serialized_tree, left_leaf, |tree, leaf| {
|
||||
leaf.write(tree)
|
||||
})
|
||||
.expect("A leaf in a note commitment tree should be serializable");
|
||||
|
||||
Optional::write(&mut serialized_tree, right_leaf, |tree, leaf| {
|
||||
leaf.write(tree)
|
||||
})
|
||||
.expect("A leaf in a note commitment tree should be serializable");
|
||||
|
||||
Vector::write(&mut serialized_tree, &parents, |tree, parent| {
|
||||
Optional::write(tree, *parent, |tree, parent| parent.write(tree))
|
||||
})
|
||||
.expect("Parent nodes in a note commitment tree should be serializable");
|
||||
}
|
||||
|
||||
Self(serialized_tree)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<Arc<NoteCommitmentTree>>> for SerializedTree {
|
||||
fn from(maybe_tree: Option<Arc<NoteCommitmentTree>>) -> Self {
|
||||
match maybe_tree {
|
||||
Some(tree) => tree.as_ref().into(),
|
||||
None => Self(vec![]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for SerializedTree {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,6 +64,8 @@ tracing = "0.1.39"
|
|||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
serde = { version = "1.0.202", features = ["serde_derive"] }
|
||||
|
||||
zcash_primitives = { version = "0.13.0" }
|
||||
|
||||
# Experimental feature getblocktemplate-rpcs
|
||||
rand = { version = "0.8.5", optional = true }
|
||||
# ECC deps used by getblocktemplate-rpcs feature
|
||||
|
|
|
@ -18,13 +18,12 @@ use tokio::{sync::broadcast, task::JoinHandle};
|
|||
use tower::{Service, ServiceExt};
|
||||
use tracing::Instrument;
|
||||
|
||||
use zcash_primitives::consensus::Parameters;
|
||||
use zebra_chain::{
|
||||
block::{self, Height, SerializedBlock},
|
||||
chain_tip::ChainTip,
|
||||
orchard,
|
||||
parameters::{ConsensusBranchId, Network, NetworkUpgrade},
|
||||
sapling,
|
||||
serialization::{SerializationError, ZcashDeserialize},
|
||||
serialization::ZcashDeserialize,
|
||||
subtree::NoteCommitmentSubtreeIndex,
|
||||
transaction::{self, SerializedTransaction, Transaction, UnminedTx},
|
||||
transparent::{self, Address},
|
||||
|
@ -34,7 +33,7 @@ use zebra_state::{HashOrHeight, MinedTx, OutputIndex, OutputLocation, Transactio
|
|||
|
||||
use crate::{
|
||||
constants::{INVALID_PARAMETERS_ERROR_CODE, MISSING_BLOCK_ERROR_CODE},
|
||||
methods::trees::{GetSubtrees, SubtreeRpcData},
|
||||
methods::trees::{GetSubtrees, GetTreestate, SubtreeRpcData},
|
||||
queue::Queue,
|
||||
};
|
||||
|
||||
|
@ -504,30 +503,18 @@ where
|
|||
let (tip_height, tip_hash) = self
|
||||
.latest_chain_tip
|
||||
.best_tip_height_and_hash()
|
||||
.ok_or_else(|| Error {
|
||||
code: ErrorCode::ServerError(0),
|
||||
message: "No Chain tip available yet".to_string(),
|
||||
data: None,
|
||||
})?;
|
||||
.ok_or_server_error("No Chain tip available yet")?;
|
||||
|
||||
// `estimated_height` field
|
||||
let current_block_time =
|
||||
self.latest_chain_tip
|
||||
.best_tip_block_time()
|
||||
.ok_or_else(|| Error {
|
||||
code: ErrorCode::ServerError(0),
|
||||
message: "No Chain tip available yet".to_string(),
|
||||
data: None,
|
||||
})?;
|
||||
let current_block_time = self
|
||||
.latest_chain_tip
|
||||
.best_tip_block_time()
|
||||
.ok_or_server_error("No Chain tip available yet")?;
|
||||
|
||||
let zebra_estimated_height = self
|
||||
.latest_chain_tip
|
||||
.estimate_network_chain_tip_height(network, Utc::now())
|
||||
.ok_or_else(|| Error {
|
||||
code: ErrorCode::ServerError(0),
|
||||
message: "No Chain tip available yet".to_string(),
|
||||
data: None,
|
||||
})?;
|
||||
.ok_or_server_error("No Chain tip available yet")?;
|
||||
|
||||
let mut estimated_height =
|
||||
if current_block_time > Utc::now() || zebra_estimated_height < tip_height {
|
||||
|
@ -606,11 +593,7 @@ where
|
|||
let valid_addresses = address_strings.valid_addresses()?;
|
||||
|
||||
let request = zebra_state::ReadRequest::AddressBalance(valid_addresses);
|
||||
let response = state.oneshot(request).await.map_err(|error| Error {
|
||||
code: ErrorCode::ServerError(0),
|
||||
message: error.to_string(),
|
||||
data: None,
|
||||
})?;
|
||||
let response = state.oneshot(request).await.map_server_error()?;
|
||||
|
||||
match response {
|
||||
zebra_state::ReadResponse::AddressBalance(balance) => Ok(AddressBalance {
|
||||
|
@ -647,11 +630,7 @@ where
|
|||
let transaction_parameter = mempool::Gossip::Tx(raw_transaction.into());
|
||||
let request = mempool::Request::Queue(vec![transaction_parameter]);
|
||||
|
||||
let response = mempool.oneshot(request).await.map_err(|error| Error {
|
||||
code: ErrorCode::ServerError(0),
|
||||
message: error.to_string(),
|
||||
data: None,
|
||||
})?;
|
||||
let response = mempool.oneshot(request).await.map_server_error()?;
|
||||
|
||||
let queue_results = match response {
|
||||
mempool::Response::Queued(results) => results,
|
||||
|
@ -666,14 +645,10 @@ where
|
|||
|
||||
tracing::debug!("sent transaction to mempool: {:?}", &queue_results[0]);
|
||||
|
||||
match &queue_results[0] {
|
||||
Ok(()) => Ok(SentTransactionHash(transaction_hash)),
|
||||
Err(error) => Err(Error {
|
||||
code: ErrorCode::ServerError(0),
|
||||
message: error.to_string(),
|
||||
data: None,
|
||||
}),
|
||||
}
|
||||
queue_results[0]
|
||||
.as_ref()
|
||||
.map(|_| SentTransactionHash(transaction_hash))
|
||||
.map_server_error()
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
@ -681,7 +656,6 @@ where
|
|||
// TODO:
|
||||
// - use `height_from_signed_int()` to handle negative heights
|
||||
// (this might be better in the state request, because it needs the state height)
|
||||
// - create a function that handles block hashes or heights, and use it in `z_get_treestate()`
|
||||
fn get_block(
|
||||
&self,
|
||||
hash_or_height: String,
|
||||
|
@ -694,14 +668,7 @@ where
|
|||
let verbosity = verbosity.unwrap_or(DEFAULT_GETBLOCK_VERBOSITY);
|
||||
|
||||
async move {
|
||||
let hash_or_height: HashOrHeight =
|
||||
hash_or_height
|
||||
.parse()
|
||||
.map_err(|error: SerializationError| Error {
|
||||
code: ErrorCode::ServerError(0),
|
||||
message: error.to_string(),
|
||||
data: None,
|
||||
})?;
|
||||
let hash_or_height: HashOrHeight = hash_or_height.parse().map_server_error()?;
|
||||
|
||||
if verbosity == 0 {
|
||||
// # Performance
|
||||
|
@ -713,11 +680,7 @@ where
|
|||
.ready()
|
||||
.and_then(|service| service.call(request))
|
||||
.await
|
||||
.map_err(|error| Error {
|
||||
code: ErrorCode::ServerError(0),
|
||||
message: error.to_string(),
|
||||
data: None,
|
||||
})?;
|
||||
.map_server_error()?;
|
||||
|
||||
match response {
|
||||
zebra_state::ReadResponse::Block(Some(block)) => {
|
||||
|
@ -761,11 +724,7 @@ where
|
|||
.ready()
|
||||
.and_then(|service| service.call(request))
|
||||
.await
|
||||
.map_err(|error| Error {
|
||||
code: ErrorCode::ServerError(0),
|
||||
message: error.to_string(),
|
||||
data: None,
|
||||
})?;
|
||||
.map_server_error()?;
|
||||
|
||||
match response {
|
||||
zebra_state::ReadResponse::BlockHash(Some(hash)) => hash,
|
||||
|
@ -913,11 +872,7 @@ where
|
|||
self.latest_chain_tip
|
||||
.best_tip_hash()
|
||||
.map(GetBlockHash)
|
||||
.ok_or(Error {
|
||||
code: ErrorCode::ServerError(0),
|
||||
message: "No blocks in state".to_string(),
|
||||
data: None,
|
||||
})
|
||||
.ok_or_server_error("No blocks in state")
|
||||
}
|
||||
|
||||
// TODO: use a generic error constructor (#5548)
|
||||
|
@ -947,11 +902,7 @@ where
|
|||
.ready()
|
||||
.and_then(|service| service.call(request))
|
||||
.await
|
||||
.map_err(|error| Error {
|
||||
code: ErrorCode::ServerError(0),
|
||||
message: error.to_string(),
|
||||
data: None,
|
||||
})?;
|
||||
.map_server_error()?;
|
||||
|
||||
match response {
|
||||
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||
|
@ -1030,11 +981,7 @@ where
|
|||
.ready()
|
||||
.and_then(|service| service.call(request))
|
||||
.await
|
||||
.map_err(|error| Error {
|
||||
code: ErrorCode::ServerError(0),
|
||||
message: error.to_string(),
|
||||
data: None,
|
||||
})?;
|
||||
.map_server_error()?;
|
||||
|
||||
match response {
|
||||
mempool::Response::Transactions(unmined_transactions) => {
|
||||
|
@ -1052,11 +999,7 @@ where
|
|||
.ready()
|
||||
.and_then(|service| service.call(request))
|
||||
.await
|
||||
.map_err(|error| Error {
|
||||
code: ErrorCode::ServerError(0),
|
||||
message: error.to_string(),
|
||||
data: None,
|
||||
})?;
|
||||
.map_server_error()?;
|
||||
|
||||
match response {
|
||||
zebra_state::ReadResponse::Transaction(Some(MinedTx {
|
||||
|
@ -1069,11 +1012,9 @@ where
|
|||
confirmations,
|
||||
verbose,
|
||||
)),
|
||||
zebra_state::ReadResponse::Transaction(None) => Err(Error {
|
||||
code: ErrorCode::ServerError(0),
|
||||
message: "Transaction not found".to_string(),
|
||||
data: None,
|
||||
}),
|
||||
zebra_state::ReadResponse::Transaction(None) => {
|
||||
Err("Transaction not found").map_server_error()
|
||||
}
|
||||
_ => unreachable!("unmatched response to a transaction request"),
|
||||
}
|
||||
}
|
||||
|
@ -1081,47 +1022,30 @@ where
|
|||
}
|
||||
|
||||
// TODO:
|
||||
// - use a generic error constructor (#5548)
|
||||
// - use `height_from_signed_int()` to handle negative heights
|
||||
// (this might be better in the state request, because it needs the state height)
|
||||
// - create a function that handles block hashes or heights, and use it in `get_block()`
|
||||
fn z_get_treestate(&self, hash_or_height: String) -> BoxFuture<Result<GetTreestate>> {
|
||||
let mut state = self.state.clone();
|
||||
let network = self.network.clone();
|
||||
|
||||
async move {
|
||||
// Convert the [`hash_or_height`] string into an actual hash or height.
|
||||
let hash_or_height = hash_or_height
|
||||
.parse()
|
||||
.map_err(|error: SerializationError| Error {
|
||||
code: ErrorCode::ServerError(0),
|
||||
message: error.to_string(),
|
||||
data: None,
|
||||
})?;
|
||||
|
||||
// # Concurrency
|
||||
//
|
||||
// For consistency, this lookup must be performed first, then all the other
|
||||
// lookups must be based on the hash.
|
||||
let hash_or_height = hash_or_height.parse().map_server_error()?;
|
||||
|
||||
// Fetch the block referenced by [`hash_or_height`] from the state.
|
||||
// TODO: If this RPC is called a lot, just get the block header,
|
||||
// rather than the whole block.
|
||||
let block_request = zebra_state::ReadRequest::Block(hash_or_height);
|
||||
let block_response = state
|
||||
//
|
||||
// # Concurrency
|
||||
//
|
||||
// For consistency, this lookup must be performed first, then all the other lookups must
|
||||
// be based on the hash.
|
||||
//
|
||||
// TODO: If this RPC is called a lot, just get the block header, rather than the whole block.
|
||||
let block = match state
|
||||
.ready()
|
||||
.and_then(|service| service.call(block_request))
|
||||
.and_then(|service| service.call(zebra_state::ReadRequest::Block(hash_or_height)))
|
||||
.await
|
||||
.map_err(|error| Error {
|
||||
code: ErrorCode::ServerError(0),
|
||||
message: error.to_string(),
|
||||
data: None,
|
||||
})?;
|
||||
|
||||
// The block hash, height, and time are all required fields in the
|
||||
// RPC response. For this reason, we throw an error early if the
|
||||
// state didn't return the requested block so that we prevent
|
||||
// further state queries.
|
||||
let block = match block_response {
|
||||
.map_server_error()?
|
||||
{
|
||||
zebra_state::ReadResponse::Block(Some(block)) => block,
|
||||
zebra_state::ReadResponse::Block(None) => {
|
||||
return Err(Error {
|
||||
|
@ -1133,73 +1057,54 @@ where
|
|||
_ => unreachable!("unmatched response to a block request"),
|
||||
};
|
||||
|
||||
let hash = hash_or_height.hash().unwrap_or_else(|| block.hash());
|
||||
let hash_or_height = hash.into();
|
||||
let hash = hash_or_height
|
||||
.hash_or_else(|_| Some(block.hash()))
|
||||
.expect("block hash");
|
||||
|
||||
// Fetch the Sapling & Orchard treestates referenced by
|
||||
// [`hash_or_height`] from the state.
|
||||
|
||||
let sapling_request = zebra_state::ReadRequest::SaplingTree(hash_or_height);
|
||||
let sapling_response = state
|
||||
.ready()
|
||||
.and_then(|service| service.call(sapling_request))
|
||||
.await
|
||||
.map_err(|error| Error {
|
||||
code: ErrorCode::ServerError(0),
|
||||
message: error.to_string(),
|
||||
data: None,
|
||||
})?;
|
||||
|
||||
let orchard_request = zebra_state::ReadRequest::OrchardTree(hash_or_height);
|
||||
let orchard_response = state
|
||||
.ready()
|
||||
.and_then(|service| service.call(orchard_request))
|
||||
.await
|
||||
.map_err(|error| Error {
|
||||
code: ErrorCode::ServerError(0),
|
||||
message: error.to_string(),
|
||||
data: None,
|
||||
})?;
|
||||
|
||||
// We've got all the data we need for the RPC response, so we
|
||||
// assemble the response.
|
||||
|
||||
let height = block
|
||||
.coinbase_height()
|
||||
.expect("verified blocks have a valid height");
|
||||
let height = hash_or_height
|
||||
.height_or_else(|_| block.coinbase_height())
|
||||
.expect("verified blocks have a coinbase height");
|
||||
|
||||
let time = u32::try_from(block.header.time.timestamp())
|
||||
.expect("Timestamps of valid blocks always fit into u32.");
|
||||
|
||||
let sapling_tree = match sapling_response {
|
||||
zebra_state::ReadResponse::SaplingTree(maybe_tree) => {
|
||||
sapling::tree::SerializedTree::from(maybe_tree)
|
||||
let sapling_nu = zcash_primitives::consensus::NetworkUpgrade::Sapling;
|
||||
let sapling = if network.is_nu_active(sapling_nu, height.into()) {
|
||||
match state
|
||||
.ready()
|
||||
.and_then(|service| {
|
||||
service.call(zebra_state::ReadRequest::SaplingTree(hash.into()))
|
||||
})
|
||||
.await
|
||||
.map_server_error()?
|
||||
{
|
||||
zebra_state::ReadResponse::SaplingTree(tree) => tree.map(|t| t.to_rpc_bytes()),
|
||||
_ => unreachable!("unmatched response to a Sapling tree request"),
|
||||
}
|
||||
_ => unreachable!("unmatched response to a sapling tree request"),
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let orchard_tree = match orchard_response {
|
||||
zebra_state::ReadResponse::OrchardTree(maybe_tree) => {
|
||||
orchard::tree::SerializedTree::from(maybe_tree)
|
||||
let orchard_nu = zcash_primitives::consensus::NetworkUpgrade::Nu5;
|
||||
let orchard = if network.is_nu_active(orchard_nu, height.into()) {
|
||||
match state
|
||||
.ready()
|
||||
.and_then(|service| {
|
||||
service.call(zebra_state::ReadRequest::OrchardTree(hash.into()))
|
||||
})
|
||||
.await
|
||||
.map_server_error()?
|
||||
{
|
||||
zebra_state::ReadResponse::OrchardTree(tree) => tree.map(|t| t.to_rpc_bytes()),
|
||||
_ => unreachable!("unmatched response to an Orchard tree request"),
|
||||
}
|
||||
_ => unreachable!("unmatched response to an orchard tree request"),
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(GetTreestate {
|
||||
hash,
|
||||
height,
|
||||
time,
|
||||
sapling: Treestate {
|
||||
commitments: Commitments {
|
||||
final_state: sapling_tree,
|
||||
},
|
||||
},
|
||||
orchard: Treestate {
|
||||
commitments: Commitments {
|
||||
final_state: orchard_tree,
|
||||
},
|
||||
},
|
||||
})
|
||||
Ok(GetTreestate::from_parts(
|
||||
hash, height, time, sapling, orchard,
|
||||
))
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
@ -1221,11 +1126,7 @@ where
|
|||
.ready()
|
||||
.and_then(|service| service.call(request))
|
||||
.await
|
||||
.map_err(|error| Error {
|
||||
code: ErrorCode::ServerError(0),
|
||||
message: error.to_string(),
|
||||
data: None,
|
||||
})?;
|
||||
.map_server_error()?;
|
||||
|
||||
let subtrees = match response {
|
||||
zebra_state::ReadResponse::SaplingSubtrees(subtrees) => subtrees,
|
||||
|
@ -1251,11 +1152,7 @@ where
|
|||
.ready()
|
||||
.and_then(|service| service.call(request))
|
||||
.await
|
||||
.map_err(|error| Error {
|
||||
code: ErrorCode::ServerError(0),
|
||||
message: error.to_string(),
|
||||
data: None,
|
||||
})?;
|
||||
.map_server_error()?;
|
||||
|
||||
let subtrees = match response {
|
||||
zebra_state::ReadResponse::OrchardSubtrees(subtrees) => subtrees,
|
||||
|
@ -1316,11 +1213,7 @@ where
|
|||
.ready()
|
||||
.and_then(|service| service.call(request))
|
||||
.await
|
||||
.map_err(|error| Error {
|
||||
code: ErrorCode::ServerError(0),
|
||||
message: error.to_string(),
|
||||
data: None,
|
||||
})?;
|
||||
.map_server_error()?;
|
||||
|
||||
let hashes = match response {
|
||||
zebra_state::ReadResponse::AddressesTransactionIds(hashes) => {
|
||||
|
@ -1368,11 +1261,7 @@ where
|
|||
.ready()
|
||||
.and_then(|service| service.call(request))
|
||||
.await
|
||||
.map_err(|error| Error {
|
||||
code: ErrorCode::ServerError(0),
|
||||
message: error.to_string(),
|
||||
data: None,
|
||||
})?;
|
||||
.map_server_error()?;
|
||||
let utxos = match response {
|
||||
zebra_state::ReadResponse::AddressUtxos(utxos) => utxos,
|
||||
_ => unreachable!("unmatched response to a UtxosByAddresses request"),
|
||||
|
@ -1422,11 +1311,9 @@ pub fn best_chain_tip_height<Tip>(latest_chain_tip: &Tip) -> Result<Height>
|
|||
where
|
||||
Tip: ChainTip + Clone + Send + Sync + 'static,
|
||||
{
|
||||
latest_chain_tip.best_tip_height().ok_or(Error {
|
||||
code: ErrorCode::ServerError(0),
|
||||
message: "No blocks in state".to_string(),
|
||||
data: None,
|
||||
})
|
||||
latest_chain_tip
|
||||
.best_tip_height()
|
||||
.ok_or_server_error("No blocks in state")
|
||||
}
|
||||
|
||||
/// Response to a `getinfo` RPC request.
|
||||
|
@ -1660,85 +1547,6 @@ impl Default for GetBlockHash {
|
|||
}
|
||||
}
|
||||
|
||||
/// Response to a `z_gettreestate` RPC request.
|
||||
///
|
||||
/// Contains the hex-encoded Sapling & Orchard note commitment trees, and their
|
||||
/// corresponding [`block::Hash`], [`Height`], and block time.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
|
||||
pub struct GetTreestate {
|
||||
/// The block hash corresponding to the treestate, hex-encoded.
|
||||
#[serde(with = "hex")]
|
||||
hash: block::Hash,
|
||||
|
||||
/// The block height corresponding to the treestate, numeric.
|
||||
height: Height,
|
||||
|
||||
/// Unix time when the block corresponding to the treestate was mined,
|
||||
/// numeric.
|
||||
///
|
||||
/// UTC seconds since the Unix 1970-01-01 epoch.
|
||||
time: u32,
|
||||
|
||||
/// A treestate containing a Sapling note commitment tree, hex-encoded.
|
||||
#[serde(skip_serializing_if = "Treestate::is_empty")]
|
||||
sapling: Treestate<sapling::tree::SerializedTree>,
|
||||
|
||||
/// A treestate containing an Orchard note commitment tree, hex-encoded.
|
||||
#[serde(skip_serializing_if = "Treestate::is_empty")]
|
||||
orchard: Treestate<orchard::tree::SerializedTree>,
|
||||
}
|
||||
|
||||
impl Default for GetTreestate {
|
||||
fn default() -> Self {
|
||||
GetTreestate {
|
||||
hash: block::Hash([0; 32]),
|
||||
height: Height(0),
|
||||
time: 0,
|
||||
sapling: Treestate {
|
||||
commitments: Commitments {
|
||||
final_state: sapling::tree::SerializedTree::default(),
|
||||
},
|
||||
},
|
||||
orchard: Treestate {
|
||||
commitments: Commitments {
|
||||
final_state: orchard::tree::SerializedTree::default(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A treestate that is included in the [`z_gettreestate`][1] RPC response.
|
||||
///
|
||||
/// [1]: https://zcash.github.io/rpc/z_gettreestate.html
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
|
||||
struct Treestate<Tree: AsRef<[u8]>> {
|
||||
/// Contains an Orchard or Sapling serialized note commitment tree,
|
||||
/// hex-encoded.
|
||||
commitments: Commitments<Tree>,
|
||||
}
|
||||
|
||||
/// A wrapper that contains either an Orchard or Sapling note commitment tree.
|
||||
///
|
||||
/// Note that in the original [`z_gettreestate`][1] RPC, [`Commitments`] also
|
||||
/// contains the field `finalRoot`. Zebra does *not* use this field.
|
||||
///
|
||||
/// [1]: https://zcash.github.io/rpc/z_gettreestate.html
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
|
||||
struct Commitments<Tree: AsRef<[u8]>> {
|
||||
/// Orchard or Sapling serialized note commitment tree, hex-encoded.
|
||||
#[serde(with = "hex")]
|
||||
#[serde(rename = "finalState")]
|
||||
final_state: Tree,
|
||||
}
|
||||
|
||||
impl<Tree: AsRef<[u8]>> Treestate<Tree> {
|
||||
/// Returns `true` if there's no serialized commitment tree.
|
||||
fn is_empty(&self) -> bool {
|
||||
self.commitments.final_state.as_ref().is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Response to a `getrawtransaction` RPC request.
|
||||
///
|
||||
/// See the notes for the [`Rpc::get_raw_transaction` method].
|
||||
|
|
|
@ -11,9 +11,18 @@ use insta::dynamic_redaction;
|
|||
use tower::buffer::Buffer;
|
||||
|
||||
use zebra_chain::{
|
||||
block::Block, chain_tip::mock::MockChainTip, parameters::Network::Mainnet,
|
||||
serialization::ZcashDeserializeInto, subtree::NoteCommitmentSubtreeData,
|
||||
block::Block,
|
||||
chain_tip::mock::MockChainTip,
|
||||
orchard,
|
||||
parameters::{
|
||||
testnet::{ConfiguredActivationHeights, Parameters},
|
||||
Network::Mainnet,
|
||||
},
|
||||
sapling,
|
||||
serialization::ZcashDeserializeInto,
|
||||
subtree::NoteCommitmentSubtreeData,
|
||||
};
|
||||
use zebra_node_services::BoxError;
|
||||
use zebra_state::{ReadRequest, ReadResponse, MAX_ON_DISK_HEIGHT};
|
||||
use zebra_test::mock_service::MockService;
|
||||
|
||||
|
@ -40,6 +49,110 @@ async fn test_rpc_response_data() {
|
|||
);
|
||||
}
|
||||
|
||||
/// Checks the output of the [`z_get_treestate`] RPC.
|
||||
///
|
||||
/// TODO:
|
||||
/// 1. Check a non-empty Sapling treestate.
|
||||
/// 2. Check an empty Orchard treestate at NU5 activation height.
|
||||
/// 3. Check a non-empty Orchard treestate.
|
||||
///
|
||||
/// To implement the todos above, we need to:
|
||||
///
|
||||
/// 1. Have a block containing Sapling note commitmnets in the state.
|
||||
/// 2. Activate NU5 at a height for which we have a block in the state.
|
||||
/// 3. Have a block containing Orchard note commitments in the state.
|
||||
#[tokio::test]
|
||||
async fn test_z_get_treestate() {
|
||||
let _init_guard = zebra_test::init();
|
||||
const SAPLING_ACTIVATION_HEIGHT: u32 = 2;
|
||||
|
||||
let testnet = Parameters::build()
|
||||
.with_activation_heights(ConfiguredActivationHeights {
|
||||
sapling: Some(SAPLING_ACTIVATION_HEIGHT),
|
||||
// We need to set the NU5 activation height higher than the height of the last block for
|
||||
// this test because we currently have only the first 10 blocks from the public Testnet,
|
||||
// none of which are compatible with NU5 due to the following consensus rule:
|
||||
//
|
||||
// > [NU5 onward] hashBlockCommitments MUST be set to the value of
|
||||
// > hashBlockCommitments for this block, as specified in [ZIP-244].
|
||||
//
|
||||
// Activating NU5 at a lower height and using the 10 blocks causes a failure in
|
||||
// [`zebra_state::populated_state`].
|
||||
nu5: Some(10),
|
||||
..Default::default()
|
||||
})
|
||||
.with_network_name("custom_testnet")
|
||||
.to_network();
|
||||
|
||||
// Initiate the snapshots of the RPC responses.
|
||||
let mut settings = insta::Settings::clone_current();
|
||||
settings.set_snapshot_suffix(network_string(&testnet).to_string());
|
||||
|
||||
let blocks: Vec<_> = testnet
|
||||
.blockchain_iter()
|
||||
.map(|(_, block_bytes)| block_bytes.zcash_deserialize_into().unwrap())
|
||||
.collect();
|
||||
|
||||
let (_, state, tip, _) = zebra_state::populated_state(blocks.clone(), &testnet).await;
|
||||
|
||||
let (rpc, _) = RpcImpl::new(
|
||||
"",
|
||||
"",
|
||||
testnet,
|
||||
false,
|
||||
true,
|
||||
Buffer::new(MockService::build().for_unit_tests::<_, _, BoxError>(), 1),
|
||||
state,
|
||||
tip,
|
||||
);
|
||||
|
||||
// Request the treestate by a hash.
|
||||
let treestate = rpc
|
||||
.z_get_treestate(blocks[0].hash().to_string())
|
||||
.await
|
||||
.expect("genesis treestate = no treestate");
|
||||
settings.bind(|| insta::assert_json_snapshot!("z_get_treestate_by_hash", treestate));
|
||||
|
||||
// Request the treestate by a hash for a block which is not in the state.
|
||||
let treestate = rpc.z_get_treestate(block::Hash([0; 32]).to_string()).await;
|
||||
settings
|
||||
.bind(|| insta::assert_json_snapshot!("z_get_treestate_by_non_existent_hash", treestate));
|
||||
|
||||
// Request the treestate before Sapling activation.
|
||||
let treestate = rpc
|
||||
.z_get_treestate((SAPLING_ACTIVATION_HEIGHT - 1).to_string())
|
||||
.await
|
||||
.expect("no Sapling treestate and no Orchard treestate");
|
||||
settings.bind(|| insta::assert_json_snapshot!("z_get_treestate_no_treestate", treestate));
|
||||
|
||||
// Request the treestate at Sapling activation.
|
||||
let treestate = rpc
|
||||
.z_get_treestate(SAPLING_ACTIVATION_HEIGHT.to_string())
|
||||
.await
|
||||
.expect("empty Sapling treestate and no Orchard treestate");
|
||||
settings.bind(|| {
|
||||
insta::assert_json_snapshot!("z_get_treestate_empty_Sapling_treestate", treestate)
|
||||
});
|
||||
|
||||
// Request the treestate for an invalid height.
|
||||
let treestate = rpc
|
||||
.z_get_treestate(EXCESSIVE_BLOCK_HEIGHT.to_string())
|
||||
.await;
|
||||
settings
|
||||
.bind(|| insta::assert_json_snapshot!("z_get_treestate_excessive_block_height", treestate));
|
||||
|
||||
// Request the treestate for an unparsable hash or height.
|
||||
let treestate = rpc.z_get_treestate("Do you even shield?".to_string()).await;
|
||||
settings.bind(|| {
|
||||
insta::assert_json_snapshot!("z_get_treestate_unparsable_hash_or_height", treestate)
|
||||
});
|
||||
|
||||
// TODO:
|
||||
// 1. Request a non-empty Sapling treestate.
|
||||
// 2. Request an empty Orchard treestate at an NU5 activation height.
|
||||
// 3. Request a non-empty Orchard treestate.
|
||||
}
|
||||
|
||||
async fn test_rpc_response_data_for_network(network: &Network) {
|
||||
// Create a continuous chain of mainnet and testnet blocks from genesis
|
||||
let block_data = network.blockchain_map();
|
||||
|
@ -241,18 +354,6 @@ async fn test_rpc_response_data_for_network(network: &Network) {
|
|||
|
||||
snapshot_rpc_getrawmempool(get_raw_mempool, &settings);
|
||||
|
||||
// `z_gettreestate`
|
||||
let tree_state = rpc
|
||||
.z_get_treestate(BLOCK_HEIGHT.to_string())
|
||||
.await
|
||||
.expect("We should have a GetTreestate struct");
|
||||
snapshot_rpc_z_gettreestate_valid(tree_state, &settings);
|
||||
|
||||
let tree_state = rpc
|
||||
.z_get_treestate(EXCESSIVE_BLOCK_HEIGHT.to_string())
|
||||
.await;
|
||||
snapshot_rpc_z_gettreestate_invalid("excessive_height", tree_state, &settings);
|
||||
|
||||
// `getrawtransaction` verbosity=0
|
||||
//
|
||||
// - similar to `getrawmempool` described above, a mempool request will be made to get the requested
|
||||
|
@ -501,22 +602,6 @@ fn snapshot_rpc_getrawmempool(raw_mempool: Vec<String>, settings: &insta::Settin
|
|||
settings.bind(|| insta::assert_json_snapshot!("get_raw_mempool", raw_mempool));
|
||||
}
|
||||
|
||||
/// Snapshot a valid `z_gettreestate` response, using `cargo insta` and JSON serialization.
|
||||
fn snapshot_rpc_z_gettreestate_valid(tree_state: GetTreestate, settings: &insta::Settings) {
|
||||
settings.bind(|| insta::assert_json_snapshot!(format!("z_get_treestate_valid"), tree_state));
|
||||
}
|
||||
|
||||
/// Snapshot an invalid `z_gettreestate` response, using `cargo insta` and JSON serialization.
|
||||
fn snapshot_rpc_z_gettreestate_invalid(
|
||||
variant: &'static str,
|
||||
tree_state: Result<GetTreestate>,
|
||||
settings: &insta::Settings,
|
||||
) {
|
||||
settings.bind(|| {
|
||||
insta::assert_json_snapshot!(format!("z_get_treestate_invalid_{variant}"), tree_state)
|
||||
});
|
||||
}
|
||||
|
||||
/// Snapshot `getrawtransaction` response, using `cargo insta` and JSON serialization.
|
||||
fn snapshot_rpc_getrawtransaction(
|
||||
variant: &'static str,
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
source: zebra-rpc/src/methods/tests/snapshot.rs
|
||||
expression: treestate
|
||||
---
|
||||
{
|
||||
"hash": "05a60a92d99d85997cce3b87616c089f6124d7342af37106edc76126334a2c38",
|
||||
"height": 0,
|
||||
"time": 1477648033
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
source: zebra-rpc/src/methods/tests/snapshot.rs
|
||||
expression: tree_state
|
||||
expression: treestate
|
||||
---
|
||||
{
|
||||
"Err": {
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
source: zebra-rpc/src/methods/tests/snapshot.rs
|
||||
expression: treestate
|
||||
---
|
||||
{
|
||||
"hash": "00f1a49e54553ac3ef735f2eb1d8247c9a87c22a47dbd7823ae70adcd6c21a18",
|
||||
"height": 2,
|
||||
"time": 1477676169,
|
||||
"sapling": {
|
||||
"commitments": {
|
||||
"finalState": "000000"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
source: zebra-rpc/src/methods/tests/snapshot.rs
|
||||
expression: tree_state
|
||||
expression: treestate
|
||||
---
|
||||
{
|
||||
"Err": {
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
source: zebra-rpc/src/methods/tests/snapshot.rs
|
||||
expression: tree_state
|
||||
expression: treestate
|
||||
---
|
||||
{
|
||||
"hash": "025579869bcf52a989337342f5f57a84f3a28b968f7d6a8307902b065a668d23",
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: zebra-rpc/src/methods/tests/snapshot.rs
|
||||
expression: treestate
|
||||
---
|
||||
{
|
||||
"Err": {
|
||||
"code": 0,
|
||||
"message": "parse error: could not convert the input string to a hash or height"
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
source: zebra-rpc/src/methods/tests/snapshot.rs
|
||||
expression: tree_state
|
||||
---
|
||||
{
|
||||
"hash": "0007bc227e1c57a4a70e237cad00e7b7ce565155ab49166bc57397a26d339283",
|
||||
"height": 1,
|
||||
"time": 1477671596
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
//! Types and functions for note commitment tree RPCs.
|
||||
//
|
||||
// TODO: move the *Tree and *Commitment types into this module.
|
||||
|
||||
use zebra_chain::subtree::{NoteCommitmentSubtreeData, NoteCommitmentSubtreeIndex};
|
||||
use zebra_chain::{
|
||||
block::Hash,
|
||||
block::Height,
|
||||
subtree::{NoteCommitmentSubtreeData, NoteCommitmentSubtreeIndex},
|
||||
};
|
||||
|
||||
/// A subtree data type that can hold Sapling or Orchard subtree roots.
|
||||
pub type SubtreeRpcData = NoteCommitmentSubtreeData<String>;
|
||||
|
@ -29,3 +31,104 @@ pub struct GetSubtrees {
|
|||
//#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub subtrees: Vec<SubtreeRpcData>,
|
||||
}
|
||||
|
||||
/// Response to a `z_gettreestate` RPC request.
|
||||
///
|
||||
/// Contains hex-encoded Sapling & Orchard note commitment trees and their corresponding
|
||||
/// [`struct@Hash`], [`Height`], and block time.
|
||||
///
|
||||
/// The format of the serialized trees represents `CommitmentTree`s from the crate
|
||||
/// `incrementalmerkletree` and not `Frontier`s from the same crate, even though `zebrad`'s
|
||||
/// `NoteCommitmentTree`s are implemented using `Frontier`s. Zebra follows the former format to stay
|
||||
/// consistent with `zcashd`'s RPCs.
|
||||
///
|
||||
/// The formats are semantically equivalent. The difference is that in `Frontier`s, the vector of
|
||||
/// ommers is dense (we know where the gaps are from the position of the leaf in the overall tree);
|
||||
/// whereas in `CommitmentTree`, the vector of ommers is sparse with [`None`] values in the gaps.
|
||||
///
|
||||
/// The dense format might be used in future RPCs.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
|
||||
pub struct GetTreestate {
|
||||
/// The block hash corresponding to the treestate, hex-encoded.
|
||||
#[serde(with = "hex")]
|
||||
hash: Hash,
|
||||
|
||||
/// The block height corresponding to the treestate, numeric.
|
||||
height: Height,
|
||||
|
||||
/// Unix time when the block corresponding to the treestate was mined,
|
||||
/// numeric.
|
||||
///
|
||||
/// UTC seconds since the Unix 1970-01-01 epoch.
|
||||
time: u32,
|
||||
|
||||
/// A treestate containing a Sapling note commitment tree, hex-encoded.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
sapling: Option<Treestate<Vec<u8>>>,
|
||||
|
||||
/// A treestate containing an Orchard note commitment tree, hex-encoded.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
orchard: Option<Treestate<Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl GetTreestate {
|
||||
/// Constructs [`GetTreestate`] from its constituent parts.
|
||||
pub fn from_parts(
|
||||
hash: Hash,
|
||||
height: Height,
|
||||
time: u32,
|
||||
sapling: Option<Vec<u8>>,
|
||||
orchard: Option<Vec<u8>>,
|
||||
) -> Self {
|
||||
let sapling = sapling.map(|tree| Treestate {
|
||||
commitments: Commitments { final_state: tree },
|
||||
});
|
||||
let orchard = orchard.map(|tree| Treestate {
|
||||
commitments: Commitments { final_state: tree },
|
||||
});
|
||||
|
||||
Self {
|
||||
hash,
|
||||
height,
|
||||
time,
|
||||
sapling,
|
||||
orchard,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GetTreestate {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
hash: Hash([0; 32]),
|
||||
height: Height::MIN,
|
||||
time: Default::default(),
|
||||
sapling: Default::default(),
|
||||
orchard: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A treestate that is included in the [`z_gettreestate`][1] RPC response.
|
||||
///
|
||||
/// [1]: https://zcash.github.io/rpc/z_gettreestate.html
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
|
||||
struct Treestate<Tree: AsRef<[u8]>> {
|
||||
/// Contains an Orchard or Sapling serialized note commitment tree,
|
||||
/// hex-encoded.
|
||||
commitments: Commitments<Tree>,
|
||||
}
|
||||
|
||||
/// A wrapper that contains either an Orchard or Sapling note commitment tree.
|
||||
///
|
||||
/// Note that in the original [`z_gettreestate`][1] RPC, [`Commitments`] also
|
||||
/// contains the field `finalRoot`. Zebra does *not* use this field.
|
||||
///
|
||||
/// [1]: https://zcash.github.io/rpc/z_gettreestate.html
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
|
||||
struct Commitments<Tree: AsRef<[u8]>> {
|
||||
/// Orchard or Sapling serialized note commitment tree, hex-encoded.
|
||||
#[serde(with = "hex")]
|
||||
#[serde(rename = "finalState")]
|
||||
final_state: Tree,
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ use quote::ToTokens;
|
|||
use serde::Serialize;
|
||||
use syn::LitStr;
|
||||
|
||||
use zebra_rpc::methods::*;
|
||||
use zebra_rpc::methods::{trees::GetTreestate, *};
|
||||
|
||||
// The API server
|
||||
const SERVER: &str = "http://localhost:8232";
|
||||
|
|
|
@ -19,7 +19,7 @@ edition = "2021"
|
|||
|
||||
# Zebra is only supported on the latest stable Rust version. See the README for details.
|
||||
# Any Zebra release can break compatibility with older Rust versions.
|
||||
rust-version = "1.73"
|
||||
rust-version = "1.76"
|
||||
|
||||
# Settings that impact runtime behaviour
|
||||
|
||||
|
|
|
@ -455,7 +455,11 @@ impl Application for ZebradApp {
|
|||
// Activate the global span, so it's visible when we load the other
|
||||
// components. Space is at a premium here, so we use an empty message,
|
||||
// short commit hash, and the unique part of the network name.
|
||||
let net = &config.network.network.to_string()[..4];
|
||||
let net = config.network.network.to_string();
|
||||
let net = match net.as_str() {
|
||||
default_net_name @ ("Testnet" | "Mainnet") => &default_net_name[..4],
|
||||
other_net_name => other_net_name,
|
||||
};
|
||||
let global_span = if let Some(git_commit) = ZebradApp::git_commit() {
|
||||
error_span!("", zebrad = git_commit, net)
|
||||
} else {
|
||||
|
|
|
@ -78,6 +78,8 @@
|
|||
//!
|
||||
//! Some of the diagnostic features are optional, and need to be enabled at compile-time.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use abscissa_core::{config, Command, FrameworkError};
|
||||
use color_eyre::eyre::{eyre, Report};
|
||||
use futures::FutureExt;
|
||||
|
@ -113,6 +115,19 @@ pub struct StartCmd {
|
|||
impl StartCmd {
|
||||
async fn start(&self) -> Result<(), Report> {
|
||||
let config = APPLICATION.config();
|
||||
let is_regtest = config.network.network.is_regtest();
|
||||
|
||||
let config = if is_regtest {
|
||||
Arc::new(ZebradConfig {
|
||||
mempool: mempool::Config {
|
||||
debug_enable_at_height: Some(0),
|
||||
..config.mempool
|
||||
},
|
||||
..Arc::unwrap_or_clone(config)
|
||||
})
|
||||
} else {
|
||||
config
|
||||
};
|
||||
|
||||
info!("initializing node state");
|
||||
let (_, max_checkpoint_height) = zebra_consensus::router::init_checkpoint_list(
|
||||
|
@ -301,7 +316,7 @@ impl StartCmd {
|
|||
);
|
||||
|
||||
info!("spawning syncer task");
|
||||
let syncer_task_handle = if config.network.network.is_regtest() {
|
||||
let syncer_task_handle = if is_regtest {
|
||||
if !syncer
|
||||
.state_contains(config.network.network.genesis_hash())
|
||||
.await?
|
||||
|
|
Loading…
Reference in New Issue