Skip to main content

hotshot_new_protocol/
utils.rs

1use anyhow::{anyhow, ensure};
2use committable::Committable;
3use hotshot_types::{
4    data::{EpochNumber, Leaf2, ViewNumber},
5    epoch_membership::EpochMembershipCoordinator,
6    message::UpgradeLock,
7    stake_table::StakeTableEntries,
8    traits::node_implementation::NodeType,
9    utils::epoch_from_block_number,
10    vote::{Certificate, HasViewNumber},
11};
12
13use crate::message::Certificate2;
14
15/// Verify that a leaf is finalized by a new-protocol Certificate2.
16///
17/// `cert2` directly commits the newest leaf in `leaf_chain`. By the indirect
18/// commit rule, every ancestor of that leaf is finalized as well. This verifier
19/// validates `cert2`, then walks backward through the certified leaf's parent
20/// links until it finds `expected_height`.
21///
22/// View numbers are allowed to skip after timeouts, so the input may contain
23/// leaves that are not on the certified ancestry path. Those leaves are ignored;
24/// every accepted step must match the current leaf's justify QC, parent
25/// commitment, and block height.
26pub async fn verify_new_protocol_leaf_chain<T: NodeType>(
27    mut leaf_chain: Vec<Leaf2<T>>,
28    coordinator: &EpochMembershipCoordinator<T>,
29    expected_height: u64,
30    upgrade_lock: &UpgradeLock<T>,
31    cert2: Certificate2<T>,
32) -> anyhow::Result<Leaf2<T>> {
33    leaf_chain.sort_by_key(|l| l.view_number());
34    leaf_chain.reverse();
35
36    ensure!(!leaf_chain.is_empty(), "empty leaf chain");
37    let newest = &leaf_chain[0];
38
39    ensure!(
40        cert2.view_number() > ViewNumber::genesis(),
41        "cert2 must not be the genesis view"
42    );
43    let epoch = EpochNumber::new(epoch_from_block_number(
44        cert2.data.block_number,
45        *coordinator.epoch_height(),
46    ));
47    ensure!(
48        cert2.data.epoch == epoch,
49        "cert2 epoch {} does not match epoch {epoch} derived from its block number {}",
50        cert2.data.epoch,
51        cert2.data.block_number
52    );
53
54    let membership = coordinator
55        .stake_table_for_epoch(Some(epoch))
56        .map_err(|err| anyhow!("no stake table available for epoch {epoch}: {err:?}"))?;
57    let entries = StakeTableEntries::<T>::from_iter(membership.stake_table()).0;
58    cert2.is_valid_cert(&entries, membership.success_threshold(), upgrade_lock)?;
59
60    ensure!(
61        cert2.data.leaf_commit == newest.commit(),
62        "cert2 does not match the newest leaf in the chain"
63    );
64    ensure!(
65        cert2.data.block_number == newest.height(),
66        "cert2 block number does not match the newest leaf"
67    );
68    ensure!(
69        cert2.view_number() == newest.view_number(),
70        "cert2 view does not match the newest leaf"
71    );
72
73    if newest.height() == expected_height {
74        return Ok(newest.clone());
75    }
76
77    let mut current = newest;
78    for leaf in leaf_chain[1..].iter() {
79        let justify_qc = current.justify_qc();
80        if justify_qc.view_number() != leaf.view_number()
81            || justify_qc.data().leaf_commit != leaf.commit()
82        {
83            tracing::warn!(
84                view = ?leaf.view_number(),
85                expected_view = ?justify_qc.view_number(),
86                "leaf is off the leafchain path; expected only after a view timeout"
87            );
88            continue;
89        }
90        ensure!(
91            current.parent_commitment() == leaf.commit(),
92            "current leaf parent commitment does not match parent leaf"
93        );
94        ensure!(
95            leaf.height().checked_add(1) == Some(current.height()),
96            "leaf heights do not chain"
97        );
98        let qc_epoch = justify_qc
99            .data()
100            .epoch
101            .ok_or_else(|| anyhow!("justify QC at height {} is missing an epoch", leaf.height()))?;
102        let membership = coordinator
103            .stake_table_for_epoch(Some(qc_epoch))
104            .map_err(|err| anyhow!("no stake table available for epoch {qc_epoch}: {err:?}"))?;
105        let entries = StakeTableEntries::<T>::from_iter(membership.stake_table()).0;
106        justify_qc.is_valid_cert(&entries, membership.success_threshold(), upgrade_lock)?;
107        if leaf.height() == expected_height {
108            return Ok(leaf.clone());
109        }
110        current = leaf;
111    }
112
113    Err(anyhow!(
114        "expected height was not found in the cert2-finalized chain"
115    ))
116}