use std::{borrow::Borrow, sync::Arc};
use zakura_chain::{
amount::{NegativeAllowed, NonNegative},
block::{
self, merkle::AuthDataRoot, Block, ChainHistoryBlockTxAuthCommitmentHash, CommitmentError,
},
history_tree::HistoryTree,
parameters::{Network, NetworkUpgrade},
value_balance::ValueBalance,
work::difficulty::CompactDifficulty,
};
use crate::{
service::{
block_iter::any_chain_ancestor_iter,
check::difficulty::pow_adjustment_block_span_for_height, finalized_state::ZakuraDb,
non_finalized_state::NonFinalizedState,
},
BoxError, SemanticallyVerifiedBlock, ValidateContextError,
};
use super::check;
#[allow(unused_imports)]
use crate::service::non_finalized_state::Chain;
pub(crate) mod anchors;
pub(crate) mod difficulty;
pub(crate) mod nullifier;
pub(crate) mod utxo;
#[cfg(feature = "internal-bench")]
#[doc(hidden)]
pub use utxo::remaining_transaction_value;
pub use utxo::transparent_coinbase_spend;
#[cfg(test)]
mod tests;
pub(crate) use difficulty::AdjustedDifficulty;
#[allow(clippy::unwrap_in_result)]
pub(crate) fn nsm_value_balance_is_non_negative(
network: &Network,
height: block::Height,
value_pools: &ValueBalance<NonNegative>,
block_value_pool_change: &ValueBalance<NegativeAllowed>,
) -> Result<(), ValidateContextError> {
let nu7_active = NetworkUpgrade::Nu7
.activation_height(network)
.is_some_and(|nu7| height >= nu7);
if !nu7_active {
return Ok(());
}
let balance_before = value_pools.nsm_value_balance_amount();
let balance_change = block_value_pool_change.nsm_value_balance_amount();
let balance_after = (balance_before + balance_change).map_err(|_| {
ValidateContextError::NegativeNsmValueBalance {
height,
balance_before,
balance_change,
}
})?;
if balance_after.zatoshis() < 0 {
return Err(ValidateContextError::NegativeNsmValueBalance {
height,
balance_before,
balance_change,
});
}
Ok(())
}
pub(crate) fn difficulty_context<C: IntoIterator>(
network: &Network,
candidate_height: block::Height,
context: C,
) -> Vec<C::Item> {
context
.into_iter()
.take(pow_adjustment_block_span_for_height(
network,
candidate_height,
))
.collect()
}
#[tracing::instrument(skip(semantically_verified, finalized_tip_height, relevant_headers))]
pub(crate) fn block_is_valid_for_recent_chain<C>(
semantically_verified: &SemanticallyVerifiedBlock,
network: &Network,
finalized_tip_height: Option<block::Height>,
parent_height: Option<block::Height>,
relevant_headers: C,
) -> Result<(), ValidateContextError>
where
C: IntoIterator,
C::Item: Borrow<block::Header>,
{
block_is_valid_for_recent_chain_data(
&semantically_verified.block,
semantically_verified.height,
network,
finalized_tip_height,
parent_height,
relevant_headers,
)
}
pub(crate) fn block_is_valid_for_recent_chain_data<C>(
candidate_block: &Block,
candidate_height: block::Height,
network: &Network,
finalized_tip_height: Option<block::Height>,
parent_height: Option<block::Height>,
relevant_headers: C,
) -> Result<(), ValidateContextError>
where
C: IntoIterator,
C::Item: Borrow<block::Header>,
{
let finalized_tip_height = finalized_tip_height
.expect("finalized state must contain at least one block to do contextual validation");
check::block_is_not_orphaned(finalized_tip_height, candidate_height)?;
let relevant_headers = difficulty_context(network, candidate_height, relevant_headers);
let Some(parent_height) = parent_height.filter(|_| !relevant_headers.is_empty()) else {
warn!(
?candidate_height,
?finalized_tip_height,
"state must contain parent block to do contextual validation"
);
return Err(ValidateContextError::NotReadyToBeCommitted);
};
check::height_one_more_than_parent_height(parent_height, candidate_height)?;
#[cfg(test)]
if relevant_headers.len() < pow_adjustment_block_span_for_height(network, candidate_height) {
return Ok(());
}
#[cfg(not(test))]
if relevant_headers.is_empty() {
return Err(ValidateContextError::NotReadyToBeCommitted);
}
let relevant_data = relevant_headers
.iter()
.map(|header| (header.borrow().difficulty_threshold, header.borrow().time));
let difficulty_adjustment =
AdjustedDifficulty::new_from_block(candidate_block, network, relevant_data)
.map_err(|_| ValidateContextError::NotReadyToBeCommitted)?;
check::difficulty_threshold_and_time_are_valid(
candidate_block.header.difficulty_threshold,
difficulty_adjustment,
)?;
Ok(())
}
#[tracing::instrument(skip(candidate_header, network, relevant_headers))]
#[cfg(test)]
pub(crate) fn header_is_valid_for_recent_chain<C>(
candidate_header: &block::Header,
previous_block_height: block::Height,
network: &Network,
relevant_headers: C,
) -> Result<(), ValidateContextError>
where
C: IntoIterator<Item = (CompactDifficulty, chrono::DateTime<chrono::Utc>)>,
{
let candidate_height = previous_block_height
.next()
.map_err(|_| ValidateContextError::NotReadyToBeCommitted)?;
let relevant_headers = difficulty_context(network, candidate_height, relevant_headers);
let difficulty_adjustment = AdjustedDifficulty::new_from_header_time(
candidate_header.time,
previous_block_height,
network,
relevant_headers,
)
.map_err(|_| ValidateContextError::NotReadyToBeCommitted)?;
check::difficulty_threshold_and_time_are_valid(
candidate_header.difficulty_threshold,
difficulty_adjustment,
)?;
Ok(())
}
#[tracing::instrument(skip(block, history_tree))]
pub(crate) fn block_commitment_is_valid_for_chain_history(
block: Arc<Block>,
network: &Network,
history_tree: &HistoryTree,
precomputed_auth_data_root: Option<AuthDataRoot>,
) -> Result<(), ValidateContextError> {
match block.commitment(network)? {
block::Commitment::PreSaplingReserved(_)
| block::Commitment::FinalSaplingRoot(_)
| block::Commitment::ChainHistoryActivationReserved => {
Ok(())
}
block::Commitment::ChainHistoryRoot(actual_history_tree_root) => {
let history_tree_root = history_tree
.hash()
.expect("the history tree of the previous block must exist since the current block has a ChainHistoryRoot");
if actual_history_tree_root == history_tree_root {
Ok(())
} else {
Err(ValidateContextError::InvalidBlockCommitment(
CommitmentError::InvalidChainHistoryRoot {
actual: actual_history_tree_root.into(),
expected: history_tree_root.into(),
},
))
}
}
block::Commitment::ChainHistoryBlockTxAuthCommitment(actual_hash_block_commitments) => {
let history_tree_root = history_tree
.hash()
.or_else(|| {
(NetworkUpgrade::Heartwood.activation_height(network)
== block.coinbase_height())
.then_some(block::CHAIN_HISTORY_ACTIVATION_RESERVED.into())
})
.expect(
"the history tree of the previous block must exist \
since the current block has a ChainHistoryBlockTxAuthCommitment",
);
let auth_data_root =
precomputed_auth_data_root.unwrap_or_else(|| block.auth_data_root());
let hash_block_commitments = ChainHistoryBlockTxAuthCommitmentHash::from_commitments(
&history_tree_root,
&auth_data_root,
);
if actual_hash_block_commitments == hash_block_commitments {
Ok(())
} else {
Err(ValidateContextError::InvalidBlockCommitment(
CommitmentError::InvalidChainHistoryBlockTxAuthCommitment {
actual: actual_hash_block_commitments.into(),
expected: hash_block_commitments.into(),
},
))
}
}
}
}
fn block_is_not_orphaned(
finalized_tip_height: block::Height,
candidate_height: block::Height,
) -> Result<(), ValidateContextError> {
if candidate_height <= finalized_tip_height {
Err(ValidateContextError::OrphanedBlock {
candidate_height,
finalized_tip_height,
})
} else {
Ok(())
}
}
fn height_one_more_than_parent_height(
parent_height: block::Height,
candidate_height: block::Height,
) -> Result<(), ValidateContextError> {
if parent_height + 1 != Some(candidate_height) {
Err(ValidateContextError::NonSequentialBlock {
candidate_height,
parent_height,
})
} else {
Ok(())
}
}
pub(crate) fn difficulty_threshold_and_time_are_valid(
difficulty_threshold: CompactDifficulty,
difficulty_adjustment: AdjustedDifficulty,
) -> Result<(), ValidateContextError> {
zakura_header_chain::validate_contextual_difficulty_and_time(
difficulty_threshold,
difficulty_adjustment,
)
.map_err(|error| match error {
zakura_header_chain::ContextualValidationError::TimeTooEarly {
candidate_time,
median_time_past,
} => ValidateContextError::TimeTooEarly {
candidate_time,
median_time_past,
},
zakura_header_chain::ContextualValidationError::TimeTooLate {
candidate_time,
block_time_max,
} => ValidateContextError::TimeTooLate {
candidate_time,
block_time_max,
},
zakura_header_chain::ContextualValidationError::InvalidDifficultyThreshold {
difficulty_threshold,
expected_difficulty,
} => ValidateContextError::InvalidDifficultyThreshold {
difficulty_threshold,
expected_difficulty,
},
})
}
pub(crate) fn legacy_chain<I>(
nu5_activation_height: block::Height,
ancestors: I,
network: &Network,
max_legacy_chain_blocks: usize,
) -> Result<(), BoxError>
where
I: Iterator<Item = Arc<Block>>,
{
let mut ancestors = ancestors.peekable();
let tip_height = ancestors.peek().and_then(|block| block.coinbase_height());
for (index, block) in ancestors.enumerate() {
if block
.coinbase_height()
.expect("valid blocks have coinbase heights")
< nu5_activation_height
{
return Ok(());
}
if index >= max_legacy_chain_blocks {
return Err(format!(
"could not find any transactions in recent blocks: \
checked {index} blocks back from {:?}",
tip_height.expect("database contains valid blocks"),
)
.into());
}
block
.check_transaction_network_upgrade_consistency(network)
.map_err(|error| {
format!("inconsistent network upgrade found in transaction: {error:?}")
})?;
let has_network_upgrade = block
.transactions
.iter()
.find_map(|trans| trans.network_upgrade())
.is_some();
if has_network_upgrade {
return Ok(());
}
}
Ok(())
}
pub(crate) fn initial_contextual_validity(
finalized_state: &ZakuraDb,
non_finalized_state: &NonFinalizedState,
semantically_verified: &SemanticallyVerifiedBlock,
) -> Result<(), ValidateContextError> {
let relevant_headers = any_chain_ancestor_iter::<block::Header>(
non_finalized_state,
finalized_state,
semantically_verified.block.header.previous_block_hash,
);
let parent_height = relevant_headers.height;
check::block_is_valid_for_recent_chain(
semantically_verified,
&non_finalized_state.network,
finalized_state.finalized_tip_height(),
parent_height,
relevant_headers,
)?;
check::nullifier::no_duplicates_in_finalized_chain(semantically_verified, finalized_state)?;
Ok(())
}
#[cfg(test)]
mod nsm_value_balance_boundary_tests {
use super::*;
use zakura_chain::{
amount::{Amount, MAX_MONEY},
parameters::testnet::{ConfiguredActivationHeights, RegtestParameters},
};
#[test]
fn rejection_matrix_covers_activation_sign_and_arithmetic_limits() {
let network = Network::new_regtest(RegtestParameters {
activation_heights: ConfiguredActivationHeights {
nu7: Some(2),
..Default::default()
},
test_nsm_reissuance_height: Some(block::Height(3)),
..Default::default()
});
for height in [1, 2, 3, 4] {
for before in [-MAX_MONEY, -1, 0, 1, MAX_MONEY] {
for delta in [-MAX_MONEY, -1, 0, 1, MAX_MONEY] {
let mut pools = ValueBalance::<NonNegative>::zero();
pools.set_nsm_value_balance_amount(Amount::try_from(before).unwrap());
let mut change = ValueBalance::<NegativeAllowed>::zero();
change.set_nsm_value_balance_amount(Amount::try_from(delta).unwrap());
let result = nsm_value_balance_is_non_negative(
&network,
block::Height(height),
&pools,
&change,
);
let sum = i128::from(before) + i128::from(delta);
let reject = height >= 2 && !(0..=i128::from(MAX_MONEY)).contains(&sum);
assert_eq!(
result.is_err(),
reject,
"height {height}, before {before}, delta {delta}"
);
assert_eq!(i64::from(pools.nsm_value_balance_amount()), before);
assert_eq!(i64::from(change.nsm_value_balance_amount()), delta);
}
}
}
}
}