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:
parent
7b2f135eca
commit
e20cf957e3
|
@ -186,6 +186,11 @@ pub enum TransactionError {
|
||||||
#[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))]
|
#[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))]
|
||||||
// This error variant is at least 128 bytes
|
// This error variant is at least 128 bytes
|
||||||
ValidateNullifiersAndAnchorsError(Box<ValidateContextError>),
|
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 {
|
impl From<ValidateContextError> for TransactionError {
|
||||||
|
|
|
@ -23,6 +23,7 @@ use zebra_chain::{
|
||||||
parameters::{Network, NetworkUpgrade},
|
parameters::{Network, NetworkUpgrade},
|
||||||
primitives::Groth16Proof,
|
primitives::Groth16Proof,
|
||||||
sapling,
|
sapling,
|
||||||
|
serialization::DateTime32,
|
||||||
transaction::{
|
transaction::{
|
||||||
self, HashType, SigHash, Transaction, UnminedTx, UnminedTxId, VerifiedUnminedTx,
|
self, HashType, SigHash, Transaction, UnminedTx, UnminedTxId, VerifiedUnminedTx,
|
||||||
},
|
},
|
||||||
|
@ -306,17 +307,15 @@ where
|
||||||
async move {
|
async move {
|
||||||
tracing::trace!(?tx_id, ?req, "got tx verify request");
|
tracing::trace!(?tx_id, ?req, "got tx verify request");
|
||||||
|
|
||||||
// Do basic checks first
|
// Do quick checks first
|
||||||
if let Some(block_time) = req.block_time() {
|
|
||||||
check::lock_time_has_passed(&tx, req.height(), block_time)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
check::has_inputs_and_outputs(&tx)?;
|
check::has_inputs_and_outputs(&tx)?;
|
||||||
check::has_enough_orchard_flags(&tx)?;
|
check::has_enough_orchard_flags(&tx)?;
|
||||||
|
|
||||||
|
// Validate the coinbase input consensus rules
|
||||||
if req.is_mempool() && tx.is_coinbase() {
|
if req.is_mempool() && tx.is_coinbase() {
|
||||||
return Err(TransactionError::CoinbaseInMempool);
|
return Err(TransactionError::CoinbaseInMempool);
|
||||||
}
|
}
|
||||||
|
|
||||||
if tx.is_coinbase() {
|
if tx.is_coinbase() {
|
||||||
check::coinbase_tx_no_prevout_joinsplit_spend(&tx)?;
|
check::coinbase_tx_no_prevout_joinsplit_spend(&tx)?;
|
||||||
} else if !tx.is_valid_non_coinbase() {
|
} else if !tx.is_valid_non_coinbase() {
|
||||||
|
@ -345,6 +344,34 @@ where
|
||||||
|
|
||||||
tracing::trace!(?tx_id, "passed quick checks");
|
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
|
// "The consensus rules applied to valueBalance, vShieldedOutput, and bindingSig
|
||||||
// in non-coinbase transactions MUST also be applied to coinbase transactions."
|
// in non-coinbase transactions MUST also be applied to coinbase transactions."
|
||||||
//
|
//
|
||||||
|
@ -356,7 +383,7 @@ where
|
||||||
// https://zips.z.cash/zip-0213#specification
|
// https://zips.z.cash/zip-0213#specification
|
||||||
|
|
||||||
// Load spent UTXOs from state.
|
// 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 =
|
let load_spent_utxos_fut =
|
||||||
Self::spent_utxos(tx.clone(), req.known_utxos(), req.is_mempool(), state.clone());
|
Self::spent_utxos(tx.clone(), req.known_utxos(), req.is_mempool(), state.clone());
|
||||||
let (spent_utxos, spent_outputs) = load_spent_utxos_fut.await?;
|
let (spent_utxos, spent_outputs) = load_spent_utxos_fut.await?;
|
||||||
|
@ -426,7 +453,7 @@ where
|
||||||
// Calculate the fee only for non-coinbase transactions.
|
// Calculate the fee only for non-coinbase transactions.
|
||||||
let mut miner_fee = None;
|
let mut miner_fee = None;
|
||||||
if !tx.is_coinbase() {
|
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 {
|
miner_fee = match value_balance {
|
||||||
Ok(vb) => match vb.remaining_transaction_value() {
|
Ok(vb) => match vb.remaining_transaction_value() {
|
||||||
Ok(tx_rtv) => Some(tx_rtv),
|
Ok(tx_rtv) => Some(tx_rtv),
|
||||||
|
@ -471,7 +498,28 @@ where
|
||||||
ZS: Service<zs::Request, Response = zs::Response, Error = BoxError> + Send + Clone + 'static,
|
ZS: Service<zs::Request, Response = zs::Response, Error = BoxError> + Send + Clone + 'static,
|
||||||
ZS::Future: Send + '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.
|
/// `known_utxos` are additional UTXOs known at the time of validation (i.e.
|
||||||
/// from previous transactions in the block).
|
/// from previous transactions in the block).
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, TimeZone, Utc};
|
||||||
use color_eyre::eyre::Report;
|
use color_eyre::eyre::Report;
|
||||||
use halo2::pasta::{group::ff::PrimeField, pallas};
|
use halo2::pasta::{group::ff::PrimeField, pallas};
|
||||||
use tower::{service_fn, ServiceExt};
|
use tower::{service_fn, ServiceExt};
|
||||||
|
@ -14,7 +14,7 @@ use zebra_chain::{
|
||||||
parameters::{Network, NetworkUpgrade},
|
parameters::{Network, NetworkUpgrade},
|
||||||
primitives::{ed25519, x25519, Groth16Proof},
|
primitives::{ed25519, x25519, Groth16Proof},
|
||||||
sapling,
|
sapling,
|
||||||
serialization::{ZcashDeserialize, ZcashDeserializeInto},
|
serialization::{DateTime32, ZcashDeserialize, ZcashDeserializeInto},
|
||||||
sprout,
|
sprout,
|
||||||
transaction::{
|
transaction::{
|
||||||
arbitrary::{
|
arbitrary::{
|
||||||
|
@ -195,10 +195,18 @@ async fn mempool_request_with_missing_input_is_rejected() {
|
||||||
};
|
};
|
||||||
|
|
||||||
tokio::spawn(async move {
|
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
|
state
|
||||||
.expect_request(zebra_state::Request::UnspentBestChainUtxo(input_outpoint))
|
.expect_request(zebra_state::Request::UnspentBestChainUtxo(input_outpoint))
|
||||||
.await
|
.await
|
||||||
.expect("verifier should call mock state service")
|
.expect("verifier should call mock state service with correct request")
|
||||||
.respond(zebra_state::Response::UnspentBestChainUtxo(None));
|
.respond(zebra_state::Response::UnspentBestChainUtxo(None));
|
||||||
|
|
||||||
state
|
state
|
||||||
|
@ -209,7 +217,7 @@ async fn mempool_request_with_missing_input_is_rejected() {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.expect("verifier should call mock state service")
|
.expect("verifier should call mock state service with correct request")
|
||||||
.respond(zebra_state::Response::ValidBestChainTipNullifiersAndAnchors);
|
.respond(zebra_state::Response::ValidBestChainTipNullifiersAndAnchors);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -253,10 +261,18 @@ async fn mempool_request_with_present_input_is_accepted() {
|
||||||
};
|
};
|
||||||
|
|
||||||
tokio::spawn(async move {
|
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
|
state
|
||||||
.expect_request(zebra_state::Request::UnspentBestChainUtxo(input_outpoint))
|
.expect_request(zebra_state::Request::UnspentBestChainUtxo(input_outpoint))
|
||||||
.await
|
.await
|
||||||
.expect("verifier should call mock state service")
|
.expect("verifier should call mock state service with correct request")
|
||||||
.respond(zebra_state::Response::UnspentBestChainUtxo(
|
.respond(zebra_state::Response::UnspentBestChainUtxo(
|
||||||
known_utxos
|
known_utxos
|
||||||
.get(&input_outpoint)
|
.get(&input_outpoint)
|
||||||
|
@ -271,7 +287,298 @@ async fn mempool_request_with_present_input_is_accepted() {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.await
|
.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);
|
.respond(zebra_state::Response::ValidBestChainTipNullifiersAndAnchors);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -559,6 +559,11 @@ pub enum Request {
|
||||||
/// Returns [`Response::ValidBestChainTipNullifiersAndAnchors`]
|
/// Returns [`Response::ValidBestChainTipNullifiersAndAnchors`]
|
||||||
CheckBestChainTipNullifiersAndAnchors(UnminedTx),
|
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")]
|
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||||
/// Performs contextual validation of the given block, but does not commit it to the state.
|
/// Performs contextual validation of the given block, but does not commit it to the state.
|
||||||
///
|
///
|
||||||
|
@ -584,6 +589,7 @@ impl Request {
|
||||||
Request::CheckBestChainTipNullifiersAndAnchors(_) => {
|
Request::CheckBestChainTipNullifiersAndAnchors(_) => {
|
||||||
"best_chain_tip_nullifiers_anchors"
|
"best_chain_tip_nullifiers_anchors"
|
||||||
}
|
}
|
||||||
|
Request::BestChainNextMedianTimePast => "best_chain_next_median_time_past",
|
||||||
#[cfg(feature = "getblocktemplate-rpcs")]
|
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||||
Request::CheckBlockProposalValidity(_) => "check_block_proposal_validity",
|
Request::CheckBlockProposalValidity(_) => "check_block_proposal_validity",
|
||||||
}
|
}
|
||||||
|
@ -772,6 +778,11 @@ pub enum ReadRequest {
|
||||||
/// Returns [`ReadResponse::ValidBestChainTipNullifiersAndAnchors`].
|
/// Returns [`ReadResponse::ValidBestChainTipNullifiersAndAnchors`].
|
||||||
CheckBestChainTipNullifiersAndAnchors(UnminedTx),
|
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")]
|
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||||
/// Looks up a block hash by height in the current best chain.
|
/// Looks up a block hash by height in the current best chain.
|
||||||
///
|
///
|
||||||
|
@ -832,6 +843,7 @@ impl ReadRequest {
|
||||||
ReadRequest::CheckBestChainTipNullifiersAndAnchors(_) => {
|
ReadRequest::CheckBestChainTipNullifiersAndAnchors(_) => {
|
||||||
"best_chain_tip_nullifiers_anchors"
|
"best_chain_tip_nullifiers_anchors"
|
||||||
}
|
}
|
||||||
|
ReadRequest::BestChainNextMedianTimePast => "best_chain_next_median_time_past",
|
||||||
#[cfg(feature = "getblocktemplate-rpcs")]
|
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||||
ReadRequest::BestChainBlockHash(_) => "best_chain_block_hash",
|
ReadRequest::BestChainBlockHash(_) => "best_chain_block_hash",
|
||||||
#[cfg(feature = "getblocktemplate-rpcs")]
|
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||||
|
@ -864,6 +876,7 @@ impl TryFrom<Request> for ReadRequest {
|
||||||
match request {
|
match request {
|
||||||
Request::Tip => Ok(ReadRequest::Tip),
|
Request::Tip => Ok(ReadRequest::Tip),
|
||||||
Request::Depth(hash) => Ok(ReadRequest::Depth(hash)),
|
Request::Depth(hash) => Ok(ReadRequest::Depth(hash)),
|
||||||
|
Request::BestChainNextMedianTimePast => Ok(ReadRequest::BestChainNextMedianTimePast),
|
||||||
|
|
||||||
Request::Block(hash_or_height) => Ok(ReadRequest::Block(hash_or_height)),
|
Request::Block(hash_or_height) => Ok(ReadRequest::Block(hash_or_height)),
|
||||||
Request::Transaction(tx_hash) => Ok(ReadRequest::Transaction(tx_hash)),
|
Request::Transaction(tx_hash) => Ok(ReadRequest::Transaction(tx_hash)),
|
||||||
|
|
|
@ -6,12 +6,13 @@ use zebra_chain::{
|
||||||
amount::{Amount, NonNegative},
|
amount::{Amount, NonNegative},
|
||||||
block::{self, Block},
|
block::{self, Block},
|
||||||
orchard, sapling,
|
orchard, sapling,
|
||||||
|
serialization::DateTime32,
|
||||||
transaction::{self, Transaction},
|
transaction::{self, Transaction},
|
||||||
transparent,
|
transparent,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "getblocktemplate-rpcs")]
|
#[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
|
// Allow *only* these unused imports, so that rustdoc link resolution
|
||||||
// will work with inline links.
|
// will work with inline links.
|
||||||
|
@ -60,6 +61,10 @@ pub enum Response {
|
||||||
/// Does not check transparent UTXO inputs
|
/// Does not check transparent UTXO inputs
|
||||||
ValidBestChainTipNullifiersAndAnchors,
|
ValidBestChainTipNullifiersAndAnchors,
|
||||||
|
|
||||||
|
/// Response to [`Request::BestChainNextMedianTimePast`].
|
||||||
|
/// Contains the median-time-past for the *next* block on the best chain.
|
||||||
|
BestChainNextMedianTimePast(DateTime32),
|
||||||
|
|
||||||
#[cfg(feature = "getblocktemplate-rpcs")]
|
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||||
/// Response to [`Request::CheckBlockProposalValidity`](crate::Request::CheckBlockProposalValidity)
|
/// Response to [`Request::CheckBlockProposalValidity`](crate::Request::CheckBlockProposalValidity)
|
||||||
ValidBlockProposal,
|
ValidBlockProposal,
|
||||||
|
@ -128,6 +133,10 @@ pub enum ReadResponse {
|
||||||
/// Does not check transparent UTXO inputs
|
/// Does not check transparent UTXO inputs
|
||||||
ValidBestChainTipNullifiersAndAnchors,
|
ValidBestChainTipNullifiersAndAnchors,
|
||||||
|
|
||||||
|
/// Response to [`ReadRequest::BestChainNextMedianTimePast`].
|
||||||
|
/// Contains the median-time-past for the *next* block on the best chain.
|
||||||
|
BestChainNextMedianTimePast(DateTime32),
|
||||||
|
|
||||||
#[cfg(feature = "getblocktemplate-rpcs")]
|
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||||
/// Response to [`ReadRequest::BestChainBlockHash`](crate::ReadRequest::BestChainBlockHash) with the
|
/// Response to [`ReadRequest::BestChainBlockHash`](crate::ReadRequest::BestChainBlockHash) with the
|
||||||
/// specified block hash.
|
/// specified block hash.
|
||||||
|
@ -195,6 +204,7 @@ impl TryFrom<ReadResponse> for Response {
|
||||||
match response {
|
match response {
|
||||||
ReadResponse::Tip(height_and_hash) => Ok(Response::Tip(height_and_hash)),
|
ReadResponse::Tip(height_and_hash) => Ok(Response::Tip(height_and_hash)),
|
||||||
ReadResponse::Depth(depth) => Ok(Response::Depth(depth)),
|
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::Block(block) => Ok(Response::Block(block)),
|
||||||
ReadResponse::Transaction(tx_and_height) => {
|
ReadResponse::Transaction(tx_and_height) => {
|
||||||
|
|
|
@ -1023,8 +1023,9 @@ impl Service<Request> for StateService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runs concurrently using the ReadStateService
|
// Runs concurrently using the ReadStateService
|
||||||
Request::Depth(_)
|
Request::Tip
|
||||||
| Request::Tip
|
| Request::Depth(_)
|
||||||
|
| Request::BestChainNextMedianTimePast
|
||||||
| Request::BlockLocator
|
| Request::BlockLocator
|
||||||
| Request::Transaction(_)
|
| Request::Transaction(_)
|
||||||
| Request::UnspentBestChainUtxo(_)
|
| Request::UnspentBestChainUtxo(_)
|
||||||
|
@ -1155,6 +1156,35 @@ impl Service<ReadRequest> for ReadStateService {
|
||||||
.boxed()
|
.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.
|
// Used by the get_block (raw) RPC and the StateService.
|
||||||
ReadRequest::Block(hash_or_height) => {
|
ReadRequest::Block(hash_or_height) => {
|
||||||
let timer = CodeTimer::start();
|
let timer = CodeTimer::start();
|
||||||
|
|
|
@ -16,13 +16,12 @@ use crate::service;
|
||||||
|
|
||||||
pub mod address;
|
pub mod address;
|
||||||
pub mod block;
|
pub mod block;
|
||||||
|
pub mod find;
|
||||||
|
pub mod tree;
|
||||||
|
|
||||||
#[cfg(feature = "getblocktemplate-rpcs")]
|
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||||
pub mod difficulty;
|
pub mod difficulty;
|
||||||
|
|
||||||
pub mod find;
|
|
||||||
pub mod tree;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
|
@ -34,16 +33,15 @@ pub use address::{
|
||||||
pub use block::{
|
pub use block::{
|
||||||
any_utxo, block, block_header, transaction, transaction_hashes_for_block, unspent_utxo, utxo,
|
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")]
|
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||||
pub use difficulty::get_block_template_chain_info;
|
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,
|
/// If a finalized state query is interrupted by a new finalized block,
|
||||||
/// retry this many times.
|
/// retry this many times.
|
||||||
///
|
///
|
||||||
|
|
|
@ -17,15 +17,24 @@ use std::{
|
||||||
sync::Arc,
|
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::{
|
use crate::{
|
||||||
constants,
|
constants,
|
||||||
service::{
|
service::{
|
||||||
|
block_iter::any_ancestor_blocks,
|
||||||
|
check::{difficulty::POW_ADJUSTMENT_BLOCK_SPAN, AdjustedDifficulty},
|
||||||
finalized_state::ZebraDb,
|
finalized_state::ZebraDb,
|
||||||
non_finalized_state::{Chain, NonFinalizedState},
|
non_finalized_state::{Chain, NonFinalizedState},
|
||||||
read::block::block_header,
|
read::{self, block::block_header, FINALIZED_STATE_QUERY_RETRIES},
|
||||||
},
|
},
|
||||||
|
BoxError,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -526,3 +535,102 @@ where
|
||||||
|
|
||||||
collect_chain_headers(chain, db, intersection, stop, max_len)
|
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")
|
||||||
|
}
|
||||||
|
|
|
@ -406,10 +406,16 @@ impl Service<Request> for Mempool {
|
||||||
let mined_ids = block.transaction_hashes.iter().cloned().collect();
|
let mined_ids = block.transaction_hashes.iter().cloned().collect();
|
||||||
tx_downloads.cancel(&mined_ids);
|
tx_downloads.cancel(&mined_ids);
|
||||||
storage.reject_and_remove_same_effects(&mined_ids, block.transactions);
|
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();
|
storage.clear_tip_rejections();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove expired transactions from the mempool.
|
// 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() {
|
if let Some(tip_height) = self.latest_chain_tip.best_tip_height() {
|
||||||
let expired_transactions = storage.remove_expired_transactions(tip_height);
|
let expired_transactions = storage.remove_expired_transactions(tip_height);
|
||||||
// Remove transactions that are expired from the peers list
|
// Remove transactions that are expired from the peers list
|
||||||
|
|
|
@ -43,7 +43,7 @@ mod verified_set;
|
||||||
pub(crate) const MAX_EVICTION_MEMORY_ENTRIES: usize = 40_000;
|
pub(crate) const MAX_EVICTION_MEMORY_ENTRIES: usize = 40_000;
|
||||||
|
|
||||||
/// Transactions rejected based on transaction authorizing data (scripts, proofs, signatures),
|
/// 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.
|
/// Each committed block clears these rejections, because new blocks can supply missing inputs.
|
||||||
#[derive(Error, Clone, Debug, PartialEq, Eq)]
|
#[derive(Error, Clone, Debug, PartialEq, Eq)]
|
||||||
|
|
Loading…
Reference in New Issue