Skip to main content

zebra_state/service/
check.rs

1//! Consensus critical contextual checks
2
3use std::sync::Arc;
4
5use chrono::Duration;
6
7use zebra_chain::{
8    block::{self, Block, ChainHistoryBlockTxAuthCommitmentHash, CommitmentError},
9    history_tree::HistoryTree,
10    parameters::{Network, NetworkUpgrade},
11    work::difficulty::CompactDifficulty,
12};
13
14use crate::{
15    service::{
16        block_iter::{any_chain_ancestor_iter, Iter},
17        check::difficulty::pow_adjustment_block_span,
18        finalized_state::ZebraDb,
19        non_finalized_state::NonFinalizedState,
20    },
21    BoxError, SemanticallyVerifiedBlock, ValidateContextError,
22};
23
24use zebra_chain::{
25    amount::{Amount, NonNegative},
26    parameters::subsidy::{self, CoinbaseTransactionError, SubsidyError},
27    value_balance::ValueBalance,
28};
29
30use crate::ContextuallyVerifiedBlock;
31
32// use self as check
33use super::check;
34
35// These types are used in doc links
36#[allow(unused_imports)]
37use crate::service::non_finalized_state::Chain;
38
39pub(crate) mod anchors;
40pub(crate) mod difficulty;
41pub(crate) mod nullifier;
42pub(crate) mod utxo;
43
44pub use utxo::transparent_coinbase_spend;
45
46#[cfg(test)]
47mod tests;
48
49pub(crate) use difficulty::AdjustedDifficulty;
50
51/// Check that the semantically verified block is contextually valid for `network`,
52/// based on the `finalized_tip_height` and `relevant_chain`.
53///
54/// This function performs checks that require a small number of recent headers,
55/// including previous height and block difficulty.
56///
57/// The relevant chain is an iterator over the ancestor headers of `block`, starting
58/// with its parent. Its initial height comes from the parent hash's state index.
59#[tracing::instrument(skip(semantically_verified, finalized_tip_height, relevant_chain))]
60pub(crate) fn block_is_valid_for_recent_chain(
61    semantically_verified: &SemanticallyVerifiedBlock,
62    network: &Network,
63    finalized_tip_height: Option<block::Height>,
64    relevant_chain: Iter<block::Header>,
65) -> Result<(), ValidateContextError> {
66    let finalized_tip_height = finalized_tip_height
67        .expect("finalized state must contain at least one block to do contextual validation");
68    check::block_is_not_orphaned(finalized_tip_height, semantically_verified.height)?;
69
70    // ZIP 218 makes the averaging window, and so the block span, depend on the block's height.
71    let block_span = pow_adjustment_block_span(network, semantically_verified.height);
72
73    let parent_height = relevant_chain.height;
74    let relevant_data: Vec<_> = relevant_chain
75        .take(block_span)
76        .map(|header| (header.difficulty_threshold, header.time))
77        .collect();
78
79    if relevant_data.is_empty() {
80        warn!(
81            ?semantically_verified,
82            ?finalized_tip_height,
83            "state must contain parent block to do contextual validation"
84        );
85
86        return Err(ValidateContextError::NotReadyToBeCommitted);
87    }
88
89    let parent_height = parent_height.expect("a parent header has an indexed height");
90    check::height_one_more_than_parent_height(parent_height, semantically_verified.height)?;
91
92    // skip this check during tests if we don't have enough blocks in the chain
93    // process_queued also checks the chain length, so we can skip this assertion during testing
94    // (tests that want to check this code should use the correct number of blocks)
95    //
96    // TODO: accept a NotReadyToBeCommitted error in those tests instead
97    #[cfg(test)]
98    if relevant_data.len() < block_span {
99        return Ok(());
100    }
101
102    // In production, blocks without enough context are invalid.
103    //
104    // The BlockVerifierRouter makes sure that the first 1 million blocks (or more) are
105    // checkpoint verified. The state queues and block write task make sure that blocks are
106    // committed in strict height order. But this function is only called on semantically
107    // verified blocks, so there will be at least 1 million blocks in the state when it is
108    // called. So this error should never happen on Mainnet or the default Testnet.
109    //
110    // It's okay to use a relevant chain of fewer than `block_span` blocks, because
111    // the MedianTime function uses height 0 if passed a negative height by the ActualTimespan function:
112    // > ActualTimespan(height : N) := MedianTime(height) − MedianTime(height − PoWAveragingWindow)
113    // > MedianTime(height : N) := median([[ nTime(𝑖) for 𝑖 from max(0, height − PoWMedianBlockSpan) up to height − 1 ]])
114    // and the MeanTarget function only requires the past `PoWAveragingWindow` (17) blocks for heights above 17,
115    // > PoWLimit, if height ≤ PoWAveragingWindow
116    // > ([ToTarget(nBits(𝑖)) for 𝑖 from height−PoWAveragingWindow up to height−1]) otherwise
117    //
118    // See the 'Difficulty Adjustment' section (page 132) in the Zcash specification.
119    #[cfg(not(test))]
120    if relevant_data.is_empty() {
121        return Err(ValidateContextError::NotReadyToBeCommitted);
122    }
123
124    let difficulty_adjustment =
125        AdjustedDifficulty::new_from_block(&semantically_verified.block, network, relevant_data);
126    check::difficulty_threshold_and_time_are_valid(
127        semantically_verified.block.header.difficulty_threshold,
128        difficulty_adjustment,
129    )?;
130
131    Ok(())
132}
133
134/// Check that `block` is contextually valid for `network`, using
135/// the `history_tree` up to and including the previous block.
136#[tracing::instrument(skip(block, history_tree))]
137pub(crate) fn block_commitment_is_valid_for_chain_history(
138    block: Arc<Block>,
139    network: &Network,
140    history_tree: &HistoryTree,
141) -> Result<(), ValidateContextError> {
142    match block.commitment(network)? {
143        block::Commitment::PreSaplingReserved(_)
144        | block::Commitment::FinalSaplingRoot(_)
145        | block::Commitment::ChainHistoryActivationReserved => {
146            // # Consensus
147            //
148            // > [Sapling and Blossom only, pre-Heartwood] hashLightClientRoot MUST
149            // > be LEBS2OSP_{256}(rt^{Sapling}) where rt^{Sapling} is the root of
150            // > the Sapling note commitment tree for the final Sapling treestate of
151            // > this block .
152            //
153            // https://zips.z.cash/protocol/protocol.pdf#blockheader
154            //
155            // We don't need to validate this rule since we checkpoint on Canopy.
156            //
157            // We also don't need to do anything in the other cases.
158            Ok(())
159        }
160        block::Commitment::ChainHistoryRoot(actual_history_tree_root) => {
161            // # Consensus
162            //
163            // > [Heartwood and Canopy only, pre-NU5] hashLightClientRoot MUST be set to the
164            // > hashChainHistoryRoot for this block , as specified in [ZIP-221].
165            //
166            // https://zips.z.cash/protocol/protocol.pdf#blockheader
167            //
168            // The network is checked by [`Block::commitment`] above; it will only
169            // return the chain history root if it's Heartwood or Canopy.
170            let history_tree_root = history_tree
171                .hash()
172                .expect("the history tree of the previous block must exist since the current block has a ChainHistoryRoot");
173            if actual_history_tree_root == history_tree_root {
174                Ok(())
175            } else {
176                Err(ValidateContextError::InvalidBlockCommitment(
177                    CommitmentError::InvalidChainHistoryRoot {
178                        actual: actual_history_tree_root.into(),
179                        expected: history_tree_root.into(),
180                    },
181                ))
182            }
183        }
184        block::Commitment::ChainHistoryBlockTxAuthCommitment(actual_hash_block_commitments) => {
185            // # Consensus
186            //
187            // > [NU5 onward] hashBlockCommitments MUST be set to the value of
188            // > hashBlockCommitments for this block, as specified in [ZIP-244].
189            //
190            // The network is checked by [`Block::commitment`] above; it will only
191            // return the block commitments if it's NU5 onward.
192            let history_tree_root = history_tree
193                .hash()
194                .or_else(|| {
195                    (NetworkUpgrade::Heartwood.activation_height(network)
196                        == block.coinbase_height())
197                    .then_some(block::CHAIN_HISTORY_ACTIVATION_RESERVED.into())
198                })
199                .expect(
200                    "the history tree of the previous block must exist \
201                 since the current block has a ChainHistoryBlockTxAuthCommitment",
202                );
203            let auth_data_root = block.auth_data_root();
204
205            let hash_block_commitments = ChainHistoryBlockTxAuthCommitmentHash::from_commitments(
206                &history_tree_root,
207                &auth_data_root,
208            );
209
210            if actual_hash_block_commitments == hash_block_commitments {
211                Ok(())
212            } else {
213                Err(ValidateContextError::InvalidBlockCommitment(
214                    CommitmentError::InvalidChainHistoryBlockTxAuthCommitment {
215                        actual: actual_hash_block_commitments.into(),
216                        expected: hash_block_commitments.into(),
217                    },
218                ))
219            }
220        }
221    }
222}
223
224/// Returns `ValidateContextError::OrphanedBlock` if the height of the given
225/// block is less than or equal to the finalized tip height.
226fn block_is_not_orphaned(
227    finalized_tip_height: block::Height,
228    candidate_height: block::Height,
229) -> Result<(), ValidateContextError> {
230    if candidate_height <= finalized_tip_height {
231        Err(ValidateContextError::OrphanedBlock {
232            candidate_height,
233            finalized_tip_height,
234        })
235    } else {
236        Ok(())
237    }
238}
239
240/// Returns `ValidateContextError::NonSequentialBlock` if the block height isn't
241/// equal to the parent_height+1.
242fn height_one_more_than_parent_height(
243    parent_height: block::Height,
244    candidate_height: block::Height,
245) -> Result<(), ValidateContextError> {
246    if parent_height + 1 != Some(candidate_height) {
247        Err(ValidateContextError::NonSequentialBlock {
248            candidate_height,
249            parent_height,
250        })
251    } else {
252        Ok(())
253    }
254}
255
256/// Validate the time and `difficulty_threshold` from a candidate block's
257/// header.
258///
259/// Uses the `difficulty_adjustment` context for the block to:
260///   * check that the candidate block's time is within the valid range,
261///     based on the network and  candidate height, and
262///   * check that the expected difficulty is equal to the block's
263///     `difficulty_threshold`.
264///
265/// These checks are performed together, because the time field is used to
266/// calculate the expected difficulty adjustment.
267fn difficulty_threshold_and_time_are_valid(
268    difficulty_threshold: CompactDifficulty,
269    difficulty_adjustment: AdjustedDifficulty,
270) -> Result<(), ValidateContextError> {
271    // Check the block header time consensus rules from the Zcash specification
272    let candidate_height = difficulty_adjustment.candidate_height();
273    let candidate_time = difficulty_adjustment.candidate_time();
274    let network = difficulty_adjustment.network();
275    let median_time_past = difficulty_adjustment.median_time_past();
276    let block_time_max =
277        median_time_past + Duration::seconds(difficulty::BLOCK_MAX_TIME_SINCE_MEDIAN.into());
278
279    // # Consensus
280    //
281    // > For each block other than the genesis block, `nTime` MUST be strictly greater
282    // than the median-time-past of that block.
283    //
284    // https://zips.z.cash/protocol/protocol.pdf#blockheader
285    let genesis_height = NetworkUpgrade::Genesis
286        .activation_height(&network)
287        .expect("Zebra always has a genesis height available");
288
289    if candidate_time <= median_time_past && candidate_height != genesis_height {
290        Err(ValidateContextError::TimeTooEarly {
291            candidate_time,
292            median_time_past,
293        })?
294    }
295
296    // # Consensus
297    //
298    // > For each block at block height 2 or greater on Mainnet, or block height 653_606
299    // or greater on Testnet, `nTime` MUST be less than or equal to the median-time-past
300    // of that block plus 90*60 seconds.
301    //
302    // https://zips.z.cash/protocol/protocol.pdf#blockheader
303    if network.is_max_block_time_enforced(candidate_height) && candidate_time > block_time_max {
304        Err(ValidateContextError::TimeTooLate {
305            candidate_time,
306            block_time_max,
307        })?
308    }
309
310    // # Consensus
311    //
312    // > For a block at block height `Height`, `nBits` MUST be equal to `ThresholdBits(Height)`.
313    //
314    // https://zips.z.cash/protocol/protocol.pdf#blockheader
315    let expected_difficulty = difficulty_adjustment.expected_difficulty_threshold();
316    if difficulty_threshold != expected_difficulty {
317        Err(ValidateContextError::InvalidDifficultyThreshold {
318            difficulty_threshold,
319            expected_difficulty,
320        })?
321    }
322
323    Ok(())
324}
325
326/// Check if zebra is following a legacy chain and return an error if so.
327///
328/// `nu5_activation_height` should be `NetworkUpgrade::Nu5.activation_height(network)`, and
329/// `max_legacy_chain_blocks` should be [`MAX_LEGACY_CHAIN_BLOCKS`](crate::constants::MAX_LEGACY_CHAIN_BLOCKS).
330/// They are only changed from the defaults for testing.
331pub(crate) fn legacy_chain<I>(
332    nu5_activation_height: block::Height,
333    ancestors: I,
334    network: &Network,
335    max_legacy_chain_blocks: usize,
336) -> Result<(), BoxError>
337where
338    I: Iterator<Item = Arc<Block>>,
339{
340    let mut ancestors = ancestors.peekable();
341    let tip_height = ancestors.peek().and_then(|block| block.coinbase_height());
342
343    for (index, block) in ancestors.enumerate() {
344        // Stop checking if the chain reaches Canopy. We won't find any more V5 transactions,
345        // so the rest of our checks are useless.
346        //
347        // If the cached tip is close to NU5 activation, but there aren't any V5 transactions in the
348        // chain yet, we could reach MAX_BLOCKS_TO_CHECK in Canopy, and incorrectly return an error.
349        if block
350            .coinbase_height()
351            .expect("valid blocks have coinbase heights")
352            < nu5_activation_height
353        {
354            return Ok(());
355        }
356
357        // If we are past our NU5 activation height, but there are no V5 transactions in recent blocks,
358        // the last Zebra instance that updated this cached state had no NU5 activation height.
359        if index >= max_legacy_chain_blocks {
360            return Err(format!(
361                "could not find any transactions in recent blocks: \
362                 checked {index} blocks back from {:?}",
363                tip_height.expect("database contains valid blocks"),
364            )
365            .into());
366        }
367
368        // If a transaction `network_upgrade` field is different from the network upgrade calculated
369        // using our activation heights, the Zebra instance that verified those blocks had different
370        // network upgrade heights.
371        block
372            .check_transaction_network_upgrade_consistency(network)
373            .map_err(|error| {
374                format!("inconsistent network upgrade found in transaction: {error:?}")
375            })?;
376
377        // If we find at least one transaction with a valid `network_upgrade` field, the Zebra instance that
378        // verified those blocks used the same network upgrade heights. (Up to this point in the chain.)
379        let has_network_upgrade = block
380            .transactions
381            .iter()
382            .find_map(|trans| trans.network_upgrade())
383            .is_some();
384        if has_network_upgrade {
385            return Ok(());
386        }
387    }
388
389    Ok(())
390}
391
392/// Perform initial contextual validity checks for the configured network,
393/// based on the committed finalized and non-finalized state.
394///
395/// Additional contextual validity checks are performed by the non-finalized [`Chain`].
396pub(crate) fn initial_contextual_validity(
397    finalized_state: &ZebraDb,
398    non_finalized_state: &NonFinalizedState,
399    semantically_verified: &SemanticallyVerifiedBlock,
400) -> Result<(), ValidateContextError> {
401    let relevant_chain = any_chain_ancestor_iter::<block::Header>(
402        non_finalized_state,
403        finalized_state,
404        semantically_verified.block.header.previous_block_hash,
405    );
406
407    // Security: check proof of work before any other checks
408    check::block_is_valid_for_recent_chain(
409        semantically_verified,
410        &non_finalized_state.network,
411        finalized_state.finalized_tip_height(),
412        relevant_chain,
413    )?;
414
415    check::nullifier::no_duplicates_in_finalized_chain(semantically_verified, finalized_state)?;
416
417    Ok(())
418}
419
420/// Checks the block subsidy, funding streams, and miner fees paid by `contextual`'s coinbase
421/// transaction, from the NSM reissuance height.
422///
423/// The block verifier checks the subsidy, funding streams and miner fees before the deployment
424/// height. From then on the block subsidy depends on the NSM value balance after the parent block,
425/// which is only known during contextual validation, so the checks run here instead.
426///
427/// [zip]: https://zips.z.cash/zip-0237
428pub(crate) fn nsm_subsidy_is_valid(
429    contextual: &ContextuallyVerifiedBlock,
430    network: &Network,
431    parent_chain_value_pools: ValueBalance<NonNegative>,
432    block_miner_fees: Amount<NonNegative>,
433) -> Result<(), ValidateContextError> {
434    let invalid_subsidy =
435        |subsidy_error: CoinbaseTransactionError| ValidateContextError::InvalidSubsidy {
436            subsidy_error,
437            height: contextual.height,
438            block_hash: contextual.hash,
439        };
440
441    let expected_block_subsidy = subsidy::block_subsidy_with_parent_pools(
442        contextual.height,
443        network,
444        parent_chain_value_pools,
445    )
446    .map_err(|error| invalid_subsidy(error.into()))?;
447
448    let deferred_pool_balance_change =
449        subsidy::subsidy_is_valid(&contextual.block, network, expected_block_subsidy)
450            .map_err(invalid_subsidy)?;
451
452    let coinbase_tx = contextual
453        .block
454        .transactions
455        .first()
456        .ok_or_else(|| invalid_subsidy(SubsidyError::NoCoinbase.into()))?;
457
458    subsidy::miner_fees_are_valid(
459        coinbase_tx,
460        contextual.height,
461        block_miner_fees,
462        expected_block_subsidy,
463        deferred_pool_balance_change,
464        network,
465    )
466    .map_err(invalid_subsidy)
467}