From e20cf957e3cc35bf061ac75c4cf19d6b07b4673c Mon Sep 17 00:00:00 2001 From: teor Date: Sat, 28 Jan 2023 07:46:51 +1000 Subject: [PATCH] fix(consensus): Verify the lock times of mempool transactions (#6027) * Implement the BestChainNextMedianTimePast state request * Verify the lock times of mempool transactions * Document that the mempool already handles lock time rejections correctly * Fix existing tests * Add new mempool lock time success and failure tests --- zebra-consensus/src/error.rs | 5 + zebra-consensus/src/transaction.rs | 64 ++++- zebra-consensus/src/transaction/tests.rs | 319 ++++++++++++++++++++++- zebra-state/src/request.rs | 13 + zebra-state/src/response.rs | 12 +- zebra-state/src/service.rs | 34 ++- zebra-state/src/service/read.rs | 16 +- zebra-state/src/service/read/find.rs | 112 +++++++- zebrad/src/components/mempool.rs | 6 + zebrad/src/components/mempool/storage.rs | 2 +- 10 files changed, 554 insertions(+), 29 deletions(-) diff --git a/zebra-consensus/src/error.rs b/zebra-consensus/src/error.rs index 7b1cd2531..e33723754 100644 --- a/zebra-consensus/src/error.rs +++ b/zebra-consensus/src/error.rs @@ -186,6 +186,11 @@ pub enum TransactionError { #[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))] // This error variant is at least 128 bytes ValidateNullifiersAndAnchorsError(Box), + + #[error("could not validate mempool transaction lock time on best chain")] + #[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))] + // TODO: turn this into a typed error + ValidateMempoolLockTimeError(String), } impl From for TransactionError { diff --git a/zebra-consensus/src/transaction.rs b/zebra-consensus/src/transaction.rs index f4b1f62b1..f0f17162f 100644 --- a/zebra-consensus/src/transaction.rs +++ b/zebra-consensus/src/transaction.rs @@ -23,6 +23,7 @@ use zebra_chain::{ parameters::{Network, NetworkUpgrade}, primitives::Groth16Proof, sapling, + serialization::DateTime32, transaction::{ self, HashType, SigHash, Transaction, UnminedTx, UnminedTxId, VerifiedUnminedTx, }, @@ -306,17 +307,15 @@ where async move { tracing::trace!(?tx_id, ?req, "got tx verify request"); - // Do basic checks first - if let Some(block_time) = req.block_time() { - check::lock_time_has_passed(&tx, req.height(), block_time)?; - } - + // Do quick checks first check::has_inputs_and_outputs(&tx)?; check::has_enough_orchard_flags(&tx)?; + // Validate the coinbase input consensus rules if req.is_mempool() && tx.is_coinbase() { return Err(TransactionError::CoinbaseInMempool); } + if tx.is_coinbase() { check::coinbase_tx_no_prevout_joinsplit_spend(&tx)?; } else if !tx.is_valid_non_coinbase() { @@ -345,6 +344,34 @@ where tracing::trace!(?tx_id, "passed quick checks"); + if let Some(block_time) = req.block_time() { + check::lock_time_has_passed(&tx, req.height(), block_time)?; + } else { + // This state query is much faster than loading UTXOs from the database, + // so it doesn't need to be executed in parallel + let state = state.clone(); + let next_median_time_past = Self::mempool_best_chain_next_median_time_past(state).await?; + + // # Consensus + // + // > the nTime field MUST represent a time strictly greater than the median of the + // > timestamps of the past PoWMedianBlockSpan blocks. + // + // + // > The transaction can be added to any block whose block time is greater than the locktime. + // + // + // If the transaction's lock time is less than the median-time-past, + // it will always be less than the next block's time, + // because the next block's time is strictly greater than the median-time-past. + // + // This is the rule implemented by `zcashd`'s mempool: + // + // + // This consensus check makes sure Zebra produces valid block templates. + check::lock_time_has_passed(&tx, req.height(), next_median_time_past.to_chrono())?; + } + // "The consensus rules applied to valueBalance, vShieldedOutput, and bindingSig // in non-coinbase transactions MUST also be applied to coinbase transactions." // @@ -356,7 +383,7 @@ where // https://zips.z.cash/zip-0213#specification // Load spent UTXOs from state. - // TODO: Make this a method of `Request` and replace `tx.clone()` with `self.transaction()`? + // The UTXOs are required for almost all the async checks. let load_spent_utxos_fut = Self::spent_utxos(tx.clone(), req.known_utxos(), req.is_mempool(), state.clone()); let (spent_utxos, spent_outputs) = load_spent_utxos_fut.await?; @@ -426,7 +453,7 @@ where // Calculate the fee only for non-coinbase transactions. let mut miner_fee = None; if !tx.is_coinbase() { - // TODO: deduplicate this code with remaining_transaction_value (#TODO: open ticket) + // TODO: deduplicate this code with remaining_transaction_value()? miner_fee = match value_balance { Ok(vb) => match vb.remaining_transaction_value() { Ok(tx_rtv) => Some(tx_rtv), @@ -471,7 +498,28 @@ where ZS: Service + Send + Clone + 'static, ZS::Future: Send + 'static, { - /// Get the UTXOs that are being spent by the given transaction. + /// Fetches the median-time-past of the *next* block after the best state tip. + /// + /// This is used to verify that the lock times of mempool transactions + /// can be included in any valid next block. + async fn mempool_best_chain_next_median_time_past( + state: Timeout, + ) -> Result { + let query = state + .clone() + .oneshot(zs::Request::BestChainNextMedianTimePast); + + if let zebra_state::Response::BestChainNextMedianTimePast(median_time_past) = query + .await + .map_err(|e| TransactionError::ValidateMempoolLockTimeError(e.to_string()))? + { + Ok(median_time_past) + } else { + unreachable!("Request::BestChainNextMedianTimePast always responds with BestChainNextMedianTimePast") + } + } + + /// Wait for the UTXOs that are being spent by the given transaction. /// /// `known_utxos` are additional UTXOs known at the time of validation (i.e. /// from previous transactions in the block). diff --git a/zebra-consensus/src/transaction/tests.rs b/zebra-consensus/src/transaction/tests.rs index b819a7291..256417187 100644 --- a/zebra-consensus/src/transaction/tests.rs +++ b/zebra-consensus/src/transaction/tests.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, sync::Arc}; -use chrono::{DateTime, Utc}; +use chrono::{DateTime, TimeZone, Utc}; use color_eyre::eyre::Report; use halo2::pasta::{group::ff::PrimeField, pallas}; use tower::{service_fn, ServiceExt}; @@ -14,7 +14,7 @@ use zebra_chain::{ parameters::{Network, NetworkUpgrade}, primitives::{ed25519, x25519, Groth16Proof}, sapling, - serialization::{ZcashDeserialize, ZcashDeserializeInto}, + serialization::{DateTime32, ZcashDeserialize, ZcashDeserializeInto}, sprout, transaction::{ arbitrary::{ @@ -195,10 +195,18 @@ async fn mempool_request_with_missing_input_is_rejected() { }; tokio::spawn(async move { + state + .expect_request(zebra_state::Request::BestChainNextMedianTimePast) + .await + .expect("verifier should call mock state service with correct request") + .respond(zebra_state::Response::BestChainNextMedianTimePast( + DateTime32::MAX, + )); + state .expect_request(zebra_state::Request::UnspentBestChainUtxo(input_outpoint)) .await - .expect("verifier should call mock state service") + .expect("verifier should call mock state service with correct request") .respond(zebra_state::Response::UnspentBestChainUtxo(None)); state @@ -209,7 +217,7 @@ async fn mempool_request_with_missing_input_is_rejected() { ) }) .await - .expect("verifier should call mock state service") + .expect("verifier should call mock state service with correct request") .respond(zebra_state::Response::ValidBestChainTipNullifiersAndAnchors); }); @@ -253,10 +261,18 @@ async fn mempool_request_with_present_input_is_accepted() { }; tokio::spawn(async move { + state + .expect_request(zebra_state::Request::BestChainNextMedianTimePast) + .await + .expect("verifier should call mock state service with correct request") + .respond(zebra_state::Response::BestChainNextMedianTimePast( + DateTime32::MAX, + )); + state .expect_request(zebra_state::Request::UnspentBestChainUtxo(input_outpoint)) .await - .expect("verifier should call mock state service") + .expect("verifier should call mock state service with correct request") .respond(zebra_state::Response::UnspentBestChainUtxo( known_utxos .get(&input_outpoint) @@ -271,7 +287,298 @@ async fn mempool_request_with_present_input_is_accepted() { ) }) .await - .expect("verifier should call mock state service") + .expect("verifier should call mock state service with correct request") + .respond(zebra_state::Response::ValidBestChainTipNullifiersAndAnchors); + }); + + let verifier_response = verifier + .oneshot(Request::Mempool { + transaction: tx.into(), + height, + }) + .await; + + assert!( + verifier_response.is_ok(), + "expected successful verification, got: {verifier_response:?}" + ); +} + +#[tokio::test] +async fn mempool_request_with_invalid_lock_time_is_rejected() { + let mut state: MockService<_, _, _, _> = MockService::build().for_prop_tests(); + let verifier = Verifier::new(Network::Mainnet, state.clone()); + + let height = NetworkUpgrade::Canopy + .activation_height(Network::Mainnet) + .expect("Canopy activation height is specified"); + let fund_height = (height - 1).expect("fake source fund block height is too small"); + let (input, output, known_utxos) = mock_transparent_transfer(fund_height, true, 0); + + // Create a non-coinbase V4 tx with the last valid expiry height. + let tx = Transaction::V4 { + inputs: vec![input], + outputs: vec![output], + lock_time: LockTime::max_lock_time_timestamp(), + expiry_height: height, + joinsplit_data: None, + sapling_shielded_data: None, + }; + + let input_outpoint = match tx.inputs()[0] { + transparent::Input::PrevOut { outpoint, .. } => outpoint, + transparent::Input::Coinbase { .. } => panic!("requires a non-coinbase transaction"), + }; + + tokio::spawn(async move { + state + .expect_request(zebra_state::Request::BestChainNextMedianTimePast) + .await + .expect("verifier should call mock state service with correct request") + .respond(zebra_state::Response::BestChainNextMedianTimePast( + DateTime32::from( + u32::try_from(LockTime::MIN_TIMESTAMP).expect("min time is valid"), + ), + )); + + state + .expect_request(zebra_state::Request::UnspentBestChainUtxo(input_outpoint)) + .await + .expect("verifier should call mock state service with correct request") + .respond(zebra_state::Response::UnspentBestChainUtxo( + known_utxos + .get(&input_outpoint) + .map(|utxo| utxo.utxo.clone()), + )); + + state + .expect_request_that(|req| { + matches!( + req, + zebra_state::Request::CheckBestChainTipNullifiersAndAnchors(_) + ) + }) + .await + .expect("verifier should call mock state service with correct request") + .respond(zebra_state::Response::ValidBestChainTipNullifiersAndAnchors); + }); + + let verifier_response = verifier + .oneshot(Request::Mempool { + transaction: tx.into(), + height, + }) + .await; + + assert_eq!( + verifier_response, + Err(TransactionError::LockedUntilAfterBlockTime( + Utc.timestamp_opt(u32::MAX.into(), 0).unwrap() + )) + ); +} + +#[tokio::test] +async fn mempool_request_with_unlocked_lock_time_is_accepted() { + let mut state: MockService<_, _, _, _> = MockService::build().for_prop_tests(); + let verifier = Verifier::new(Network::Mainnet, state.clone()); + + let height = NetworkUpgrade::Canopy + .activation_height(Network::Mainnet) + .expect("Canopy activation height is specified"); + let fund_height = (height - 1).expect("fake source fund block height is too small"); + let (input, output, known_utxos) = mock_transparent_transfer(fund_height, true, 0); + + // Create a non-coinbase V4 tx with the last valid expiry height. + let tx = Transaction::V4 { + inputs: vec![input], + outputs: vec![output], + lock_time: LockTime::unlocked(), + expiry_height: height, + joinsplit_data: None, + sapling_shielded_data: None, + }; + + let input_outpoint = match tx.inputs()[0] { + transparent::Input::PrevOut { outpoint, .. } => outpoint, + transparent::Input::Coinbase { .. } => panic!("requires a non-coinbase transaction"), + }; + + tokio::spawn(async move { + state + .expect_request(zebra_state::Request::BestChainNextMedianTimePast) + .await + .expect("verifier should call mock state service with correct request") + .respond(zebra_state::Response::BestChainNextMedianTimePast( + DateTime32::from( + u32::try_from(LockTime::MIN_TIMESTAMP).expect("min time is valid"), + ), + )); + + state + .expect_request(zebra_state::Request::UnspentBestChainUtxo(input_outpoint)) + .await + .expect("verifier should call mock state service with correct request") + .respond(zebra_state::Response::UnspentBestChainUtxo( + known_utxos + .get(&input_outpoint) + .map(|utxo| utxo.utxo.clone()), + )); + + state + .expect_request_that(|req| { + matches!( + req, + zebra_state::Request::CheckBestChainTipNullifiersAndAnchors(_) + ) + }) + .await + .expect("verifier should call mock state service with correct request") + .respond(zebra_state::Response::ValidBestChainTipNullifiersAndAnchors); + }); + + let verifier_response = verifier + .oneshot(Request::Mempool { + transaction: tx.into(), + height, + }) + .await; + + assert!( + verifier_response.is_ok(), + "expected successful verification, got: {verifier_response:?}" + ); +} + +#[tokio::test] +async fn mempool_request_with_lock_time_max_sequence_number_is_accepted() { + let mut state: MockService<_, _, _, _> = MockService::build().for_prop_tests(); + let verifier = Verifier::new(Network::Mainnet, state.clone()); + + let height = NetworkUpgrade::Canopy + .activation_height(Network::Mainnet) + .expect("Canopy activation height is specified"); + let fund_height = (height - 1).expect("fake source fund block height is too small"); + let (mut input, output, known_utxos) = mock_transparent_transfer(fund_height, true, 0); + + // Ignore the lock time. + input.set_sequence(u32::MAX); + + // Create a non-coinbase V4 tx with the last valid expiry height. + let tx = Transaction::V4 { + inputs: vec![input], + outputs: vec![output], + lock_time: LockTime::max_lock_time_timestamp(), + expiry_height: height, + joinsplit_data: None, + sapling_shielded_data: None, + }; + + let input_outpoint = match tx.inputs()[0] { + transparent::Input::PrevOut { outpoint, .. } => outpoint, + transparent::Input::Coinbase { .. } => panic!("requires a non-coinbase transaction"), + }; + + tokio::spawn(async move { + state + .expect_request(zebra_state::Request::BestChainNextMedianTimePast) + .await + .expect("verifier should call mock state service with correct request") + .respond(zebra_state::Response::BestChainNextMedianTimePast( + DateTime32::from( + u32::try_from(LockTime::MIN_TIMESTAMP).expect("min time is valid"), + ), + )); + + state + .expect_request(zebra_state::Request::UnspentBestChainUtxo(input_outpoint)) + .await + .expect("verifier should call mock state service with correct request") + .respond(zebra_state::Response::UnspentBestChainUtxo( + known_utxos + .get(&input_outpoint) + .map(|utxo| utxo.utxo.clone()), + )); + + state + .expect_request_that(|req| { + matches!( + req, + zebra_state::Request::CheckBestChainTipNullifiersAndAnchors(_) + ) + }) + .await + .expect("verifier should call mock state service with correct request") + .respond(zebra_state::Response::ValidBestChainTipNullifiersAndAnchors); + }); + + let verifier_response = verifier + .oneshot(Request::Mempool { + transaction: tx.into(), + height, + }) + .await; + + assert!( + verifier_response.is_ok(), + "expected successful verification, got: {verifier_response:?}" + ); +} + +#[tokio::test] +async fn mempool_request_with_past_lock_time_is_accepted() { + let mut state: MockService<_, _, _, _> = MockService::build().for_prop_tests(); + let verifier = Verifier::new(Network::Mainnet, state.clone()); + + let height = NetworkUpgrade::Canopy + .activation_height(Network::Mainnet) + .expect("Canopy activation height is specified"); + let fund_height = (height - 1).expect("fake source fund block height is too small"); + let (input, output, known_utxos) = mock_transparent_transfer(fund_height, true, 0); + + // Create a non-coinbase V4 tx with the last valid expiry height. + let tx = Transaction::V4 { + inputs: vec![input], + outputs: vec![output], + lock_time: LockTime::min_lock_time_timestamp(), + expiry_height: height, + joinsplit_data: None, + sapling_shielded_data: None, + }; + + let input_outpoint = match tx.inputs()[0] { + transparent::Input::PrevOut { outpoint, .. } => outpoint, + transparent::Input::Coinbase { .. } => panic!("requires a non-coinbase transaction"), + }; + + tokio::spawn(async move { + state + .expect_request(zebra_state::Request::BestChainNextMedianTimePast) + .await + .expect("verifier should call mock state service with correct request") + .respond(zebra_state::Response::BestChainNextMedianTimePast( + DateTime32::MAX, + )); + + state + .expect_request(zebra_state::Request::UnspentBestChainUtxo(input_outpoint)) + .await + .expect("verifier should call mock state service with correct request") + .respond(zebra_state::Response::UnspentBestChainUtxo( + known_utxos + .get(&input_outpoint) + .map(|utxo| utxo.utxo.clone()), + )); + + state + .expect_request_that(|req| { + matches!( + req, + zebra_state::Request::CheckBestChainTipNullifiersAndAnchors(_) + ) + }) + .await + .expect("verifier should call mock state service with correct request") .respond(zebra_state::Response::ValidBestChainTipNullifiersAndAnchors); }); diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 1ab59694f..1cdfcff87 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -559,6 +559,11 @@ pub enum Request { /// Returns [`Response::ValidBestChainTipNullifiersAndAnchors`] CheckBestChainTipNullifiersAndAnchors(UnminedTx), + /// Calculates the median-time-past for the *next* block on the best chain. + /// + /// Returns [`Response::BestChainNextMedianTimePast`] when successful. + BestChainNextMedianTimePast, + #[cfg(feature = "getblocktemplate-rpcs")] /// Performs contextual validation of the given block, but does not commit it to the state. /// @@ -584,6 +589,7 @@ impl Request { Request::CheckBestChainTipNullifiersAndAnchors(_) => { "best_chain_tip_nullifiers_anchors" } + Request::BestChainNextMedianTimePast => "best_chain_next_median_time_past", #[cfg(feature = "getblocktemplate-rpcs")] Request::CheckBlockProposalValidity(_) => "check_block_proposal_validity", } @@ -772,6 +778,11 @@ pub enum ReadRequest { /// Returns [`ReadResponse::ValidBestChainTipNullifiersAndAnchors`]. CheckBestChainTipNullifiersAndAnchors(UnminedTx), + /// Calculates the median-time-past for the *next* block on the best chain. + /// + /// Returns [`ReadResponse::BestChainNextMedianTimePast`] when successful. + BestChainNextMedianTimePast, + #[cfg(feature = "getblocktemplate-rpcs")] /// Looks up a block hash by height in the current best chain. /// @@ -832,6 +843,7 @@ impl ReadRequest { ReadRequest::CheckBestChainTipNullifiersAndAnchors(_) => { "best_chain_tip_nullifiers_anchors" } + ReadRequest::BestChainNextMedianTimePast => "best_chain_next_median_time_past", #[cfg(feature = "getblocktemplate-rpcs")] ReadRequest::BestChainBlockHash(_) => "best_chain_block_hash", #[cfg(feature = "getblocktemplate-rpcs")] @@ -864,6 +876,7 @@ impl TryFrom for ReadRequest { match request { Request::Tip => Ok(ReadRequest::Tip), Request::Depth(hash) => Ok(ReadRequest::Depth(hash)), + Request::BestChainNextMedianTimePast => Ok(ReadRequest::BestChainNextMedianTimePast), Request::Block(hash_or_height) => Ok(ReadRequest::Block(hash_or_height)), Request::Transaction(tx_hash) => Ok(ReadRequest::Transaction(tx_hash)), diff --git a/zebra-state/src/response.rs b/zebra-state/src/response.rs index 1ee264cb6..c5a6160c4 100644 --- a/zebra-state/src/response.rs +++ b/zebra-state/src/response.rs @@ -6,12 +6,13 @@ use zebra_chain::{ amount::{Amount, NonNegative}, block::{self, Block}, orchard, sapling, + serialization::DateTime32, transaction::{self, Transaction}, transparent, }; #[cfg(feature = "getblocktemplate-rpcs")] -use zebra_chain::{serialization::DateTime32, work::difficulty::CompactDifficulty}; +use zebra_chain::work::difficulty::CompactDifficulty; // Allow *only* these unused imports, so that rustdoc link resolution // will work with inline links. @@ -60,6 +61,10 @@ pub enum Response { /// Does not check transparent UTXO inputs ValidBestChainTipNullifiersAndAnchors, + /// Response to [`Request::BestChainNextMedianTimePast`]. + /// Contains the median-time-past for the *next* block on the best chain. + BestChainNextMedianTimePast(DateTime32), + #[cfg(feature = "getblocktemplate-rpcs")] /// Response to [`Request::CheckBlockProposalValidity`](crate::Request::CheckBlockProposalValidity) ValidBlockProposal, @@ -128,6 +133,10 @@ pub enum ReadResponse { /// Does not check transparent UTXO inputs ValidBestChainTipNullifiersAndAnchors, + /// Response to [`ReadRequest::BestChainNextMedianTimePast`]. + /// Contains the median-time-past for the *next* block on the best chain. + BestChainNextMedianTimePast(DateTime32), + #[cfg(feature = "getblocktemplate-rpcs")] /// Response to [`ReadRequest::BestChainBlockHash`](crate::ReadRequest::BestChainBlockHash) with the /// specified block hash. @@ -195,6 +204,7 @@ impl TryFrom for Response { match response { ReadResponse::Tip(height_and_hash) => Ok(Response::Tip(height_and_hash)), ReadResponse::Depth(depth) => Ok(Response::Depth(depth)), + ReadResponse::BestChainNextMedianTimePast(median_time_past) => Ok(Response::BestChainNextMedianTimePast(median_time_past)), ReadResponse::Block(block) => Ok(Response::Block(block)), ReadResponse::Transaction(tx_and_height) => { diff --git a/zebra-state/src/service.rs b/zebra-state/src/service.rs index f155e0cb2..e73224c57 100644 --- a/zebra-state/src/service.rs +++ b/zebra-state/src/service.rs @@ -1023,8 +1023,9 @@ impl Service for StateService { } // Runs concurrently using the ReadStateService - Request::Depth(_) - | Request::Tip + Request::Tip + | Request::Depth(_) + | Request::BestChainNextMedianTimePast | Request::BlockLocator | Request::Transaction(_) | Request::UnspentBestChainUtxo(_) @@ -1155,6 +1156,35 @@ impl Service for ReadStateService { .boxed() } + // Used by the StateService. + ReadRequest::BestChainNextMedianTimePast => { + let timer = CodeTimer::start(); + + let state = self.clone(); + + let span = Span::current(); + tokio::task::spawn_blocking(move || { + span.in_scope(move || { + let non_finalized_state = state.latest_non_finalized_state(); + let median_time_past = + read::next_median_time_past(&non_finalized_state, &state.db); + + // The work is done in the future. + timer.finish( + module_path!(), + line!(), + "ReadRequest::BestChainNextMedianTimePast", + ); + + Ok(ReadResponse::BestChainNextMedianTimePast(median_time_past?)) + }) + }) + .map(|join_result| { + join_result.expect("panic in ReadRequest::BestChainNextMedianTimePast") + }) + .boxed() + } + // Used by the get_block (raw) RPC and the StateService. ReadRequest::Block(hash_or_height) => { let timer = CodeTimer::start(); diff --git a/zebra-state/src/service/read.rs b/zebra-state/src/service/read.rs index de1161aeb..9f674a700 100644 --- a/zebra-state/src/service/read.rs +++ b/zebra-state/src/service/read.rs @@ -16,13 +16,12 @@ use crate::service; pub mod address; pub mod block; +pub mod find; +pub mod tree; #[cfg(feature = "getblocktemplate-rpcs")] pub mod difficulty; -pub mod find; -pub mod tree; - #[cfg(test)] mod tests; @@ -34,16 +33,15 @@ pub use address::{ pub use block::{ any_utxo, block, block_header, transaction, transaction_hashes_for_block, unspent_utxo, utxo, }; +pub use find::{ + best_tip, block_locator, chain_contains_hash, depth, find_chain_hashes, find_chain_headers, + hash_by_height, height_by_hash, next_median_time_past, tip, tip_height, +}; +pub use tree::{orchard_tree, sapling_tree}; #[cfg(feature = "getblocktemplate-rpcs")] pub use difficulty::get_block_template_chain_info; -pub use find::{ - best_tip, block_locator, chain_contains_hash, depth, find_chain_hashes, find_chain_headers, - hash_by_height, height_by_hash, tip, tip_height, -}; -pub use tree::{orchard_tree, sapling_tree}; - /// If a finalized state query is interrupted by a new finalized block, /// retry this many times. /// diff --git a/zebra-state/src/service/read/find.rs b/zebra-state/src/service/read/find.rs index a4ff421f9..f32ea839d 100644 --- a/zebra-state/src/service/read/find.rs +++ b/zebra-state/src/service/read/find.rs @@ -17,15 +17,24 @@ use std::{ sync::Arc, }; -use zebra_chain::block::{self, Height}; +use chrono::{DateTime, Utc}; +use zebra_chain::{ + block::{self, Block, Height}, + parameters::Network, + serialization::DateTime32, + work::difficulty::CompactDifficulty, +}; use crate::{ constants, service::{ + block_iter::any_ancestor_blocks, + check::{difficulty::POW_ADJUSTMENT_BLOCK_SPAN, AdjustedDifficulty}, finalized_state::ZebraDb, non_finalized_state::{Chain, NonFinalizedState}, - read::block::block_header, + read::{self, block::block_header, FINALIZED_STATE_QUERY_RETRIES}, }, + BoxError, }; #[cfg(test)] @@ -526,3 +535,102 @@ where collect_chain_headers(chain, db, intersection, stop, max_len) } + +/// Returns the median-time-past of the *next* block to be added to the best chain in +/// `non_finalized_state` or `db`. +/// +/// # Panics +/// +/// - If we don't have enough blocks in the state. +pub fn next_median_time_past( + non_finalized_state: &NonFinalizedState, + db: &ZebraDb, +) -> Result { + let mut best_relevant_chain_result = best_relevant_chain(non_finalized_state, db); + + // Retry the finalized state query if it was interrupted by a finalizing block. + // + // TODO: refactor this into a generic retry(finalized_closure, process_and_check_closure) fn + for _ in 0..FINALIZED_STATE_QUERY_RETRIES { + if best_relevant_chain_result.is_ok() { + break; + } + + best_relevant_chain_result = best_relevant_chain(non_finalized_state, db); + } + + Ok(calculate_median_time_past(best_relevant_chain_result?)) +} + +/// Do a consistency check by checking the finalized tip before and after all other database queries. +/// +/// Returns recent blocks in reverse height order from the tip. +/// Returns an error if the tip obtained before and after is not the same. +/// +/// # Panics +/// +/// - If we don't have enough blocks in the state. +fn best_relevant_chain( + non_finalized_state: &NonFinalizedState, + db: &ZebraDb, +) -> Result<[Arc; POW_ADJUSTMENT_BLOCK_SPAN], BoxError> { + let state_tip_before_queries = read::best_tip(non_finalized_state, db).ok_or_else(|| { + BoxError::from("Zebra's state is empty, wait until it syncs to the chain tip") + })?; + + let best_relevant_chain = + any_ancestor_blocks(non_finalized_state, db, state_tip_before_queries.1); + let best_relevant_chain: Vec<_> = best_relevant_chain + .into_iter() + .take(POW_ADJUSTMENT_BLOCK_SPAN) + .collect(); + let best_relevant_chain = best_relevant_chain.try_into().map_err(|_error| { + "Zebra's state only has a few blocks, wait until it syncs to the chain tip" + })?; + + let state_tip_after_queries = + read::best_tip(non_finalized_state, db).expect("already checked for an empty tip"); + + if state_tip_before_queries != state_tip_after_queries { + return Err("Zebra is committing too many blocks to the state, \ + wait until it syncs to the chain tip" + .into()); + } + + Ok(best_relevant_chain) +} + +/// Returns the median-time-past for the provided `relevant_chain`. +/// +/// The `relevant_chain` has blocks in reverse height order. +/// +/// See [`next_median_time_past()`] for details. +fn calculate_median_time_past( + relevant_chain: [Arc; POW_ADJUSTMENT_BLOCK_SPAN], +) -> DateTime32 { + let relevant_data: Vec<(CompactDifficulty, DateTime)> = relevant_chain + .iter() + .map(|block| (block.header.difficulty_threshold, block.header.time)) + .collect(); + + // TODO: split out median-time-past into its own struct? + let ignored_time = DateTime::default(); + let ignored_height = Height(0); + let ignored_network = Network::Mainnet; + + // Get the median-time-past, which doesn't depend on the time or the previous block height. + // `context` will always have the correct length, because this function takes an array. + let median_time_past = AdjustedDifficulty::new_from_header_time( + ignored_time, + ignored_height, + ignored_network, + relevant_data, + ) + .median_time_past(); + + // > Define the median-time-past of a block to be the median of the nTime fields of the + // > preceding PoWMedianBlockSpan blocks (or all preceding blocks if there are fewer than + // > PoWMedianBlockSpan). The median-time-past of a genesis block is not defined. + // https://zips.z.cash/protocol/protocol.pdf#blockheader + DateTime32::try_from(median_time_past).expect("valid blocks have in-range times") +} diff --git a/zebrad/src/components/mempool.rs b/zebrad/src/components/mempool.rs index 3d3ea8264..969b4f3b9 100644 --- a/zebrad/src/components/mempool.rs +++ b/zebrad/src/components/mempool.rs @@ -406,10 +406,16 @@ impl Service for Mempool { let mined_ids = block.transaction_hashes.iter().cloned().collect(); tx_downloads.cancel(&mined_ids); storage.reject_and_remove_same_effects(&mined_ids, block.transactions); + + // Clear any transaction rejections if they might have become valid after + // the new block was added to the tip. storage.clear_tip_rejections(); } // Remove expired transactions from the mempool. + // + // Lock times never expire, because block times are strictly increasing. + // So we don't need to check them here. if let Some(tip_height) = self.latest_chain_tip.best_tip_height() { let expired_transactions = storage.remove_expired_transactions(tip_height); // Remove transactions that are expired from the peers list diff --git a/zebrad/src/components/mempool/storage.rs b/zebrad/src/components/mempool/storage.rs index 39a68b081..b2fbdef1d 100644 --- a/zebrad/src/components/mempool/storage.rs +++ b/zebrad/src/components/mempool/storage.rs @@ -43,7 +43,7 @@ mod verified_set; pub(crate) const MAX_EVICTION_MEMORY_ENTRIES: usize = 40_000; /// Transactions rejected based on transaction authorizing data (scripts, proofs, signatures), -/// These rejections are only valid for the current tip. +/// or lock times. These rejections are only valid for the current tip. /// /// Each committed block clears these rejections, because new blocks can supply missing inputs. #[derive(Error, Clone, Debug, PartialEq, Eq)]