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))]
|
||||
// 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 {
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)]
|
||||
|
|
Loading…
Reference in New Issue