hotshot_new_protocol/
utils.rs1use 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
15pub 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}