diff --git a/zebra-consensus/Cargo.toml b/zebra-consensus/Cargo.toml index 86fabe3ce8a..2a60bddef0d 100644 --- a/zebra-consensus/Cargo.toml +++ b/zebra-consensus/Cargo.toml @@ -7,7 +7,10 @@ edition = "2021" [features] default = [] -getblocktemplate-rpcs = [] +getblocktemplate-rpcs = [ + "zebra-state/getblocktemplate-rpcs", + "zebra-chain/getblocktemplate-rpcs", +] proptest-impl = ["proptest", "proptest-derive", "zebra-chain/proptest-impl", "zebra-state/proptest-impl"] [dependencies] diff --git a/zebra-consensus/src/block.rs b/zebra-consensus/src/block.rs index a822323fe0d..231b4a94ba4 100644 --- a/zebra-consensus/src/block.rs +++ b/zebra-consensus/src/block.rs @@ -21,20 +21,17 @@ use thiserror::Error; use tower::{Service, ServiceExt}; use tracing::Instrument; -use zebra_chain::{ - amount::Amount, - block::{self, Block}, - parameters::Network, - transparent, - work::equihash, -}; +use zebra_chain::{amount::Amount, block, parameters::Network, transparent, work::equihash}; use zebra_state as zs; use crate::{error::*, transaction as tx, BoxError}; pub mod check; +pub mod request; pub mod subsidy; +pub use request::Request; + #[cfg(test)] mod tests; @@ -74,6 +71,11 @@ pub enum VerifyBlockError { // TODO: make this into a concrete type, and add it to is_duplicate_request() (#2908) Commit(#[source] BoxError), + #[cfg(feature = "getblocktemplate-rpcs")] + #[error("unable to validate block proposal: failed semantic verification (proof of work is not checked for proposals)")] + // TODO: make this into a concrete type (see #5732) + ValidateProposal(#[source] BoxError), + #[error("invalid transaction")] Transaction(#[from] TransactionError), } @@ -115,7 +117,7 @@ where } } -impl Service> for BlockVerifier +impl Service for BlockVerifier where S: Service + Send + Clone + 'static, S::Future: Send + 'static, @@ -134,11 +136,13 @@ where Poll::Ready(Ok(())) } - fn call(&mut self, block: Arc) -> Self::Future { + fn call(&mut self, request: Request) -> Self::Future { let mut state_service = self.state_service.clone(); let mut transaction_verifier = self.transaction_verifier.clone(); let network = self.network; + let block = request.block(); + // We don't include the block hash, because it's likely already in a parent span let span = tracing::debug_span!("block", height = ?block.coinbase_height()); @@ -172,10 +176,17 @@ where Err(BlockError::MaxHeight(height, hash, block::Height::MAX))?; } - // Do the difficulty checks first, to raise the threshold for - // attacks that use any other fields. - check::difficulty_is_valid(&block.header, network, &height, &hash)?; - check::equihash_solution_is_valid(&block.header)?; + // > The block data MUST be validated and checked against the server's usual + // > acceptance rules (excluding the check for a valid proof-of-work). + // + if request.is_proposal() { + check::difficulty_threshold_is_valid(&block.header, network, &height, &hash)?; + } else { + // Do the difficulty checks first, to raise the threshold for + // attacks that use any other fields. + check::difficulty_is_valid(&block.header, network, &height, &hash)?; + check::equihash_solution_is_valid(&block.header)?; + } // Next, check the Merkle root validity, to ensure that // the header binds to the transactions in the blocks. @@ -279,6 +290,23 @@ where new_outputs, transaction_hashes, }; + + // Return early for proposal requests when getblocktemplate-rpcs feature is enabled + #[cfg(feature = "getblocktemplate-rpcs")] + if request.is_proposal() { + return match state_service + .ready() + .await + .map_err(VerifyBlockError::ValidateProposal)? + .call(zs::Request::CheckBlockProposalValidity(prepared_block)) + .await + .map_err(VerifyBlockError::ValidateProposal)? + { + zs::Response::ValidBlockProposal => Ok(hash), + _ => unreachable!("wrong response for CheckBlockProposalValidity"), + }; + } + match state_service .ready() .await diff --git a/zebra-consensus/src/block/check.rs b/zebra-consensus/src/block/check.rs index 094adf47b2c..ddd3dbefa63 100644 --- a/zebra-consensus/src/block/check.rs +++ b/zebra-consensus/src/block/check.rs @@ -58,24 +58,22 @@ pub fn coinbase_is_first(block: &Block) -> Result, Ok(first.clone()) } -/// Returns `Ok(())` if `hash` passes: -/// - the target difficulty limit for `network` (PoWLimit), and -/// - the difficulty filter, -/// based on the fields in `header`. +/// Returns `Ok(ExpandedDifficulty)` if the`difficulty_threshold` of `header` is at least as difficult as +/// the target difficulty limit for `network` (PoWLimit) /// -/// If the block is invalid, returns an error containing `height` and `hash`. -pub fn difficulty_is_valid( +/// If the header difficulty threshold is invalid, returns an error containing `height` and `hash`. +pub fn difficulty_threshold_is_valid( header: &Header, network: Network, height: &Height, hash: &Hash, -) -> Result<(), BlockError> { +) -> Result { let difficulty_threshold = header .difficulty_threshold .to_expanded() .ok_or(BlockError::InvalidDifficulty(*height, *hash))?; - // Note: the comparisons in this function are u256 integer comparisons, like + // Note: the comparison in this function is a u256 integer comparison, like // zcashd and bitcoin. Greater values represent *less* work. // The PowLimit check is part of `Threshold()` in the spec, but it doesn't @@ -90,6 +88,26 @@ pub fn difficulty_is_valid( ))?; } + Ok(difficulty_threshold) +} + +/// Returns `Ok(())` if `hash` passes: +/// - the target difficulty limit for `network` (PoWLimit), and +/// - the difficulty filter, +/// based on the fields in `header`. +/// +/// If the block is invalid, returns an error containing `height` and `hash`. +pub fn difficulty_is_valid( + header: &Header, + network: Network, + height: &Height, + hash: &Hash, +) -> Result<(), BlockError> { + let difficulty_threshold = difficulty_threshold_is_valid(header, network, height, hash)?; + + // Note: the comparison in this function is a u256 integer comparison, like + // zcashd and bitcoin. Greater values represent *less* work. + // # Consensus // // > The block MUST pass the difficulty filter. diff --git a/zebra-consensus/src/block/request.rs b/zebra-consensus/src/block/request.rs new file mode 100644 index 00000000000..9a0dbcc9cf9 --- /dev/null +++ b/zebra-consensus/src/block/request.rs @@ -0,0 +1,40 @@ +//! Block verifier request type. + +use std::sync::Arc; + +use zebra_chain::block::Block; + +#[derive(Debug, Clone, PartialEq, Eq)] +/// A request to the chain or block verifier +pub enum Request { + /// Performs semantic validation, then asks the state to perform contextual validation and commit the block + Commit(Arc), + + #[cfg(feature = "getblocktemplate-rpcs")] + /// Performs semantic validation but skips checking proof of work, + /// then asks the state to perform contextual validation. + /// Does not commit the block to the state. + CheckProposal(Arc), +} + +impl Request { + /// Returns inner block + pub fn block(&self) -> Arc { + Arc::clone(match self { + Request::Commit(block) => block, + + #[cfg(feature = "getblocktemplate-rpcs")] + Request::CheckProposal(block) => block, + }) + } + + /// Returns `true` if the request is a proposal + pub fn is_proposal(&self) -> bool { + match self { + Request::Commit(_) => false, + + #[cfg(feature = "getblocktemplate-rpcs")] + Request::CheckProposal(_) => true, + } + } +} diff --git a/zebra-consensus/src/block/tests.rs b/zebra-consensus/src/block/tests.rs index 1901984541b..09512633b1c 100644 --- a/zebra-consensus/src/block/tests.rs +++ b/zebra-consensus/src/block/tests.rs @@ -28,19 +28,18 @@ use crate::{parameters::SLOW_START_SHIFT, transaction}; use super::*; -static VALID_BLOCK_TRANSCRIPT: Lazy< - Vec<(Arc, Result)>, -> = Lazy::new(|| { - let block: Arc<_> = - Block::zcash_deserialize(&zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES[..]) - .unwrap() - .into(); - let hash = Ok(block.as_ref().into()); - vec![(block, hash)] -}); +static VALID_BLOCK_TRANSCRIPT: Lazy)>> = + Lazy::new(|| { + let block: Arc<_> = + Block::zcash_deserialize(&zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES[..]) + .unwrap() + .into(); + let hash = Ok(block.as_ref().into()); + vec![(Request::Commit(block), hash)] + }); static INVALID_TIME_BLOCK_TRANSCRIPT: Lazy< - Vec<(Arc, Result)>, + Vec<(Request, Result)>, > = Lazy::new(|| { let mut block: Block = Block::zcash_deserialize(&zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES[..]).unwrap(); @@ -55,11 +54,14 @@ static INVALID_TIME_BLOCK_TRANSCRIPT: Lazy< .unwrap(); Arc::make_mut(&mut block.header).time = three_hours_in_the_future; - vec![(Arc::new(block), Err(ExpectedTranscriptError::Any))] + vec![( + Request::Commit(Arc::new(block)), + Err(ExpectedTranscriptError::Any), + )] }); static INVALID_HEADER_SOLUTION_TRANSCRIPT: Lazy< - Vec<(Arc, Result)>, + Vec<(Request, Result)>, > = Lazy::new(|| { let mut block: Block = Block::zcash_deserialize(&zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES[..]).unwrap(); @@ -67,11 +69,14 @@ static INVALID_HEADER_SOLUTION_TRANSCRIPT: Lazy< // Change nonce to something invalid Arc::make_mut(&mut block.header).nonce = [0; 32]; - vec![(Arc::new(block), Err(ExpectedTranscriptError::Any))] + vec![( + Request::Commit(Arc::new(block)), + Err(ExpectedTranscriptError::Any), + )] }); static INVALID_COINBASE_TRANSCRIPT: Lazy< - Vec<(Arc, Result)>, + Vec<(Request, Result)>, > = Lazy::new(|| { let header = block::Header::zcash_deserialize(&zebra_test::vectors::DUMMY_HEADER[..]).unwrap(); @@ -105,9 +110,18 @@ static INVALID_COINBASE_TRANSCRIPT: Lazy< assert_eq!(block3.transactions.len(), 2); vec![ - (Arc::new(block1), Err(ExpectedTranscriptError::Any)), - (Arc::new(block2), Err(ExpectedTranscriptError::Any)), - (Arc::new(block3), Err(ExpectedTranscriptError::Any)), + ( + Request::Commit(Arc::new(block1)), + Err(ExpectedTranscriptError::Any), + ), + ( + Request::Commit(Arc::new(block2)), + Err(ExpectedTranscriptError::Any), + ), + ( + Request::Commit(Arc::new(block3)), + Err(ExpectedTranscriptError::Any), + ), ] }); diff --git a/zebra-consensus/src/chain.rs b/zebra-consensus/src/chain.rs index 59803180612..4f54f417768 100644 --- a/zebra-consensus/src/chain.rs +++ b/zebra-consensus/src/chain.rs @@ -15,7 +15,6 @@ use std::{ future::Future, pin::Pin, - sync::Arc, task::{Context, Poll}, }; @@ -27,15 +26,14 @@ use tower::{buffer::Buffer, util::BoxService, Service, ServiceExt}; use tracing::{instrument, Span}; use zebra_chain::{ - block::{self, Block, Height}, + block::{self, Height}, parameters::Network, }; use zebra_state as zs; use crate::{ - block::BlockVerifier, - block::VerifyBlockError, + block::{BlockVerifier, Request, VerifyBlockError}, checkpoint::{CheckpointList, CheckpointVerifier, VerifyCheckpointError}, error::TransactionError, transaction, BoxError, Config, @@ -129,7 +127,7 @@ impl VerifyChainError { } } -impl Service> for ChainVerifier +impl Service for ChainVerifier where S: Service + Send + Clone + 'static, S::Future: Send + 'static, @@ -167,14 +165,29 @@ where Poll::Ready(Ok(())) } - fn call(&mut self, block: Arc) -> Self::Future { + fn call(&mut self, request: Request) -> Self::Future { + let block = request.block(); + match block.coinbase_height() { + #[cfg(feature = "getblocktemplate-rpcs")] + // There's currently no known use case for block proposals below the checkpoint height, + // so it's okay to immediately return an error here. + Some(height) if height <= self.max_checkpoint_height && request.is_proposal() => { + async { + // TODO: Add a `ValidateProposalError` enum with a `BelowCheckpoint` variant? + Err(VerifyBlockError::ValidateProposal( + "block proposals must be above checkpoint height".into(), + ))? + } + .boxed() + } + Some(height) if height <= self.max_checkpoint_height => { self.checkpoint.call(block).map_err(Into::into).boxed() } // This also covers blocks with no height, which the block verifier // will reject immediately. - _ => self.block.call(block).map_err(Into::into).boxed(), + _ => self.block.call(request).map_err(Into::into).boxed(), } } } @@ -219,7 +232,7 @@ pub async fn init( mut state_service: S, debug_skip_parameter_preload: bool, ) -> ( - Buffer, block::Hash, VerifyChainError>, Arc>, + Buffer, Request>, Buffer< BoxService, transaction::Request, diff --git a/zebra-consensus/src/chain/tests.rs b/zebra-consensus/src/chain/tests.rs index 015e1d75f1a..308be754bdb 100644 --- a/zebra-consensus/src/chain/tests.rs +++ b/zebra-consensus/src/chain/tests.rs @@ -49,7 +49,7 @@ async fn verifiers_from_network( network: Network, ) -> ( impl Service< - Arc, + Request, Response = block::Hash, Error = BoxError, Future = impl Future>, @@ -77,7 +77,7 @@ async fn verifiers_from_network( } static BLOCK_VERIFY_TRANSCRIPT_GENESIS: Lazy< - Vec<(Arc, Result)>, + Vec<(Request, Result)>, > = Lazy::new(|| { let block: Arc<_> = Block::zcash_deserialize(&zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES[..]) @@ -85,27 +85,29 @@ static BLOCK_VERIFY_TRANSCRIPT_GENESIS: Lazy< .into(); let hash = Ok(block.hash()); - vec![(block, hash)] + vec![(Request::Commit(block), hash)] }); static BLOCK_VERIFY_TRANSCRIPT_GENESIS_FAIL: Lazy< - Vec<(Arc, Result)>, + Vec<(Request, Result)>, > = Lazy::new(|| { let block: Arc<_> = Block::zcash_deserialize(&zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES[..]) .unwrap() .into(); - vec![(block, Err(ExpectedTranscriptError::Any))] + vec![(Request::Commit(block), Err(ExpectedTranscriptError::Any))] }); -static NO_COINBASE_TRANSCRIPT: Lazy< - Vec<(Arc, Result)>, -> = Lazy::new(|| { - let block = block_no_transactions(); +static NO_COINBASE_TRANSCRIPT: Lazy)>> = + Lazy::new(|| { + let block = block_no_transactions(); - vec![(Arc::new(block), Err(ExpectedTranscriptError::Any))] -}); + vec![( + Request::Commit(Arc::new(block)), + Err(ExpectedTranscriptError::Any), + )] + }); static NO_COINBASE_STATE_TRANSCRIPT: Lazy< Vec<(zs::Request, Result)>, diff --git a/zebra-consensus/src/lib.rs b/zebra-consensus/src/lib.rs index 669e14dd83b..f07511d2a26 100644 --- a/zebra-consensus/src/lib.rs +++ b/zebra-consensus/src/lib.rs @@ -51,7 +51,7 @@ pub use block::{ funding_streams::{funding_stream_address, funding_stream_values, new_coinbase_script}, general::miner_subsidy, }, - VerifyBlockError, MAX_BLOCK_SIGOPS, + Request, VerifyBlockError, MAX_BLOCK_SIGOPS, }; pub use chain::VerifyChainError; pub use checkpoint::{ diff --git a/zebra-rpc/src/methods/get_block_template_rpcs.rs b/zebra-rpc/src/methods/get_block_template_rpcs.rs index deac8245dff..10aeb96ffde 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs.rs @@ -26,9 +26,8 @@ use crate::methods::{ DEFAULT_SOLUTION_RATE_WINDOW_SIZE, GET_BLOCK_TEMPLATE_MEMPOOL_LONG_POLL_INTERVAL, }, get_block_template::{ - check_block_template_parameters, check_miner_address, check_synced_to_tip, - fetch_mempool_transactions, fetch_state_tip_and_local_time, - generate_coinbase_and_roots, + check_miner_address, check_synced_to_tip, fetch_mempool_transactions, + fetch_state_tip_and_local_time, generate_coinbase_and_roots, validate_block_proposal, }, types::{ get_block_template::GetBlockTemplate, get_mining_info, hex_data::HexData, @@ -101,7 +100,7 @@ pub trait GetBlockTemplateRpc { fn get_block_template( &self, parameters: Option, - ) -> BoxFuture>; + ) -> BoxFuture>; /// Submits block to the node to be validated and committed. /// Returns the [`submit_block::Response`] for the operation, as a JSON string. @@ -165,7 +164,7 @@ where Response = zebra_state::ReadResponse, Error = zebra_state::BoxError, >, - ChainVerifier: Service, Response = block::Hash, Error = zebra_consensus::BoxError> + ChainVerifier: Service + Clone + Send + Sync @@ -217,7 +216,7 @@ where + Sync + 'static, Tip: ChainTip + Clone + Send + Sync + 'static, - ChainVerifier: Service, Response = block::Hash, Error = zebra_consensus::BoxError> + ChainVerifier: Service + Clone + Send + Sync @@ -265,12 +264,12 @@ where + 'static, >::Future: Send, Tip: ChainTip + Clone + Send + Sync + 'static, - ChainVerifier: Service, Response = block::Hash, Error = zebra_consensus::BoxError> + ChainVerifier: Service + Clone + Send + Sync + 'static, - >>::Future: Send, + >::Future: Send, SyncStatus: ChainSyncStatus + Clone + Send + Sync + 'static, { fn get_block_count(&self) -> Result { @@ -312,11 +311,11 @@ where .boxed() } - // TODO: use HexData to handle block proposal data, and a generic error constructor (#5548) + // TODO: use a generic error constructor (#5548) fn get_block_template( &self, parameters: Option, - ) -> BoxFuture> { + ) -> BoxFuture> { // Should we generate coinbase transactions that are exactly like zcashd's? // // This is useful for testing, but either way Zebra should obey the consensus rules. @@ -332,21 +331,28 @@ where let sync_status = self.sync_status.clone(); let state = self.state.clone(); + if let Some(HexData(block_proposal_bytes)) = parameters + .as_ref() + .and_then(get_block_template::JsonParameters::block_proposal_data) + { + return validate_block_proposal(self.chain_verifier.clone(), block_proposal_bytes) + .boxed(); + } + // To implement long polling correctly, we split this RPC into multiple phases. async move { + get_block_template::check_parameters(¶meters)?; + + let client_long_poll_id = parameters + .as_ref() + .and_then(|params| params.long_poll_id.clone()); + // - One-off checks // Check config and parameters. // These checks always have the same result during long polling. let miner_address = check_miner_address(miner_address)?; - let mut client_long_poll_id = None; - if let Some(parameters) = parameters { - check_block_template_parameters(¶meters)?; - - client_long_poll_id = parameters.long_poll_id; - } - // - Checks and fetches that can change during long polling // // Set up the loop. @@ -506,7 +512,7 @@ where ?server_long_poll_id, ?client_long_poll_id, "returning from long poll due to a state error.\ - Is Zebra shutting down?" + Is Zebra shutting down?" ); return Err(Error { @@ -582,7 +588,7 @@ where COINBASE_LIKE_ZCASHD, ); - Ok(response) + Ok(response.into()) } .boxed() } @@ -608,7 +614,7 @@ where message: error.to_string(), data: None, })? - .call(Arc::new(block)) + .call(zebra_consensus::Request::Commit(Arc::new(block))) .await; let chain_error = match chain_verifier_response { diff --git a/zebra-rpc/src/methods/get_block_template_rpcs/constants.rs b/zebra-rpc/src/methods/get_block_template_rpcs/constants.rs index 1fcc28e5743..ed624e4dc83 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs/constants.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs/constants.rs @@ -18,6 +18,8 @@ pub const GET_BLOCK_TEMPLATE_MEMPOOL_LONG_POLL_INTERVAL: u64 = 5; pub const GET_BLOCK_TEMPLATE_NONCE_RANGE_FIELD: &str = "00000000ffffffff"; /// A hardcoded list of fields that the miner can change from the block template. +/// +/// pub const GET_BLOCK_TEMPLATE_MUTABLE_FIELD: &[&str] = &[ // Standard mutations, copied from zcashd "time", @@ -26,8 +28,9 @@ pub const GET_BLOCK_TEMPLATE_MUTABLE_FIELD: &[&str] = &[ ]; /// A hardcoded list of Zebra's getblocktemplate RPC capabilities. -/// Currently empty. -pub const GET_BLOCK_TEMPLATE_CAPABILITIES_FIELD: &[&str] = &[]; +/// +/// +pub const GET_BLOCK_TEMPLATE_CAPABILITIES_FIELD: &[&str] = &["proposal"]; /// The max estimated distance to the chain tip for the getblocktemplate method. /// @@ -35,6 +38,8 @@ pub const GET_BLOCK_TEMPLATE_CAPABILITIES_FIELD: &[&str] = &[]; /// > A full validator MUST NOT accept blocks with nTime more than two hours in the future /// > according to its clock. This is not strictly a consensus rule because it is nondeterministic, /// > and clock time varies between nodes. +/// > +/// > pub const MAX_ESTIMATED_DISTANCE_TO_NETWORK_CHAIN_TIP: i32 = 100; /// The RPC error code used by `zcashd` for when it's still downloading initial blocks. diff --git a/zebra-rpc/src/methods/get_block_template_rpcs/get_block_template.rs b/zebra-rpc/src/methods/get_block_template_rpcs/get_block_template.rs index d2cfdb78549..7270e810f3c 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs/get_block_template.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs/get_block_template.rs @@ -8,12 +8,14 @@ use tower::{Service, ServiceExt}; use zebra_chain::{ amount::{self, Amount, NegativeOrZero, NonNegative}, block::{ + self, merkle::{self, AuthDataRoot}, ChainHistoryBlockTxAuthCommitmentHash, Height, }, chain_sync_status::ChainSyncStatus, chain_tip::ChainTip, parameters::Network, + serialization::ZcashDeserializeInto, transaction::{Transaction, UnminedTx, VerifiedUnminedTx}, transparent, }; @@ -25,26 +27,57 @@ use zebra_state::GetBlockTemplateChainInfo; use crate::methods::get_block_template_rpcs::{ constants::{MAX_ESTIMATED_DISTANCE_TO_NETWORK_CHAIN_TIP, NOT_SYNCED_ERROR_CODE}, - types::{default_roots::DefaultRoots, get_block_template, transaction::TransactionTemplate}, + types::{default_roots::DefaultRoots, transaction::TransactionTemplate}, }; pub use crate::methods::get_block_template_rpcs::types::get_block_template::*; // - Parameter checks -/// Returns an error if the get block template RPC `parameters` are invalid. -pub fn check_block_template_parameters( - parameters: &get_block_template::JsonParameters, -) -> Result<()> { - if parameters.data.is_some() || parameters.mode == GetBlockTemplateRequestMode::Proposal { - return Err(Error { +/// Checks that `data` is omitted in `Template` mode or provided in `Proposal` mode, +/// +/// Returns an error if there's a mismatch between the mode and whether `data` is provided. +pub fn check_parameters(parameters: &Option) -> Result<()> { + let Some(parameters) = parameters else { + return Ok(()) + }; + + match parameters { + JsonParameters { + mode: GetBlockTemplateRequestMode::Template, + data: None, + .. + } + | JsonParameters { + mode: GetBlockTemplateRequestMode::Proposal, + data: Some(_), + .. + } => Ok(()), + + JsonParameters { + mode: GetBlockTemplateRequestMode::Proposal, + data: None, + .. + } => Err(Error { code: ErrorCode::InvalidParams, - message: "\"proposal\" mode is currently unsupported by Zebra".to_string(), + message: "\"data\" parameter must be \ + provided in \"proposal\" mode" + .to_string(), data: None, - }); - } + }), - Ok(()) + JsonParameters { + mode: GetBlockTemplateRequestMode::Template, + data: Some(_), + .. + } => Err(Error { + code: ErrorCode::InvalidParams, + message: "\"data\" parameter must be \ + omitted in \"template\" mode" + .to_string(), + data: None, + }), + } } /// Returns the miner address, or an error if it is invalid. @@ -60,6 +93,42 @@ pub fn check_miner_address( }) } +/// Attempts to validate block proposal against all of the server's +/// usual acceptance rules (except proof-of-work). +/// +/// Returns a `getblocktemplate` [`Response`]. +pub async fn validate_block_proposal( + mut chain_verifier: ChainVerifier, + block_proposal_bytes: Vec, +) -> Result +where + ChainVerifier: Service + + Clone + + Send + + Sync + + 'static, +{ + let Ok(block) = block_proposal_bytes.zcash_deserialize_into() else { + return Ok(ProposalRejectReason::Rejected.into()) + }; + + let chain_verifier_response = chain_verifier + .ready() + .await + .map_err(|error| Error { + code: ErrorCode::ServerError(0), + message: error.to_string(), + data: None, + })? + .call(zebra_consensus::Request::CheckProposal(Arc::new(block))) + .await; + + Ok(chain_verifier_response + .map(|_hash| ProposalResponse::Valid) + .unwrap_or_else(|_| ProposalRejectReason::Rejected.into()) + .into()) +} + // - State and syncer checks /// Returns an error if Zebra is not synced to the consensus chain tip. diff --git a/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template.rs b/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template.rs index 510871067d9..dcf62d32fa4 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template.rs @@ -27,7 +27,7 @@ pub mod parameters; pub use parameters::*; -/// A serialized `getblocktemplate` RPC response. +/// A serialized `getblocktemplate` RPC response in template mode. #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub struct GetBlockTemplate { /// The getblocktemplate RPC capabilities supported by Zebra. @@ -162,6 +162,14 @@ pub struct GetBlockTemplate { } impl GetBlockTemplate { + /// Returns a `Vec` of capabilities supported by the `getblocktemplate` RPC + pub fn capabilities() -> Vec { + GET_BLOCK_TEMPLATE_CAPABILITIES_FIELD + .iter() + .map(ToString::to_string) + .collect() + } + /// Returns a new [`GetBlockTemplate`] struct, based on the supplied arguments and defaults. /// /// The result of this method only depends on the supplied arguments and constants. @@ -203,10 +211,7 @@ impl GetBlockTemplate { .expect("state always returns a valid difficulty value"); // Convert default values - let capabilities: Vec = GET_BLOCK_TEMPLATE_CAPABILITIES_FIELD - .iter() - .map(ToString::to_string) - .collect(); + let capabilities: Vec = Self::capabilities(); let mutable: Vec = GET_BLOCK_TEMPLATE_MUTABLE_FIELD .iter() .map(ToString::to_string) @@ -253,3 +258,68 @@ impl GetBlockTemplate { } } } + +/// Error response to a `getblocktemplate` RPC request in proposal mode. +#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum ProposalRejectReason { + /// Block proposal rejected as invalid. + Rejected, +} + +/// Response to a `getblocktemplate` RPC request in proposal mode. +/// +/// See +#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(untagged, rename_all = "kebab-case")] +pub enum ProposalResponse { + /// Block proposal was rejected as invalid, returns `reject-reason` and server `capabilities`. + ErrorResponse { + /// Reason the proposal was invalid as-is. + reject_reason: ProposalRejectReason, + + /// The getblocktemplate RPC capabilities supported by Zebra. + capabilities: Vec, + }, + + /// Block proposal was successfully validated, returns null. + Valid, +} + +#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +/// A `getblocktemplate` RPC response. +pub enum Response { + /// `getblocktemplate` RPC request in template mode. + TemplateMode(Box), + + /// `getblocktemplate` RPC request in proposal mode. + ProposalMode(ProposalResponse), +} + +impl From for ProposalResponse { + fn from(reject_reason: ProposalRejectReason) -> Self { + Self::ErrorResponse { + reject_reason, + capabilities: GetBlockTemplate::capabilities(), + } + } +} + +impl From for Response { + fn from(error_response: ProposalRejectReason) -> Self { + Self::ProposalMode(ProposalResponse::from(error_response)) + } +} + +impl From for Response { + fn from(proposal_response: ProposalResponse) -> Self { + Self::ProposalMode(proposal_response) + } +} + +impl From for Response { + fn from(template: GetBlockTemplate) -> Self { + Self::TemplateMode(Box::new(template)) + } +} diff --git a/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template/parameters.rs b/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template/parameters.rs index 01152891816..312ebf51f92 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template/parameters.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template/parameters.rs @@ -3,7 +3,6 @@ use crate::methods::get_block_template_rpcs::types::{hex_data::HexData, long_poll::LongPollId}; /// Defines whether the RPC method should generate a block template or attempt to validate a block proposal. -/// `Proposal` mode is currently unsupported and will return an error. #[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum GetBlockTemplateRequestMode { @@ -11,7 +10,6 @@ pub enum GetBlockTemplateRequestMode { Template, /// Indicates a request to validate block data. - /// Currently unsupported and will return an error. Proposal, } @@ -63,19 +61,17 @@ pub enum GetBlockTemplateCapability { /// All other fields are optional. #[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, Default)] pub struct JsonParameters { - /// Must be set to "template" or omitted, as "proposal" mode is currently unsupported. - /// /// Defines whether the RPC method should generate a block template or attempt to /// validate block data, checking against all of the server's usual acceptance rules /// (excluding the check for a valid proof-of-work). - // TODO: Support `proposal` mode. #[serde(default)] pub mode: GetBlockTemplateRequestMode, - /// Must be omitted as "proposal" mode is currently unsupported. + /// Must be omitted when `getblocktemplate` RPC is called in "template" mode (or when `mode` is omitted). + /// Must be provided when `getblocktemplate` RPC is called in "proposal" mode. /// /// Hex-encoded block data to be validated and checked against the server's usual acceptance rules - /// (excluding the check for a valid proof-of-work) when `mode` is set to `proposal`. + /// (excluding the check for a valid proof-of-work). pub data: Option, /// A list of client-side supported capability features @@ -87,4 +83,29 @@ pub struct JsonParameters { /// In Zebra, the ID represents the chain tip, max time, and mempool contents. #[serde(rename = "longpollid")] pub long_poll_id: Option, + + /// The workid for the block template. + /// + /// currently unused. + #[serde(rename = "workid")] + pub _work_id: Option, +} + +impl JsonParameters { + /// Returns Some(data) with the block proposal hexdata if in `Proposal` mode and `data` is provided. + pub fn block_proposal_data(&self) -> Option { + match self { + Self { data: None, .. } + | Self { + mode: GetBlockTemplateRequestMode::Template, + .. + } => None, + + Self { + mode: GetBlockTemplateRequestMode::Proposal, + data, + .. + } => data.clone(), + } + } } diff --git a/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs b/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs index 2da8102dfb7..5a6108bac18 100644 --- a/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs +++ b/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs @@ -23,13 +23,16 @@ use zebra_node_services::mempool; use zebra_state::{GetBlockTemplateChainInfo, ReadRequest, ReadResponse}; -use zebra_test::mock_service::{MockService, PanicAssertion}; +use zebra_test::{ + mock_service::{MockService, PanicAssertion}, + vectors::BLOCK_MAINNET_1_BYTES, +}; use crate::methods::{ get_block_template_rpcs::{ self, types::{ - get_block_template::{self, GetBlockTemplate}, + get_block_template::{self, GetBlockTemplateRequestMode}, get_mining_info, hex_data::HexData, long_poll::{LongPollId, LONG_POLL_ID_LENGTH}, @@ -159,12 +162,12 @@ pub async fn test_responses( // create a new rpc instance with new state and mock let get_block_template_rpc = GetBlockTemplateRpcImpl::new( network, - mining_config, + mining_config.clone(), Buffer::new(mempool.clone(), 1), new_read_state.clone(), - mock_chain_tip, + mock_chain_tip.clone(), chain_verifier, - mock_sync_status, + mock_sync_status.clone(), ); // Basic variant (default mode and no extra features) @@ -195,10 +198,12 @@ pub async fn test_responses( .await .respond(mempool::Response::FullTransactions(vec![])); - let get_block_template = get_block_template + let get_block_template::Response::TemplateMode(get_block_template) = get_block_template .await .expect("unexpected panic in getblocktemplate RPC task") - .expect("unexpected error in getblocktemplate RPC call"); + .expect("unexpected error in getblocktemplate RPC call") else { + panic!("this getblocktemplate call without parameters should return the `TemplateMode` variant of the response") + }; let coinbase_tx: Transaction = get_block_template .coinbase_txn @@ -207,7 +212,12 @@ pub async fn test_responses( .zcash_deserialize_into() .expect("coinbase bytes are valid"); - snapshot_rpc_getblocktemplate("basic", get_block_template, coinbase_tx, &settings); + snapshot_rpc_getblocktemplate( + "basic", + (*get_block_template).into(), + Some(coinbase_tx), + &settings, + ); // long polling feature with submit old field @@ -250,10 +260,12 @@ pub async fn test_responses( .await .respond(mempool::Response::FullTransactions(vec![])); - let get_block_template = get_block_template + let get_block_template::Response::TemplateMode(get_block_template) = get_block_template .await .expect("unexpected panic in getblocktemplate RPC task") - .expect("unexpected error in getblocktemplate RPC call"); + .expect("unexpected error in getblocktemplate RPC call") else { + panic!("this getblocktemplate call without parameters should return the `TemplateMode` variant of the response") + }; let coinbase_tx: Transaction = get_block_template .coinbase_txn @@ -262,7 +274,62 @@ pub async fn test_responses( .zcash_deserialize_into() .expect("coinbase bytes are valid"); - snapshot_rpc_getblocktemplate("long_poll", get_block_template, coinbase_tx, &settings); + snapshot_rpc_getblocktemplate( + "long_poll", + (*get_block_template).into(), + Some(coinbase_tx), + &settings, + ); + + // `getblocktemplate` proposal mode variant + + let get_block_template = tokio::spawn(get_block_template_rpc.get_block_template(Some( + get_block_template::JsonParameters { + mode: GetBlockTemplateRequestMode::Proposal, + data: Some(HexData("".into())), + ..Default::default() + }, + ))); + + let get_block_template = get_block_template + .await + .expect("unexpected panic in getblocktemplate RPC task") + .expect("unexpected error in getblocktemplate RPC call"); + + snapshot_rpc_getblocktemplate("invalid-proposal", get_block_template, None, &settings); + + let mut mock_chain_verifier = MockService::build().for_unit_tests(); + let get_block_template_rpc = GetBlockTemplateRpcImpl::new( + network, + mining_config, + Buffer::new(mempool.clone(), 1), + new_read_state.clone(), + mock_chain_tip, + mock_chain_verifier.clone(), + mock_sync_status, + ); + + let get_block_template = tokio::spawn(get_block_template_rpc.get_block_template(Some( + get_block_template::JsonParameters { + mode: GetBlockTemplateRequestMode::Proposal, + data: Some(HexData(BLOCK_MAINNET_1_BYTES.to_vec())), + ..Default::default() + }, + ))); + + tokio::spawn(async move { + mock_chain_verifier + .expect_request_that(|req| matches!(req, zebra_consensus::Request::CheckProposal(_))) + .await + .respond(Hash::from([0; 32])); + }); + + let get_block_template = get_block_template + .await + .expect("unexpected panic in getblocktemplate RPC task") + .expect("unexpected error in getblocktemplate RPC call"); + + snapshot_rpc_getblocktemplate("proposal", get_block_template, None, &settings); // `submitblock` @@ -287,19 +354,22 @@ fn snapshot_rpc_getblockhash(block_hash: GetBlockHash, settings: &insta::Setting /// Snapshot `getblocktemplate` response, using `cargo insta` and JSON serialization. fn snapshot_rpc_getblocktemplate( variant: &'static str, - block_template: GetBlockTemplate, - coinbase_tx: Transaction, + block_template: get_block_template::Response, + coinbase_tx: Option, settings: &insta::Settings, ) { settings.bind(|| { insta::assert_json_snapshot!(format!("get_block_template_{variant}"), block_template) }); - settings.bind(|| { - insta::assert_ron_snapshot!( - format!("get_block_template_{variant}.coinbase_tx"), - coinbase_tx - ) - }); + + if let Some(coinbase_tx) = coinbase_tx { + settings.bind(|| { + insta::assert_ron_snapshot!( + format!("get_block_template_{variant}.coinbase_tx"), + coinbase_tx + ) + }); + }; } /// Snapshot `submitblock` response, using `cargo insta` and JSON serialization. diff --git a/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template_basic@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template_basic@mainnet_10.snap index d0853e4a2ea..9506416935e 100644 --- a/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template_basic@mainnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template_basic@mainnet_10.snap @@ -3,7 +3,9 @@ source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs expression: block_template --- { - "capabilities": [], + "capabilities": [ + "proposal" + ], "version": 4, "previousblockhash": "0000000000d723156d9b65ffcf4984da7a19675ed7e2f06d9e5d5188af087bf8", "blockcommitmentshash": "02990723c6b62a724651322d141b4a72a4ffd66518167d809badbd5117d5518a", diff --git a/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template_basic@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template_basic@testnet_10.snap index 0b6e9e0bd54..8a5dd4bc8f5 100644 --- a/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template_basic@testnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template_basic@testnet_10.snap @@ -3,7 +3,9 @@ source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs expression: block_template --- { - "capabilities": [], + "capabilities": [ + "proposal" + ], "version": 4, "previousblockhash": "0000000000d723156d9b65ffcf4984da7a19675ed7e2f06d9e5d5188af087bf8", "blockcommitmentshash": "3b25791957f9383b6ce851d728a78309664d5d7a82ca87b6a9125a2f2c529792", diff --git a/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template_invalid-proposal@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template_invalid-proposal@mainnet_10.snap new file mode 100644 index 00000000000..3a63ad3879a --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template_invalid-proposal@mainnet_10.snap @@ -0,0 +1,10 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs +expression: block_template +--- +{ + "reject_reason": "rejected", + "capabilities": [ + "proposal" + ] +} diff --git a/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template_invalid-proposal@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template_invalid-proposal@testnet_10.snap new file mode 100644 index 00000000000..3a63ad3879a --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template_invalid-proposal@testnet_10.snap @@ -0,0 +1,10 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs +expression: block_template +--- +{ + "reject_reason": "rejected", + "capabilities": [ + "proposal" + ] +} diff --git a/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template_long_poll@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template_long_poll@mainnet_10.snap index 686028d69c8..043c937cd8b 100644 --- a/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template_long_poll@mainnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template_long_poll@mainnet_10.snap @@ -3,7 +3,9 @@ source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs expression: block_template --- { - "capabilities": [], + "capabilities": [ + "proposal" + ], "version": 4, "previousblockhash": "0000000000d723156d9b65ffcf4984da7a19675ed7e2f06d9e5d5188af087bf8", "blockcommitmentshash": "02990723c6b62a724651322d141b4a72a4ffd66518167d809badbd5117d5518a", diff --git a/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template_long_poll@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template_long_poll@testnet_10.snap index ba44908e62e..e258d55e638 100644 --- a/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template_long_poll@testnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template_long_poll@testnet_10.snap @@ -3,7 +3,9 @@ source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs expression: block_template --- { - "capabilities": [], + "capabilities": [ + "proposal" + ], "version": 4, "previousblockhash": "0000000000d723156d9b65ffcf4984da7a19675ed7e2f06d9e5d5188af087bf8", "blockcommitmentshash": "3b25791957f9383b6ce851d728a78309664d5d7a82ca87b6a9125a2f2c529792", diff --git a/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template_proposal@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template_proposal@mainnet_10.snap new file mode 100644 index 00000000000..600686f4fe5 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template_proposal@mainnet_10.snap @@ -0,0 +1,5 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs +expression: block_template +--- +null diff --git a/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template_proposal@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template_proposal@testnet_10.snap new file mode 100644 index 00000000000..600686f4fe5 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template_proposal@testnet_10.snap @@ -0,0 +1,5 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs +expression: block_template +--- +null diff --git a/zebra-rpc/src/methods/tests/vectors.rs b/zebra-rpc/src/methods/tests/vectors.rs index dfadd172d15..a3615a7af6a 100644 --- a/zebra-rpc/src/methods/tests/vectors.rs +++ b/zebra-rpc/src/methods/tests/vectors.rs @@ -997,15 +997,16 @@ async fn rpc_getblocktemplate_mining_address(use_p2pkh: bool) { }; let get_block_template_fut = get_block_template_rpc.get_block_template(None); - let (get_block_template, ..) = tokio::join!( get_block_template_fut, mock_mempool_request_handler, mock_read_state_request_handler, ); - let get_block_template = - get_block_template.expect("unexpected error in getblocktemplate RPC call"); + let get_block_template::Response::TemplateMode(get_block_template) = get_block_template + .expect("unexpected error in getblocktemplate RPC call") else { + panic!("this getblocktemplate call without parameters should return the `TemplateMode` variant of the response") + }; assert_eq!( get_block_template.capabilities, @@ -1099,7 +1100,7 @@ async fn rpc_getblocktemplate_mining_address(use_p2pkh: bool) { ..Default::default() })) .await - .expect_err("needs an error when using unsupported mode"); + .expect_err("needs an error when called in proposal mode without data"); assert_eq!(get_block_template_sync_error.code, ErrorCode::InvalidParams); @@ -1109,7 +1110,7 @@ async fn rpc_getblocktemplate_mining_address(use_p2pkh: bool) { ..Default::default() })) .await - .expect_err("needs an error when passing in block data"); + .expect_err("needs an error when passing in block data in template mode"); assert_eq!(get_block_template_sync_error.code, ErrorCode::InvalidParams); diff --git a/zebra-rpc/src/server.rs b/zebra-rpc/src/server.rs index bfa373e6cc0..d3548a0572e 100644 --- a/zebra-rpc/src/server.rs +++ b/zebra-rpc/src/server.rs @@ -7,7 +7,7 @@ //! See the full list of //! [Differences between JSON-RPC 1.0 and 2.0.](https://www.simple-is-better.org/rpc/#differences-between-1-0-and-2-0) -use std::{fmt, panic, sync::Arc}; +use std::{fmt, panic}; use jsonrpc_core::{Compatibility, MetaIoHandler}; use jsonrpc_http_server::{CloseHandle, ServerBuilder}; @@ -17,10 +17,7 @@ use tower::{buffer::Buffer, Service}; use tracing::{Instrument, *}; use zebra_chain::{ - block::{self, Block}, - chain_sync_status::ChainSyncStatus, - chain_tip::ChainTip, - parameters::Network, + block, chain_sync_status::ChainSyncStatus, chain_tip::ChainTip, parameters::Network, }; use zebra_node_services::mempool; @@ -107,12 +104,15 @@ impl RpcServer { + 'static, State::Future: Send, Tip: ChainTip + Clone + Send + Sync + 'static, - ChainVerifier: Service, Response = block::Hash, Error = zebra_consensus::BoxError> - + Clone + ChainVerifier: Service< + zebra_consensus::Request, + Response = block::Hash, + Error = zebra_consensus::BoxError, + > + Clone + Send + Sync + 'static, - >>::Future: Send, + >::Future: Send, SyncStatus: ChainSyncStatus + Clone + Send + Sync + 'static, { if let Some(listen_addr) = config.listen_addr { diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index c7f04eb79af..1ab59694f09 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -558,6 +558,13 @@ pub enum Request { /// /// Returns [`Response::ValidBestChainTipNullifiersAndAnchors`] CheckBestChainTipNullifiersAndAnchors(UnminedTx), + + #[cfg(feature = "getblocktemplate-rpcs")] + /// Performs contextual validation of the given block, but does not commit it to the state. + /// + /// Returns [`Response::ValidBlockProposal`] when successful. + /// See `[ReadRequest::CheckBlockProposalValidity]` for details. + CheckBlockProposalValidity(PreparedBlock), } impl Request { @@ -577,6 +584,8 @@ impl Request { Request::CheckBestChainTipNullifiersAndAnchors(_) => { "best_chain_tip_nullifiers_anchors" } + #[cfg(feature = "getblocktemplate-rpcs")] + Request::CheckBlockProposalValidity(_) => "check_block_proposal_validity", } } @@ -790,6 +799,16 @@ pub enum ReadRequest { /// Optionally estimate the network speed at the time when a certain block was found height: Option, }, + + #[cfg(feature = "getblocktemplate-rpcs")] + /// Performs contextual validation of the given block, but does not commit it to the state. + /// + /// It is the caller's responsibility to perform semantic validation. + /// (The caller does not need to check proof of work for block proposals.) + /// + /// Returns [`ReadResponse::ValidBlockProposal`] when successful, or an error if + /// the block fails contextual validation. + CheckBlockProposalValidity(PreparedBlock), } impl ReadRequest { @@ -819,6 +838,8 @@ impl ReadRequest { ReadRequest::ChainInfo => "chain_info", #[cfg(feature = "getblocktemplate-rpcs")] ReadRequest::SolutionRate { .. } => "solution_rate", + #[cfg(feature = "getblocktemplate-rpcs")] + ReadRequest::CheckBlockProposalValidity(_) => "check_block_proposal_validity", } } @@ -866,6 +887,11 @@ impl TryFrom for ReadRequest { Err("ReadService does not write blocks") } + #[cfg(feature = "getblocktemplate-rpcs")] + Request::CheckBlockProposalValidity(prepared) => { + Ok(ReadRequest::CheckBlockProposalValidity(prepared)) + } + Request::AwaitUtxo(_) => Err("ReadService does not track pending UTXOs. \ Manually convert the request to ReadRequest::AnyChainUtxo, \ and handle pending UTXOs"), diff --git a/zebra-state/src/response.rs b/zebra-state/src/response.rs index 74386de9d90..1d529c2b991 100644 --- a/zebra-state/src/response.rs +++ b/zebra-state/src/response.rs @@ -59,6 +59,10 @@ pub enum Response { /// /// Does not check transparent UTXO inputs ValidBestChainTipNullifiersAndAnchors, + + #[cfg(feature = "getblocktemplate-rpcs")] + /// Response to [`Request::CheckBlockProposalValidity`](crate::Request::CheckBlockProposalValidity) + ValidBlockProposal, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -137,6 +141,10 @@ pub enum ReadResponse { #[cfg(feature = "getblocktemplate-rpcs")] /// Response to [`ReadRequest::SolutionRate`](crate::ReadRequest::SolutionRate) SolutionRate(Option), + + #[cfg(feature = "getblocktemplate-rpcs")] + /// Response to [`ReadRequest::CheckBlockProposalValidity`](crate::ReadRequest::CheckBlockProposalValidity) + ValidBlockProposal, } /// A structure with the information needed from the state to build a `getblocktemplate` RPC response. @@ -215,6 +223,9 @@ impl TryFrom for Response { Err("there is no corresponding Response for this ReadResponse") } + #[cfg(feature = "getblocktemplate-rpcs")] + ReadResponse::ValidBlockProposal => Ok(Response::ValidBlockProposal), + #[cfg(feature = "getblocktemplate-rpcs")] ReadResponse::BlockHash(_) => { Err("there is no corresponding Response for this ReadResponse") diff --git a/zebra-state/src/service.rs b/zebra-state/src/service.rs index b97f0652e5f..f155e0cb203 100644 --- a/zebra-state/src/service.rs +++ b/zebra-state/src/service.rs @@ -1047,6 +1047,24 @@ impl Service for StateService { } .boxed() } + + #[cfg(feature = "getblocktemplate-rpcs")] + Request::CheckBlockProposalValidity(_) => { + // Redirect the request to the concurrent ReadStateService + let read_service = self.read_service.clone(); + + async move { + let req = req + .try_into() + .expect("ReadRequest conversion should not fail"); + + let rsp = read_service.oneshot(req).await?; + let rsp = rsp.try_into().expect("Response conversion should not fail"); + + Ok(rsp) + } + .boxed() + } } } } @@ -1082,7 +1100,7 @@ impl Service for ReadStateService { Poll::Ready(Ok(())) } - #[instrument(name = "read_state", skip(self))] + #[instrument(name = "read_state", skip(self, req))] fn call(&mut self, req: ReadRequest) -> Self::Future { req.count_metric(); @@ -1688,6 +1706,58 @@ impl Service for ReadStateService { .map(|join_result| join_result.expect("panic in ReadRequest::ChainInfo")) .boxed() } + + #[cfg(feature = "getblocktemplate-rpcs")] + ReadRequest::CheckBlockProposalValidity(prepared) => { + let timer = CodeTimer::start(); + + let state = self.clone(); + + // # Performance + // + // Allow other async tasks to make progress while concurrently reading blocks from disk. + let span = Span::current(); + tokio::task::spawn_blocking(move || { + span.in_scope(move || { + tracing::info!("attempting to validate and commit block proposal onto a cloned non-finalized state"); + let mut latest_non_finalized_state = state.latest_non_finalized_state(); + + // The previous block of a valid proposal must be on the best chain tip. + let Some((_best_tip_height, best_tip_hash)) = read::best_tip(&latest_non_finalized_state, &state.db) else { + return Err("state is empty: wait for Zebra to sync before submitting a proposal".into()); + }; + + if prepared.block.header.previous_block_hash != best_tip_hash { + return Err("proposal is not based on the current best chain tip: previous block hash must be the best chain tip".into()); + } + + // This clone of the non-finalized state is dropped when this closure returns. + // The non-finalized state that's used in the rest of the state (including finalizing + // blocks into the db) is not mutated here. + // + // TODO: Convert `CommitBlockError` to a new `ValidateProposalError`? + latest_non_finalized_state.should_count_metrics = false; + write::validate_and_commit_non_finalized( + &state.db, + &mut latest_non_finalized_state, + prepared, + )?; + + // The work is done in the future. + timer.finish( + module_path!(), + line!(), + "ReadRequest::CheckBlockProposalValidity", + ); + + Ok(ReadResponse::ValidBlockProposal) + }) + }) + .map(|join_result| { + join_result.expect("panic in ReadRequest::CheckBlockProposalValidity") + }) + .boxed() + } } } } diff --git a/zebra-state/src/service/check.rs b/zebra-state/src/service/check.rs index 24db7846ead..d8e747964e4 100644 --- a/zebra-state/src/service/check.rs +++ b/zebra-state/src/service/check.rs @@ -14,7 +14,7 @@ use zebra_chain::{ use crate::{ service::{ block_iter::any_ancestor_blocks, check::difficulty::POW_ADJUSTMENT_BLOCK_SPAN, - finalized_state::FinalizedState, non_finalized_state::NonFinalizedState, + finalized_state::ZebraDb, non_finalized_state::NonFinalizedState, }, BoxError, PreparedBlock, ValidateContextError, }; @@ -365,25 +365,25 @@ where /// /// Additional contextual validity checks are performed by the non-finalized [`Chain`]. pub(crate) fn initial_contextual_validity( - finalized_state: &FinalizedState, + finalized_state: &ZebraDb, non_finalized_state: &NonFinalizedState, prepared: &PreparedBlock, ) -> Result<(), ValidateContextError> { let relevant_chain = any_ancestor_blocks( non_finalized_state, - &finalized_state.db, + finalized_state, prepared.block.header.previous_block_hash, ); // Security: check proof of work before any other checks check::block_is_valid_for_recent_chain( prepared, - finalized_state.network(), - finalized_state.db.finalized_tip_height(), + non_finalized_state.network, + finalized_state.finalized_tip_height(), relevant_chain, )?; - check::nullifier::no_duplicates_in_finalized_chain(prepared, &finalized_state.db)?; + check::nullifier::no_duplicates_in_finalized_chain(prepared, finalized_state)?; Ok(()) } diff --git a/zebra-state/src/service/check/tests/anchors.rs b/zebra-state/src/service/check/tests/anchors.rs index f79c1ff896e..11564201e12 100644 --- a/zebra-state/src/service/check/tests/anchors.rs +++ b/zebra-state/src/service/check/tests/anchors.rs @@ -81,10 +81,12 @@ fn check_sprout_anchors() { // Validate and commit [`block_1`]. This will add an anchor referencing the // empty note commitment tree to the state. - assert!( - validate_and_commit_non_finalized(&finalized_state, &mut non_finalized_state, block_1) - .is_ok() - ); + assert!(validate_and_commit_non_finalized( + &finalized_state.db, + &mut non_finalized_state, + block_1 + ) + .is_ok()); let check_unmined_tx_anchors_result = unmined_txs.iter().try_for_each(|unmined_tx| { tx_anchors_refer_to_final_treestates( @@ -98,7 +100,7 @@ fn check_sprout_anchors() { // Validate and commit [`block_2`]. This will also check the anchors. assert_eq!( - validate_and_commit_non_finalized(&finalized_state, &mut non_finalized_state, block_2), + validate_and_commit_non_finalized(&finalized_state.db, &mut non_finalized_state, block_2), Ok(()) ); } @@ -288,10 +290,12 @@ fn check_sapling_anchors() { Err(ValidateContextError::UnknownSaplingAnchor { .. }) )); - assert!( - validate_and_commit_non_finalized(&finalized_state, &mut non_finalized_state, block1) - .is_ok() - ); + assert!(validate_and_commit_non_finalized( + &finalized_state.db, + &mut non_finalized_state, + block1 + ) + .is_ok()); let check_unmined_tx_anchors_result = unmined_txs.iter().try_for_each(|unmined_tx| { tx_anchors_refer_to_final_treestates( @@ -304,7 +308,7 @@ fn check_sapling_anchors() { assert!(check_unmined_tx_anchors_result.is_ok()); assert_eq!( - validate_and_commit_non_finalized(&finalized_state, &mut non_finalized_state, block2), + validate_and_commit_non_finalized(&finalized_state.db, &mut non_finalized_state, block2), Ok(()) ); } diff --git a/zebra-state/src/service/check/tests/nullifier.rs b/zebra-state/src/service/check/tests/nullifier.rs index 3b1e43d343d..904426d4256 100644 --- a/zebra-state/src/service/check/tests/nullifier.rs +++ b/zebra-state/src/service/check/tests/nullifier.rs @@ -102,7 +102,7 @@ proptest! { } else { let block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( - &finalized_state, + &finalized_state.db, &mut non_finalized_state, block1.clone() ); @@ -156,7 +156,7 @@ proptest! { let block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( - &finalized_state, + &finalized_state.db, &mut non_finalized_state, block1 ); @@ -217,7 +217,7 @@ proptest! { let block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( - &finalized_state, + &finalized_state.db, &mut non_finalized_state, block1 ); @@ -278,8 +278,10 @@ proptest! { let block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( - &finalized_state, - &mut non_finalized_state, block1); + &finalized_state.db, + &mut non_finalized_state, + block1 + ); prop_assert_eq!( commit_result, @@ -364,7 +366,7 @@ proptest! { } else { let block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( - &finalized_state, + &finalized_state.db, &mut non_finalized_state, block1.clone() ); @@ -383,7 +385,7 @@ proptest! { let block2 = Arc::new(block2).prepare(); let commit_result = validate_and_commit_non_finalized( - &finalized_state, + &finalized_state.db, &mut non_finalized_state, block2 ); @@ -459,8 +461,10 @@ proptest! { } else { let block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( - &finalized_state, - &mut non_finalized_state, block1.clone()); + &finalized_state.db, + &mut non_finalized_state, + block1.clone() + ); prop_assert_eq!(commit_result, Ok(())); prop_assert_eq!(Some((Height(1), block1.hash)), read::best_tip(&non_finalized_state, &finalized_state.db)); @@ -506,8 +510,10 @@ proptest! { let block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( - &finalized_state, - &mut non_finalized_state, block1); + &finalized_state.db, + &mut non_finalized_state, + block1 + ); prop_assert_eq!( commit_result, @@ -560,7 +566,7 @@ proptest! { let block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( - &finalized_state, + &finalized_state.db, &mut non_finalized_state, block1 ); @@ -639,8 +645,10 @@ proptest! { } else { let block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( - &finalized_state, - &mut non_finalized_state, block1.clone()); + &finalized_state.db, + &mut non_finalized_state, + block1.clone() + ); prop_assert_eq!(commit_result, Ok(())); prop_assert_eq!(Some((Height(1), block1.hash)), read::best_tip(&non_finalized_state, &finalized_state.db)); @@ -655,8 +663,10 @@ proptest! { let block2 = Arc::new(block2).prepare(); let commit_result = validate_and_commit_non_finalized( - &finalized_state, - &mut non_finalized_state, block2); + &finalized_state.db, + &mut non_finalized_state, + block2 + ); prop_assert_eq!( commit_result, @@ -731,8 +741,10 @@ proptest! { } else { let block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( - &finalized_state, - &mut non_finalized_state, block1.clone()); + &finalized_state.db, + &mut non_finalized_state, + block1.clone() + ); prop_assert_eq!(commit_result, Ok(())); prop_assert_eq!(Some((Height(1), block1.hash)), read::best_tip(&non_finalized_state, &finalized_state.db)); @@ -779,8 +791,10 @@ proptest! { let block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( - &finalized_state, - &mut non_finalized_state, block1); + &finalized_state.db, + &mut non_finalized_state, + block1 + ); prop_assert_eq!( commit_result, @@ -837,8 +851,10 @@ proptest! { let block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( - &finalized_state, - &mut non_finalized_state, block1); + &finalized_state.db, + &mut non_finalized_state, + block1 + ); prop_assert_eq!( commit_result, @@ -918,8 +934,10 @@ proptest! { } else { let block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( - &finalized_state, - &mut non_finalized_state, block1.clone()); + &finalized_state.db, + &mut non_finalized_state, + block1.clone() + ); prop_assert_eq!(commit_result, Ok(())); prop_assert_eq!(Some((Height(1), block1.hash)), read::best_tip(&non_finalized_state, &finalized_state.db)); @@ -933,8 +951,10 @@ proptest! { let block2 = Arc::new(block2).prepare(); let commit_result = validate_and_commit_non_finalized( - &finalized_state, - &mut non_finalized_state, block2); + &finalized_state.db, + &mut non_finalized_state, + block2 + ); prop_assert_eq!( commit_result, diff --git a/zebra-state/src/service/check/tests/utxo.rs b/zebra-state/src/service/check/tests/utxo.rs index efb70c66504..6d14b8a13dd 100644 --- a/zebra-state/src/service/check/tests/utxo.rs +++ b/zebra-state/src/service/check/tests/utxo.rs @@ -194,8 +194,10 @@ proptest! { } else { let block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( - &finalized_state, - &mut non_finalized_state, block1.clone()); + &finalized_state.db, + &mut non_finalized_state, + block1.clone() + ); // the block was committed prop_assert_eq!(commit_result, Ok(())); @@ -279,8 +281,10 @@ proptest! { } else { let block2 = Arc::new(block2).prepare(); let commit_result = validate_and_commit_non_finalized( - &finalized_state, - &mut non_finalized_state, block2.clone()); + &finalized_state.db, + &mut non_finalized_state, + block2.clone() + ); // the block was committed prop_assert_eq!(commit_result, Ok(())); @@ -357,8 +361,10 @@ proptest! { let block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( - &finalized_state, - &mut non_finalized_state, block1); + &finalized_state.db, + &mut non_finalized_state, + block1 + ); // the block was rejected prop_assert_eq!( @@ -419,8 +425,10 @@ proptest! { let block2 = Arc::new(block2).prepare(); let commit_result = validate_and_commit_non_finalized( - &finalized_state, - &mut non_finalized_state, block2); + &finalized_state.db, + &mut non_finalized_state, + block2 + ); // the block was rejected prop_assert_eq!( @@ -503,8 +511,10 @@ proptest! { let block2 = Arc::new(block2).prepare(); let commit_result = validate_and_commit_non_finalized( - &finalized_state, - &mut non_finalized_state, block2); + &finalized_state.db, + &mut non_finalized_state, + block2 + ); // the block was rejected prop_assert_eq!( @@ -615,8 +625,10 @@ proptest! { } else { let block2 = block2.clone().prepare(); let commit_result = validate_and_commit_non_finalized( - &finalized_state, - &mut non_finalized_state, block2.clone()); + &finalized_state.db, + &mut non_finalized_state, + block2.clone() + ); // the block was committed prop_assert_eq!(commit_result, Ok(())); @@ -651,8 +663,10 @@ proptest! { let block3 = Arc::new(block3).prepare(); let commit_result = validate_and_commit_non_finalized( - &finalized_state, - &mut non_finalized_state, block3); + &finalized_state.db, + &mut non_finalized_state, + block3 + ); // the block was rejected if use_finalized_state_spend { @@ -725,8 +739,10 @@ proptest! { let block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( - &finalized_state, - &mut non_finalized_state, block1); + &finalized_state.db, + &mut non_finalized_state, + block1 + ); // the block was rejected prop_assert_eq!( @@ -790,8 +806,10 @@ proptest! { let block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( - &finalized_state, - &mut non_finalized_state, block1); + &finalized_state.db, + &mut non_finalized_state, + block1 + ); // the block was rejected prop_assert_eq!( @@ -885,7 +903,7 @@ fn new_state_with_mainnet_transparent_data( } else { let block1 = block1.clone().prepare(); let commit_result = validate_and_commit_non_finalized( - &finalized_state, + &finalized_state.db, &mut non_finalized_state, block1.clone(), ); diff --git a/zebra-state/src/service/non_finalized_state.rs b/zebra-state/src/service/non_finalized_state.rs index 74fb3d7ff25..3e343da7405 100644 --- a/zebra-state/src/service/non_finalized_state.rs +++ b/zebra-state/src/service/non_finalized_state.rs @@ -44,9 +44,14 @@ pub struct NonFinalizedState { pub chain_set: BTreeSet>, /// The configured Zcash network. - // - // Note: this field is currently unused, but it's useful for debugging. pub network: Network, + + #[cfg(feature = "getblocktemplate-rpcs")] + /// Configures the non-finalized state to count metrics. + /// + /// Used for skipping metrics counting when testing block proposals + /// with a commit to a cloned non-finalized state. + pub should_count_metrics: bool, } impl NonFinalizedState { @@ -55,6 +60,8 @@ impl NonFinalizedState { NonFinalizedState { chain_set: Default::default(), network, + #[cfg(feature = "getblocktemplate-rpcs")] + should_count_metrics: true, } } @@ -494,6 +501,11 @@ impl NonFinalizedState { /// Update the metrics after `block` is committed fn update_metrics_for_committed_block(&self, height: block::Height, hash: block::Hash) { + #[cfg(feature = "getblocktemplate-rpcs")] + if !self.should_count_metrics { + return; + } + metrics::counter!("state.memory.committed.block.count", 1); metrics::gauge!("state.memory.committed.block.height", height.0 as f64); @@ -517,6 +529,11 @@ impl NonFinalizedState { /// Update the metrics after `self.chain_set` is modified fn update_metrics_for_chains(&self) { + #[cfg(feature = "getblocktemplate-rpcs")] + if !self.should_count_metrics { + return; + } + metrics::gauge!("state.memory.chain.count", self.chain_set.len() as f64); metrics::gauge!( "state.memory.best.chain.length", diff --git a/zebra-state/src/service/write.rs b/zebra-state/src/service/write.rs index 5ba92218431..8f33af7b6d1 100644 --- a/zebra-state/src/service/write.rs +++ b/zebra-state/src/service/write.rs @@ -12,7 +12,7 @@ use crate::{ constants::MAX_BLOCK_REORG_HEIGHT, service::{ check, - finalized_state::FinalizedState, + finalized_state::{FinalizedState, ZebraDb}, non_finalized_state::NonFinalizedState, queued_blocks::{QueuedFinalized, QueuedNonFinalized}, BoxError, ChainTipBlock, ChainTipSender, CloneError, @@ -36,17 +36,17 @@ const PARENT_ERROR_MAP_LIMIT: usize = MAX_BLOCK_REORG_HEIGHT as usize * 2; /// non-finalized state if it is contextually valid. #[tracing::instrument(level = "debug", skip(prepared), fields(height = ?prepared.height, hash = %prepared.hash))] pub(crate) fn validate_and_commit_non_finalized( - finalized_state: &FinalizedState, + finalized_state: &ZebraDb, non_finalized_state: &mut NonFinalizedState, prepared: PreparedBlock, ) -> Result<(), CommitBlockError> { check::initial_contextual_validity(finalized_state, non_finalized_state, &prepared)?; let parent_hash = prepared.block.header.previous_block_hash; - if finalized_state.db.finalized_tip_hash() == parent_hash { - non_finalized_state.commit_new_chain(prepared, &finalized_state.db)?; + if finalized_state.finalized_tip_hash() == parent_hash { + non_finalized_state.commit_new_chain(prepared, finalized_state)?; } else { - non_finalized_state.commit_block(prepared, &finalized_state.db)?; + non_finalized_state.commit_block(prepared, finalized_state)?; } Ok(()) @@ -205,7 +205,7 @@ pub fn write_blocks_from_channels( } else { tracing::trace!(?child_hash, "validating queued child"); result = validate_and_commit_non_finalized( - &finalized_state, + &finalized_state.db, &mut non_finalized_state, queued_child, ) diff --git a/zebrad/src/components/inbound.rs b/zebrad/src/components/inbound.rs index 36a121e6dd1..57a7b4f3b1d 100644 --- a/zebrad/src/components/inbound.rs +++ b/zebrad/src/components/inbound.rs @@ -24,10 +24,7 @@ use tower::{buffer::Buffer, timeout::Timeout, util::BoxService, Service, Service use zebra_network as zn; use zebra_state as zs; -use zebra_chain::{ - block::{self, Block}, - transaction::UnminedTxId, -}; +use zebra_chain::{block, transaction::UnminedTxId}; use zebra_consensus::chain::VerifyChainError; use zebra_network::{ constants::{ADDR_RESPONSE_LIMIT_DENOMINATOR, MAX_ADDRS_IN_MESSAGE}, @@ -53,7 +50,10 @@ type BlockDownloadPeerSet = Buffer, zn::Request>; type State = Buffer, zs::Request>; type Mempool = Buffer, mempool::Request>; -type BlockVerifier = Buffer, block::Hash, VerifyChainError>, Arc>; +type BlockVerifier = Buffer< + BoxService, + zebra_consensus::Request, +>; type GossipedBlockDownloads = BlockDownloads, Timeout, State>; diff --git a/zebrad/src/components/inbound/downloads.rs b/zebrad/src/components/inbound/downloads.rs index 250b904831f..130d324ba45 100644 --- a/zebrad/src/components/inbound/downloads.rs +++ b/zebrad/src/components/inbound/downloads.rs @@ -4,7 +4,6 @@ use std::{ collections::HashMap, convert::TryFrom, pin::Pin, - sync::Arc, task::{Context, Poll}, }; @@ -18,10 +17,7 @@ use tokio::{sync::oneshot, task::JoinHandle}; use tower::{Service, ServiceExt}; use tracing_futures::Instrument; -use zebra_chain::{ - block::{self, Block}, - chain_tip::ChainTip, -}; +use zebra_chain::{block, chain_tip::ChainTip}; use zebra_network as zn; use zebra_state as zs; @@ -77,7 +73,10 @@ pub struct Downloads where ZN: Service + Send + Clone + 'static, ZN::Future: Send, - ZV: Service, Response = block::Hash, Error = BoxError> + Send + Clone + 'static, + ZV: Service + + Send + + Clone + + 'static, ZV::Future: Send, ZS: Service + Send + Clone + 'static, ZS::Future: Send, @@ -117,7 +116,10 @@ impl Stream for Downloads where ZN: Service + Send + Clone + 'static, ZN::Future: Send, - ZV: Service, Response = block::Hash, Error = BoxError> + Send + Clone + 'static, + ZV: Service + + Send + + Clone + + 'static, ZV::Future: Send, ZS: Service + Send + Clone + 'static, ZS::Future: Send, @@ -160,7 +162,10 @@ impl Downloads where ZN: Service + Send + Clone + 'static, ZN::Future: Send, - ZV: Service, Response = block::Hash, Error = BoxError> + Send + Clone + 'static, + ZV: Service + + Send + + Clone + + 'static, ZV::Future: Send, ZS: Service + Send + Clone + 'static, ZS::Future: Send, @@ -338,7 +343,7 @@ where } verifier - .oneshot(block) + .oneshot(zebra_consensus::Request::Commit(block)) .await .map(|hash| (hash, block_height)) } diff --git a/zebrad/src/components/inbound/tests/real_peer_set.rs b/zebrad/src/components/inbound/tests/real_peer_set.rs index e1611fa90ff..c610f1a1f71 100644 --- a/zebrad/src/components/inbound/tests/real_peer_set.rs +++ b/zebrad/src/components/inbound/tests/real_peer_set.rs @@ -1,6 +1,6 @@ //! Inbound service tests with a real peer set. -use std::{iter, net::SocketAddr, sync::Arc}; +use std::{iter, net::SocketAddr}; use futures::FutureExt; use indexmap::IndexSet; @@ -13,7 +13,7 @@ use tower::{ }; use zebra_chain::{ - block::{self, Block, Height}, + block::{self, Height}, parameters::Network, serialization::ZcashDeserializeInto, transaction::{AuthDigest, Hash as TxHash, Transaction, UnminedTx, UnminedTxId, WtxId}, @@ -603,7 +603,7 @@ async fn setup( Buffer, mempool::Request>, Buffer, zebra_state::Request>, // mocked services - MockService, block::Hash, PanicAssertion, VerifyChainError>, + MockService, MockService, // real tasks JoinHandle>, diff --git a/zebrad/src/components/sync.rs b/zebrad/src/components/sync.rs index 1a502945bc1..33f42622559 100644 --- a/zebrad/src/components/sync.rs +++ b/zebrad/src/components/sync.rs @@ -2,7 +2,7 @@ //! //! It is used when Zebra is a long way behind the current chain tip. -use std::{cmp::max, collections::HashSet, pin::Pin, sync::Arc, task::Poll, time::Duration}; +use std::{cmp::max, collections::HashSet, pin::Pin, task::Poll, time::Duration}; use color_eyre::eyre::{eyre, Report}; use futures::stream::{FuturesUnordered, StreamExt}; @@ -15,7 +15,7 @@ use tower::{ }; use zebra_chain::{ - block::{self, Block, Height}, + block::{self, Height}, chain_tip::ChainTip, parameters::genesis_hash, }; @@ -300,7 +300,7 @@ where + Clone + 'static, ZS::Future: Send, - ZV: Service, Response = block::Hash, Error = BoxError> + ZV: Service + Send + Sync + Clone @@ -381,7 +381,7 @@ where + Clone + 'static, ZS::Future: Send, - ZV: Service, Response = block::Hash, Error = BoxError> + ZV: Service + Send + Sync + Clone diff --git a/zebrad/src/components/sync/downloads.rs b/zebrad/src/components/sync/downloads.rs index 10c4bda8f31..4b6b5f00256 100644 --- a/zebrad/src/components/sync/downloads.rs +++ b/zebrad/src/components/sync/downloads.rs @@ -24,7 +24,7 @@ use tower::{hedge, Service, ServiceExt}; use tracing_futures::Instrument; use zebra_chain::{ - block::{self, Block, Height}, + block::{self, Height}, chain_tip::ChainTip, }; use zebra_network as zn; @@ -163,7 +163,7 @@ pub struct Downloads where ZN: Service + Send + Sync + 'static, ZN::Future: Send, - ZV: Service, Response = block::Hash, Error = BoxError> + ZV: Service + Send + Sync + Clone @@ -217,7 +217,7 @@ impl Stream for Downloads where ZN: Service + Send + Sync + 'static, ZN::Future: Send, - ZV: Service, Response = block::Hash, Error = BoxError> + ZV: Service + Send + Sync + Clone @@ -264,7 +264,7 @@ impl Downloads where ZN: Service + Send + Sync + 'static, ZN::Future: Send, - ZV: Service, Response = block::Hash, Error = BoxError> + ZV: Service + Send + Sync + Clone @@ -516,7 +516,7 @@ where // Verify the block. let mut rsp = verifier .map_err(|error| BlockDownloadVerifyError::VerifierServiceError { error })? - .call(block).boxed(); + .call(zebra_consensus::Request::Commit(block)).boxed(); // Add a shorter timeout to workaround a known bug (#5125) let short_timeout_max = (max_checkpoint_height + FINAL_CHECKPOINT_BLOCK_VERIFY_TIMEOUT_LIMIT).expect("checkpoint block height is in valid range"); diff --git a/zebrad/src/components/sync/tests/vectors.rs b/zebrad/src/components/sync/tests/vectors.rs index 4acc67760c7..468d74958ab 100644 --- a/zebrad/src/components/sync/tests/vectors.rs +++ b/zebrad/src/components/sync/tests/vectors.rs @@ -89,7 +89,7 @@ async fn sync_blocks_ok() -> Result<(), crate::BoxError> { .respond(zn::Response::Blocks(vec![Available(block0.clone())])); chain_verifier - .expect_request(block0) + .expect_request(zebra_consensus::Request::Commit(block0)) .await .respond(block0_hash); @@ -175,9 +175,9 @@ async fn sync_blocks_ok() -> Result<(), crate::BoxError> { for _ in 1..=2 { chain_verifier - .expect_request_that(|req| remaining_blocks.remove(&req.hash()).is_some()) + .expect_request_that(|req| remaining_blocks.remove(&req.block().hash()).is_some()) .await - .respond_with(|req| req.hash()); + .respond_with(|req| req.block().hash()); } assert_eq!( remaining_blocks, @@ -239,9 +239,9 @@ async fn sync_blocks_ok() -> Result<(), crate::BoxError> { for _ in 3..=4 { chain_verifier - .expect_request_that(|req| remaining_blocks.remove(&req.hash()).is_some()) + .expect_request_that(|req| remaining_blocks.remove(&req.block().hash()).is_some()) .await - .respond_with(|req| req.hash()); + .respond_with(|req| req.block().hash()); } assert_eq!( remaining_blocks, @@ -316,7 +316,7 @@ async fn sync_blocks_duplicate_hashes_ok() -> Result<(), crate::BoxError> { .respond(zn::Response::Blocks(vec![Available(block0.clone())])); chain_verifier - .expect_request(block0) + .expect_request(zebra_consensus::Request::Commit(block0)) .await .respond(block0_hash); @@ -404,9 +404,9 @@ async fn sync_blocks_duplicate_hashes_ok() -> Result<(), crate::BoxError> { for _ in 1..=2 { chain_verifier - .expect_request_that(|req| remaining_blocks.remove(&req.hash()).is_some()) + .expect_request_that(|req| remaining_blocks.remove(&req.block().hash()).is_some()) .await - .respond_with(|req| req.hash()); + .respond_with(|req| req.block().hash()); } assert_eq!( remaining_blocks, @@ -470,9 +470,9 @@ async fn sync_blocks_duplicate_hashes_ok() -> Result<(), crate::BoxError> { for _ in 3..=4 { chain_verifier - .expect_request_that(|req| remaining_blocks.remove(&req.hash()).is_some()) + .expect_request_that(|req| remaining_blocks.remove(&req.block().hash()).is_some()) .await - .respond_with(|req| req.hash()); + .respond_with(|req| req.block().hash()); } assert_eq!( remaining_blocks, @@ -598,7 +598,7 @@ async fn sync_block_too_high_obtain_tips() -> Result<(), crate::BoxError> { .respond(zn::Response::Blocks(vec![Available(block0.clone())])); chain_verifier - .expect_request(block0) + .expect_request(zebra_consensus::Request::Commit(block0)) .await .respond(block0_hash); @@ -759,7 +759,7 @@ async fn sync_block_too_high_extend_tips() -> Result<(), crate::BoxError> { .respond(zn::Response::Blocks(vec![Available(block0.clone())])); chain_verifier - .expect_request(block0) + .expect_request(zebra_consensus::Request::Commit(block0)) .await .respond(block0_hash); @@ -845,9 +845,9 @@ async fn sync_block_too_high_extend_tips() -> Result<(), crate::BoxError> { for _ in 1..=2 { chain_verifier - .expect_request_that(|req| remaining_blocks.remove(&req.hash()).is_some()) + .expect_request_that(|req| remaining_blocks.remove(&req.block().hash()).is_some()) .await - .respond_with(|req| req.hash()); + .respond_with(|req| req.block().hash()); } assert_eq!( remaining_blocks, @@ -927,7 +927,7 @@ fn setup() -> ( impl Future> + Send, SyncStatus, // ChainVerifier - MockService, block::Hash, PanicAssertion>, + MockService, // PeerSet MockService, // StateService