diff --git a/Cargo.lock b/Cargo.lock index e393f2aad2d..51770c5191f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6191,6 +6191,7 @@ dependencies = [ "tower", "tracing", "zcash_address", + "zcash_primitives 0.13.0", "zebra-chain", "zebra-consensus", "zebra-network", diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 5c2df3ebd8a..0dd14a013b3 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -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) diff --git a/book/src/user/regtest.md b/book/src/user/regtest.md new file mode 100644 index 00000000000..8499f29dde0 --- /dev/null +++ b/book/src/user/regtest.md @@ -0,0 +1,61 @@ +# 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" +``` + +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. + +Compile Zebra with `cargo build --features "internal-miner"` and add `internal_miner = true` in the mining section of its configuration to use the internal miner with Regtest: + +```toml +[mining] +internal_miner = true +``` + +## 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. + +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. + +See the `regtest_submit_blocks()` acceptance test as an example for using Zebra's RPC methods to submit blocks on Regtest. \ No newline at end of file diff --git a/zebra-chain/src/orchard/tree.rs b/zebra-chain/src/orchard/tree.rs index 25cde25d962..b3433a99c6d 100644 --- a/zebra-chain/src/orchard/tree.rs +++ b/zebra-chain/src/orchard/tree.rs @@ -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 { + // 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> 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); - -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>> for SerializedTree { - fn from(maybe_tree: Option>) -> Self { - match maybe_tree { - Some(tree) => tree.as_ref().into(), - None => Self(Vec::new()), - } - } -} - -impl AsRef<[u8]> for SerializedTree { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} diff --git a/zebra-chain/src/sapling/tree.rs b/zebra-chain/src/sapling/tree.rs index 7b137422b66..dc6aa8d2967 100644 --- a/zebra-chain/src/sapling/tree.rs +++ b/zebra-chain/src/sapling/tree.rs @@ -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 compatible with `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 compatible with `zcashd`'s RPCs. + pub fn to_rpc_bytes(&self) -> Vec { + // 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> 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); - -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]: - 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>> for SerializedTree { - fn from(maybe_tree: Option>) -> Self { - match maybe_tree { - Some(tree) => tree.as_ref().into(), - None => Self(vec![]), - } - } -} - -impl AsRef<[u8]> for SerializedTree { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} diff --git a/zebra-rpc/Cargo.toml b/zebra-rpc/Cargo.toml index 88450a49338..a5a6396b7ac 100644 --- a/zebra-rpc/Cargo.toml +++ b/zebra-rpc/Cargo.toml @@ -64,6 +64,8 @@ tracing = "0.1.39" hex = { version = "0.4.3", features = ["serde"] } serde = { version = "1.0.201", 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 diff --git a/zebra-rpc/src/methods.rs b/zebra-rpc/src/methods.rs index 92c7aa22a46..72464abdd08 100644 --- a/zebra-rpc/src/methods.rs +++ b/zebra-rpc/src/methods.rs @@ -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}, @@ -51,6 +50,8 @@ pub mod get_block_template_rpcs; #[cfg(feature = "getblocktemplate-rpcs")] pub use get_block_template_rpcs::{GetBlockTemplateRpc, GetBlockTemplateRpcImpl}; +use self::trees::GetTreestate; + #[cfg(test)] mod tests; @@ -504,30 +505,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(to_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(to_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(to_error("No Chain tip available yet"))?; let mut estimated_height = if current_block_time > Utc::now() || zebra_estimated_height < tip_height { @@ -606,11 +595,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_err(to_error)?; match response { zebra_state::ReadResponse::AddressBalance(balance) => Ok(AddressBalance { @@ -647,11 +632,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_err(to_error)?; let queue_results = match response { mempool::Response::Queued(results) => results, @@ -666,14 +647,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_err(to_error) } .boxed() } @@ -694,14 +671,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_err(to_error)?; if verbosity == 0 { // # Performance @@ -713,11 +683,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_err(to_error)?; match response { zebra_state::ReadResponse::Block(Some(block)) => { @@ -761,11 +727,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_err(to_error)?; match response { zebra_state::ReadResponse::BlockHash(Some(hash)) => hash, @@ -947,11 +909,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_err(to_error)?; match response { #[cfg(feature = "getblocktemplate-rpcs")] @@ -1030,11 +988,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_err(to_error)?; match response { mempool::Response::Transactions(unmined_transactions) => { @@ -1052,11 +1006,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_err(to_error)?; match response { zebra_state::ReadResponse::Transaction(Some(MinedTx { @@ -1069,59 +1019,38 @@ 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(to_error("Transaction not found")) + } _ => unreachable!("unmatched response to a transaction request"), } } .boxed() } - // 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> { 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, - })?; + let hash_or_height = hash_or_height.parse().map_err(to_error)?; + // Fetch the block referenced by [`hash_or_height`] from the state. + // // # Concurrency // - // For consistency, this lookup must be performed first, then all the other - // lookups must be based on the hash. - - // Fetch the block referenced by [`hash_or_height`] from the state. - // 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 + // 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_err(to_error)? + { zebra_state::ReadResponse::Block(Some(block)) => block, zebra_state::ReadResponse::Block(None) => { return Err(Error { @@ -1133,73 +1062,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(); - - // 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, - })?; + let hash = hash_or_height + .hash_or_else(|_| Some(block.hash())) + .expect("block hash"); - // 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("block 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_err(to_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_err(to_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 +1131,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_err(to_error)?; let subtrees = match response { zebra_state::ReadResponse::SaplingSubtrees(subtrees) => subtrees, @@ -1251,11 +1157,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_err(to_error)?; let subtrees = match response { zebra_state::ReadResponse::OrchardSubtrees(subtrees) => subtrees, @@ -1316,11 +1218,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_err(to_error)?; let hashes = match response { zebra_state::ReadResponse::AddressesTransactionIds(hashes) => { @@ -1368,11 +1266,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_err(to_error)?; let utxos = match response { zebra_state::ReadResponse::AddressUtxos(utxos) => utxos, _ => unreachable!("unmatched response to a UtxosByAddresses request"), @@ -1422,11 +1316,9 @@ pub fn best_chain_tip_height(latest_chain_tip: &Tip) -> Result 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(to_error("No blocks in state")) } /// Response to a `getinfo` RPC request. @@ -1660,85 +1552,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, - - /// A treestate containing an Orchard note commitment tree, hex-encoded. - #[serde(skip_serializing_if = "Treestate::is_empty")] - orchard: Treestate, -} - -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> { - /// Contains an Orchard or Sapling serialized note commitment tree, - /// hex-encoded. - commitments: Commitments, -} - -/// A wrapper that contains either an Orchard or Sapling note commitment tree. -/// -/// Note that in the original [`z_gettreestate`][1] RPC, [`Commitments`] also -/// contains the field `finalRoot`. Zebra does *not* use this field. -/// -/// [1]: https://zcash.github.io/rpc/z_gettreestate.html -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] -struct Commitments> { - /// Orchard or Sapling serialized note commitment tree, hex-encoded. - #[serde(with = "hex")] - #[serde(rename = "finalState")] - final_state: Tree, -} - -impl> Treestate { - /// Returns `true` if there's no serialized commitment tree. - fn is_empty(&self) -> bool { - self.commitments.final_state.as_ref().is_empty() - } -} - /// Response to a `getrawtransaction` RPC request. /// /// See the notes for the [`Rpc::get_raw_transaction` method]. @@ -1938,3 +1751,12 @@ pub fn height_from_signed_int(index: i32, tip_height: Height) -> Result Ok(Height(sanitized_height)) } } + +/// Turns anything implementing [`ToString`] into [`Error`]. +fn to_error(err: impl ToString) -> Error { + Error { + code: ErrorCode::ServerError(0), + message: err.to_string(), + data: None, + } +} diff --git a/zebra-rpc/src/methods/tests/snapshot.rs b/zebra-rpc/src/methods/tests/snapshot.rs index b978805324d..0ee6dc90099 100644 --- a/zebra-rpc/src/methods/tests/snapshot.rs +++ b/zebra-rpc/src/methods/tests/snapshot.rs @@ -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,105 @@ 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 rsp = 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", rsp)); + + // Request the treestate by a hash for a block which is not in the state. + let rsp = rpc.z_get_treestate(block::Hash([0; 32]).to_string()).await; + settings.bind(|| insta::assert_json_snapshot!("z_get_treestate_by_non_existent_hash", rsp)); + + // Request the treestate before Sapling activation. + let rsp = 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", rsp)); + + // Request the treestate at Sapling activation. + let rsp = 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", rsp)); + + // Request the treestate for an invalid height. + let rsp = rpc + .z_get_treestate(EXCESSIVE_BLOCK_HEIGHT.to_string()) + .await; + settings.bind(|| insta::assert_json_snapshot!("z_get_treestate_excessive_block_height", rsp)); + + // Request the treestate for an unparsable hash or height. + let rsp = rpc.z_get_treestate("Do you even shield?".to_string()).await; + settings + .bind(|| insta::assert_json_snapshot!("z_get_treestate_unparsable_hash_or_height", rsp)); + + // 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 +349,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 +597,6 @@ fn snapshot_rpc_getrawmempool(raw_mempool: Vec, 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, - 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, diff --git a/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_by_hash@custom_testnet.snap b/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_by_hash@custom_testnet.snap new file mode 100644 index 00000000000..5569cdc11bc --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_by_hash@custom_testnet.snap @@ -0,0 +1,9 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: rsp +--- +{ + "hash": "05a60a92d99d85997cce3b87616c089f6124d7342af37106edc76126334a2c38", + "height": 0, + "time": 1477648033 +} diff --git a/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_invalid_excessive_height@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_by_non_existent_hash@custom_testnet.snap similarity index 86% rename from zebra-rpc/src/methods/tests/snapshots/z_get_treestate_invalid_excessive_height@mainnet_10.snap rename to zebra-rpc/src/methods/tests/snapshots/z_get_treestate_by_non_existent_hash@custom_testnet.snap index fd6c362fd59..0f8f4f7eaf8 100644 --- a/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_invalid_excessive_height@mainnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_by_non_existent_hash@custom_testnet.snap @@ -1,6 +1,6 @@ --- source: zebra-rpc/src/methods/tests/snapshot.rs -expression: tree_state +expression: rsp --- { "Err": { diff --git a/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_empty_Sapling_treestate@custom_testnet.snap b/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_empty_Sapling_treestate@custom_testnet.snap new file mode 100644 index 00000000000..218d3f22251 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_empty_Sapling_treestate@custom_testnet.snap @@ -0,0 +1,14 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: rsp +--- +{ + "hash": "00f1a49e54553ac3ef735f2eb1d8247c9a87c22a47dbd7823ae70adcd6c21a18", + "height": 2, + "time": 1477676169, + "sapling": { + "commitments": { + "finalState": "000000" + } + } +} diff --git a/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_invalid_excessive_height@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_excessive_block_height@custom_testnet.snap similarity index 86% rename from zebra-rpc/src/methods/tests/snapshots/z_get_treestate_invalid_excessive_height@testnet_10.snap rename to zebra-rpc/src/methods/tests/snapshots/z_get_treestate_excessive_block_height@custom_testnet.snap index fd6c362fd59..0f8f4f7eaf8 100644 --- a/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_invalid_excessive_height@testnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_excessive_block_height@custom_testnet.snap @@ -1,6 +1,6 @@ --- source: zebra-rpc/src/methods/tests/snapshot.rs -expression: tree_state +expression: rsp --- { "Err": { diff --git a/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_valid@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_no_treestate@custom_testnet.snap similarity index 88% rename from zebra-rpc/src/methods/tests/snapshots/z_get_treestate_valid@testnet_10.snap rename to zebra-rpc/src/methods/tests/snapshots/z_get_treestate_no_treestate@custom_testnet.snap index 474b781d30e..402d52a2b78 100644 --- a/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_valid@testnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_no_treestate@custom_testnet.snap @@ -1,6 +1,6 @@ --- source: zebra-rpc/src/methods/tests/snapshot.rs -expression: tree_state +expression: rsp --- { "hash": "025579869bcf52a989337342f5f57a84f3a28b968f7d6a8307902b065a668d23", diff --git a/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_unparsable_hash_or_height@custom_testnet.snap b/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_unparsable_hash_or_height@custom_testnet.snap new file mode 100644 index 00000000000..ecd3fdd3121 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_unparsable_hash_or_height@custom_testnet.snap @@ -0,0 +1,10 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: rsp +--- +{ + "Err": { + "code": 0, + "message": "parse error: could not convert the input string to a hash or height" + } +} diff --git a/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_valid@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_valid@mainnet_10.snap deleted file mode 100644 index 845d918b214..00000000000 --- a/zebra-rpc/src/methods/tests/snapshots/z_get_treestate_valid@mainnet_10.snap +++ /dev/null @@ -1,9 +0,0 @@ ---- -source: zebra-rpc/src/methods/tests/snapshot.rs -expression: tree_state ---- -{ - "hash": "0007bc227e1c57a4a70e237cad00e7b7ce565155ab49166bc57397a26d339283", - "height": 1, - "time": 1477671596 -} diff --git a/zebra-rpc/src/methods/trees.rs b/zebra-rpc/src/methods/trees.rs index 43389c43d69..c470b283557 100644 --- a/zebra-rpc/src/methods/trees.rs +++ b/zebra-rpc/src/methods/trees.rs @@ -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; @@ -29,3 +31,93 @@ pub struct GetSubtrees { //#[serde(skip_serializing_if = "Vec::is_empty")] pub subtrees: Vec, } + +/// Response to a `z_gettreestate` RPC request. +/// +/// Contains the hex-encoded Sapling & Orchard note commitment trees, and their corresponding +/// [`struct@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: 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>>, + + /// A treestate containing an Orchard note commitment tree, hex-encoded. + #[serde(skip_serializing_if = "Option::is_none")] + orchard: Option>>, +} + +impl GetTreestate { + /// Constructs [`GetTreestate`] from its constituent parts. + pub fn from_parts( + hash: Hash, + height: Height, + time: u32, + sapling: Option>, + orchard: Option>, + ) -> 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> { + /// Contains an Orchard or Sapling serialized note commitment tree, + /// hex-encoded. + commitments: Commitments, +} + +/// A wrapper that contains either an Orchard or Sapling note commitment tree. +/// +/// Note that in the original [`z_gettreestate`][1] RPC, [`Commitments`] also +/// contains the field `finalRoot`. Zebra does *not* use this field. +/// +/// [1]: https://zcash.github.io/rpc/z_gettreestate.html +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] +struct Commitments> { + /// Orchard or Sapling serialized note commitment tree, hex-encoded. + #[serde(with = "hex")] + #[serde(rename = "finalState")] + final_state: Tree, +} diff --git a/zebra-utils/src/bin/openapi-generator/main.rs b/zebra-utils/src/bin/openapi-generator/main.rs index 6dc24db9590..020c166534b 100644 --- a/zebra-utils/src/bin/openapi-generator/main.rs +++ b/zebra-utils/src/bin/openapi-generator/main.rs @@ -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"; diff --git a/zebrad/Cargo.toml b/zebrad/Cargo.toml index 6311721663d..1114f29f09f 100644 --- a/zebrad/Cargo.toml +++ b/zebrad/Cargo.toml @@ -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 diff --git a/zebrad/src/commands/start.rs b/zebrad/src/commands/start.rs index 2f2dabf0daa..aa2b3f781f9 100644 --- a/zebrad/src/commands/start.rs +++ b/zebrad/src/commands/start.rs @@ -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?