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
This commit is contained in:
teor 2023-01-28 07:46:51 +10:00 committed by GitHub
parent 7b2f135eca
commit e20cf957e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 554 additions and 29 deletions

View File

@ -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<ValidateContextError>),
#[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<ValidateContextError> for TransactionError {

View File

@ -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.
// <https://zips.z.cash/protocol/protocol.pdf#blockheader>
//
// > The transaction can be added to any block whose block time is greater than the locktime.
// <https://developer.bitcoin.org/devguide/transactions.html#locktime-and-sequence-number>
//
// 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:
// <https://github.com/zcash/zcash/blob/9e1efad2d13dca5ee094a38e6aa25b0f2464da94/src/main.cpp#L776-L784>
//
// 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<zs::Request, Response = zs::Response, Error = BoxError> + 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<ZS>,
) -> Result<DateTime32, TransactionError> {
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).

View File

@ -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);
});

View File

@ -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<Request> 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)),

View File

@ -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<ReadResponse> 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) => {

View File

@ -1023,8 +1023,9 @@ impl Service<Request> 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<ReadRequest> 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();

View File

@ -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.
///

View File

@ -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<DateTime32, BoxError> {
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<Block>; 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<Block>; POW_ADJUSTMENT_BLOCK_SPAN],
) -> DateTime32 {
let relevant_data: Vec<(CompactDifficulty, DateTime<Utc>)> = 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")
}

View File

@ -406,10 +406,16 @@ impl Service<Request> 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

View File

@ -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)]