1use std::sync::Arc;
2
3use anyhow::{Context, Result, bail, ensure};
4use committable::Committable;
5use espresso_types::{Certificate2, Leaf2, SeqTypes};
6use hotshot_query_service_types::availability::LeafQueryData;
7use hotshot_types::{
8 data::EpochNumber,
9 epoch_membership::EpochMembership,
10 simple_certificate::QuorumCertificate2,
11 traits::block_contents::BlockHeader,
12 utils::{epoch_from_block_number, is_last_block},
13 vote::HasViewNumber,
14};
15use serde::{Deserialize, Serialize};
16use versions::{EPOCH_VERSION, NEW_PROTOCOL_VERSION};
17
18use super::quorum::{Certificate, ChainVersions, Quorum};
19use crate::consensus::quorum::StakeTableQuorum;
20
21#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
26pub enum FinalityProof {
27 #[default]
29 Assumption,
30
31 HotStuff2 {
42 committing_qc: Arc<Certificate>,
43 deciding_qc: Arc<Certificate>,
44 },
45
46 NewProtocol {
51 cert2: Arc<Certificate2<SeqTypes>>,
52 leaf_qc: Box<QuorumCertificate2<SeqTypes>>,
53 },
54
55 HotStuff {
60 precommit_qc: Arc<Certificate>,
61 committing_qc: Arc<Certificate>,
62 deciding_qc: Arc<Certificate>,
63 },
64}
65#[derive(Clone, Copy, Debug)]
70pub enum LeafProofHint<'a, Q> {
71 Quorum(&'a Q),
77
78 Assumption(&'a Leaf2),
83}
84
85impl<'a> LeafProofHint<'a, StakeTableQuorum<EpochMembership<SeqTypes>>> {
86 pub fn assumption(leaf: &'a Leaf2) -> Self {
93 Self::Assumption(leaf)
94 }
95}
96
97#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
99pub struct LeafProof {
100 leaves: Vec<Leaf2>,
107
108 proof: FinalityProof,
110}
111
112impl LeafProof {
113 pub async fn verify(
117 &self,
118 hint: LeafProofHint<'_, impl Quorum>,
119 ) -> Result<LeafQueryData<SeqTypes>> {
120 let mut leaves = self.leaves.iter();
121 let leaf = leaves.next().context("empty leaf chain")?;
122
123 let mut opt_qc = None;
126
127 let mut curr = leaf;
129 for next in leaves {
130 ensure!(curr.commit() == next.parent_commitment());
131 curr = next;
132
133 if opt_qc.is_none() {
134 opt_qc = Some(next.justify_qc().clone());
136 }
137 }
138
139 let final_qc = match (&self.proof, hint) {
141 (FinalityProof::Assumption, LeafProofHint::Assumption(finalized)) => {
142 ensure!(finalized.parent_commitment() == curr.commit());
145 finalized.justify_qc().clone()
146 },
147 (
148 FinalityProof::HotStuff2 {
149 committing_qc,
150 deciding_qc,
151 },
152 LeafProofHint::Quorum(quorum),
153 ) => {
154 let ChainVersions {
157 leaf: version,
158 max_cert,
159 } = quorum
160 .verify_qc_chain_and_get_version(curr, [&**committing_qc, &**deciding_qc])
161 .await?;
162
163 ensure!(version >= EPOCH_VERSION);
167 ensure!(
168 version < NEW_PROTOCOL_VERSION,
169 "HotStuff2 finality proof used for new-protocol leaf"
170 );
171 ensure!(
174 max_cert < NEW_PROTOCOL_VERSION,
175 "HotStuff2 finality proof spans the new-protocol cutover"
176 );
177
178 committing_qc.qc().clone()
179 },
180 (
181 FinalityProof::HotStuff {
182 precommit_qc,
183 committing_qc,
184 deciding_qc,
185 },
186 LeafProofHint::Quorum(quorum),
187 ) => {
188 let ChainVersions { leaf: version, .. } = quorum
191 .verify_qc_chain_and_get_version(
192 curr,
193 [&**precommit_qc, &**committing_qc, &**deciding_qc],
194 )
195 .await?;
196
197 ensure!(version < EPOCH_VERSION);
200
201 precommit_qc.qc().clone()
202 },
203 (FinalityProof::NewProtocol { cert2, leaf_qc }, LeafProofHint::Quorum(quorum)) => {
204 let version = curr.block_header().version();
206 ensure!(
207 version >= NEW_PROTOCOL_VERSION,
208 "new protocol finality proof used for pre-new-protocol leaf"
209 );
210 ensure!(
211 cert2.data.leaf_commit == curr.commit(),
212 "cert2 leaf commitment does not match leaf"
213 );
214 ensure!(
219 cert2.view_number() >= curr.view_number(),
220 "cert2 view number is earlier than the leaf"
221 );
222 ensure!(
223 cert2.data.block_number == curr.block_header().block_number(),
224 "cert2 block number does not match leaf"
225 );
226 if cert2.view_number() > curr.view_number() {
227 ensure!(
228 is_last_block(curr.height(), quorum.epoch_height()),
229 "cert2 view is after the leaf's, which is not the last block of its epoch"
230 );
231 }
232 let epoch = EpochNumber::new(epoch_from_block_number(
233 curr.height(),
234 quorum.epoch_height(),
235 ));
236 ensure!(
237 cert2.data.epoch == epoch,
238 "cert2 epoch {} does not match the leaf's epoch {epoch}",
239 cert2.data.epoch
240 );
241
242 quorum
243 .verify_cert2(cert2, version)
244 .await
245 .context("verifying cert2 signature")?;
246
247 leaf_qc.as_ref().clone()
248 },
249 (proof, hint) => {
250 let required = match proof {
251 FinalityProof::Assumption => "finalized leaf",
252 FinalityProof::HotStuff { .. }
253 | FinalityProof::HotStuff2 { .. }
254 | FinalityProof::NewProtocol { .. } => "quorum",
255 };
256 let supplied = match hint {
257 LeafProofHint::Assumption(..) => "finalized leaf",
258 LeafProofHint::Quorum(..) => "quorum",
259 };
260 bail!(
261 "verifier supplied wrong hint for proof: proof requires a {required} but \
262 supplied hint is {supplied}"
263 );
264 },
265 };
266
267 let qc = opt_qc.unwrap_or(final_qc);
270
271 let info = LeafQueryData::new(leaf.clone(), qc)?;
272 Ok(info)
273 }
274
275 pub fn push(&mut self, new_leaf: LeafQueryData<SeqTypes>) -> bool {
280 let len = self.leaves.len();
281
282 if len >= 2
286 && self.leaves[len - 2].block_header().version() >= EPOCH_VERSION
287 && self.leaves[len - 1].block_header().version() < NEW_PROTOCOL_VERSION
288 {
289 let committing_qc = Certificate::for_parent(&self.leaves[len - 1]);
290 let deciding_qc = Certificate::for_parent(new_leaf.leaf());
291 if committing_qc.view_number() == self.leaves[len - 2].view_number()
292 && deciding_qc.view_number() == committing_qc.view_number() + 1
293 {
294 debug_assert!(committing_qc.leaf_commit() == self.leaves[len - 2].commit());
297 debug_assert!(deciding_qc.leaf_commit() == self.leaves[len - 1].commit());
298
299 self.proof = FinalityProof::HotStuff2 {
300 committing_qc: Arc::new(committing_qc),
301 deciding_qc: Arc::new(deciding_qc),
302 };
303
304 self.leaves.pop();
307
308 return true;
309 }
310 }
311
312 if len >= 3 && self.leaves[len - 3].block_header().version() < EPOCH_VERSION {
315 let precommit_qc = Certificate::for_parent(&self.leaves[len - 2]);
316 let committing_qc = Certificate::for_parent(&self.leaves[len - 1]);
317 let deciding_qc = Certificate::for_parent(new_leaf.leaf());
318 if precommit_qc.view_number() == self.leaves[len - 3].view_number()
319 && committing_qc.view_number() == precommit_qc.view_number() + 1
320 && deciding_qc.view_number() == committing_qc.view_number() + 1
321 {
322 debug_assert!(precommit_qc.leaf_commit() == self.leaves[len - 3].commit());
325 debug_assert!(committing_qc.leaf_commit() == self.leaves[len - 2].commit());
326 debug_assert!(deciding_qc.leaf_commit() == self.leaves[len - 1].commit());
327
328 self.proof = FinalityProof::HotStuff {
329 precommit_qc: Arc::new(precommit_qc),
330 committing_qc: Arc::new(committing_qc),
331 deciding_qc: Arc::new(deciding_qc),
332 };
333
334 self.leaves.pop();
337 self.leaves.pop();
338
339 return true;
340 }
341 }
342
343 self.leaves.push(new_leaf.leaf().clone());
345 false
346 }
347
348 pub fn add_certificate(
350 &mut self,
351 cert2: Arc<Certificate2<SeqTypes>>,
352 leaf_qc: QuorumCertificate2<SeqTypes>,
353 ) {
354 debug_assert!(cert2.data.leaf_commit == self.leaves[self.leaves.len() - 1].commit());
355
356 self.proof = FinalityProof::NewProtocol {
357 cert2,
358 leaf_qc: Box::new(leaf_qc),
359 };
360 }
361
362 pub fn proof(&self) -> &FinalityProof {
364 &self.proof
365 }
366
367 pub fn leaves(&self) -> &[Leaf2] {
369 &self.leaves
370 }
371
372 pub fn epoch(&self, epoch_height: u64) -> Result<Option<EpochNumber>> {
378 if matches!(self.proof, FinalityProof::Assumption) {
379 Ok(None)
380 } else {
381 let height = self.leaves.last().context("empty leaf chain")?.height();
382 let epoch = epoch_from_block_number(height, epoch_height);
383 Ok(Some(epoch.into()))
384 }
385 }
386}
387
388#[cfg(test)]
389mod test {
390 use std::marker::PhantomData;
391
392 use hotshot_types::{data::ViewNumber, simple_vote::Vote2Data};
393 use pretty_assertions::assert_eq;
394 use versions::{DRB_AND_HEADER_UPGRADE_VERSION, Upgrade};
395
396 use super::*;
397 use crate::testing::{
398 AlwaysFalseQuorum, AlwaysTrueQuorum, LEAF_CHAIN_EPOCH_HEIGHT, LEGACY_VERSION,
399 VersionCheckQuorum, leaf_chain, leaf_chain_with_upgrade,
400 };
401
402 fn cert2_proof(leaf: &LeafQueryData<SeqTypes>, view: u64, epoch: u64) -> LeafProof {
404 let data = Vote2Data {
405 leaf_commit: leaf.leaf().commit(),
406 epoch: EpochNumber::new(epoch),
407 block_number: leaf.leaf().height(),
408 };
409 let commitment = data.commit();
411 let view = ViewNumber::new(view);
412 LeafProof {
413 leaves: vec![leaf.leaf().clone()],
414 proof: FinalityProof::NewProtocol {
415 cert2: Arc::new(Certificate2::new(data, commitment, view, None, PhantomData)),
416 leaf_qc: Box::new(leaf.qc().clone()),
417 },
418 }
419 }
420
421 fn hand_crafted_two_chain(
424 leaf: &LeafQueryData<SeqTypes>,
425 committing_qc: Certificate,
426 deciding_qc: Certificate,
427 ) -> LeafProof {
428 LeafProof {
429 leaves: vec![leaf.leaf().clone()],
430 proof: FinalityProof::HotStuff2 {
431 committing_qc: Arc::new(committing_qc),
432 deciding_qc: Arc::new(deciding_qc),
433 },
434 }
435 }
436
437 #[test_log::test(tokio::test(flavor = "multi_thread"))]
438 async fn test_hotstuff2() {
439 let mut proof = LeafProof::default();
440
441 let leaves = leaf_chain(1..=3, EPOCH_VERSION).await;
443 assert!(!proof.push(leaves[0].clone()));
444 assert!(!proof.push(leaves[1].clone()));
445 assert!(proof.push(leaves[2].clone()));
446 assert_eq!(
447 proof
448 .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum::new(
449 LEAF_CHAIN_EPOCH_HEIGHT
450 )))
451 .await
452 .unwrap(),
453 leaves[0]
454 );
455 }
456
457 #[test_log::test(tokio::test(flavor = "multi_thread"))]
458 async fn test_invalid_qc() {
459 let mut proof = LeafProof::default();
460
461 let leaves = leaf_chain(1..=3, EPOCH_VERSION).await;
463 assert!(!proof.push(leaves[0].clone()));
464 assert!(!proof.push(leaves[1].clone()));
465 assert!(proof.push(leaves[2].clone()));
466
467 assert_eq!(
469 proof
470 .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum::new(
471 LEAF_CHAIN_EPOCH_HEIGHT
472 )))
473 .await
474 .unwrap(),
475 leaves[0]
476 );
477 proof
479 .verify(LeafProofHint::Quorum(&AlwaysFalseQuorum::new(
480 LEAF_CHAIN_EPOCH_HEIGHT,
481 )))
482 .await
483 .unwrap_err();
484 }
485
486 #[test_log::test(tokio::test(flavor = "multi_thread"))]
487 async fn test_assumption() {
488 let mut proof = LeafProof::default();
489
490 let leaves = leaf_chain(1..=2, EPOCH_VERSION).await;
493 assert!(!proof.push(leaves[0].clone()));
494 assert_eq!(
495 proof
496 .verify(LeafProofHint::assumption(leaves[1].leaf()))
497 .await
498 .unwrap(),
499 leaves[0]
500 );
501 }
502
503 #[test_log::test(tokio::test(flavor = "multi_thread"))]
504 async fn test_no_chain() {
505 let mut proof = LeafProof::default();
506
507 let leaves = leaf_chain(1..=4, EPOCH_VERSION).await;
510 assert!(!proof.push(leaves[0].clone()));
511 assert!(!proof.push(leaves[2].clone()));
512
513 proof
517 .verify(LeafProofHint::assumption(leaves[3].leaf()))
518 .await
519 .unwrap_err();
520 }
521
522 #[test_log::test(tokio::test(flavor = "multi_thread"))]
523 async fn test_new_protocol_two_chain_rejected() {
524 let leaves = leaf_chain(1..=3, NEW_PROTOCOL_VERSION).await;
525
526 let mut proof = LeafProof::default();
528 assert!(!proof.push(leaves[0].clone()));
529 assert!(!proof.push(leaves[1].clone()));
530 assert!(!proof.push(leaves[2].clone()));
531
532 let proof = hand_crafted_two_chain(
534 &leaves[0],
535 Certificate::for_parent(leaves[1].leaf()),
536 Certificate::for_parent(leaves[2].leaf()),
537 );
538 let err = proof
539 .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum::new(
540 LEAF_CHAIN_EPOCH_HEIGHT,
541 )))
542 .await
543 .unwrap_err();
544 assert!(
545 err.to_string()
546 .contains("HotStuff2 finality proof used for new-protocol leaf"),
547 "{err:#}"
548 );
549 }
550
551 #[test_log::test(tokio::test(flavor = "multi_thread"))]
552 async fn test_cutover_two_chain_pre_cutover() {
553 let leaves = leaf_chain_with_upgrade(
556 1..=3,
557 3,
558 Upgrade::new(DRB_AND_HEADER_UPGRADE_VERSION, NEW_PROTOCOL_VERSION),
559 )
560 .await;
561 assert_eq!(leaves[0].header().version(), DRB_AND_HEADER_UPGRADE_VERSION);
562 assert_eq!(leaves[1].header().version(), DRB_AND_HEADER_UPGRADE_VERSION);
563 assert_eq!(leaves[2].header().version(), NEW_PROTOCOL_VERSION);
564
565 let mut proof = LeafProof::default();
566 assert!(!proof.push(leaves[0].clone()));
567 assert!(!proof.push(leaves[1].clone()));
568 assert!(proof.push(leaves[2].clone()));
569 assert!(matches!(proof.proof(), FinalityProof::HotStuff2 { .. }));
570 assert_eq!(
571 proof
572 .verify(LeafProofHint::Quorum(&VersionCheckQuorum::new(
573 leaves.iter().map(|leaf| leaf.leaf().clone()),
574 LEAF_CHAIN_EPOCH_HEIGHT
575 )))
576 .await
577 .unwrap(),
578 leaves[0]
579 );
580 }
581
582 #[test_log::test(tokio::test(flavor = "multi_thread"))]
583 async fn test_cutover_two_chain_spanning_rejected() {
584 let leaves = leaf_chain_with_upgrade(
587 1..=3,
588 2,
589 Upgrade::new(DRB_AND_HEADER_UPGRADE_VERSION, NEW_PROTOCOL_VERSION),
590 )
591 .await;
592 assert_eq!(leaves[0].header().version(), DRB_AND_HEADER_UPGRADE_VERSION);
593 assert_eq!(leaves[1].header().version(), NEW_PROTOCOL_VERSION);
594
595 let mut proof = LeafProof::default();
597 assert!(!proof.push(leaves[0].clone()));
598 assert!(!proof.push(leaves[1].clone()));
599 assert!(!proof.push(leaves[2].clone()));
600
601 let proof = hand_crafted_two_chain(
603 &leaves[0],
604 Certificate::for_parent(leaves[1].leaf()),
605 Certificate::for_parent(leaves[2].leaf()),
606 );
607 assert!(matches!(proof.proof(), FinalityProof::HotStuff2 { .. }));
608 let err = proof
609 .verify(LeafProofHint::Quorum(&VersionCheckQuorum::new(
610 leaves.iter().map(|leaf| leaf.leaf().clone()),
611 LEAF_CHAIN_EPOCH_HEIGHT,
612 )))
613 .await
614 .unwrap_err();
615 assert!(
616 err.to_string()
617 .contains("HotStuff2 finality proof spans the new-protocol cutover"),
618 "{err:#}"
619 );
620 }
621
622 #[test_log::test(tokio::test(flavor = "multi_thread"))]
623 async fn test_legacy_hotstuff_three_chain() {
624 let mut proof = LeafProof::default();
625
626 let leaves = leaf_chain(1..=4, LEGACY_VERSION).await;
628 assert!(!proof.push(leaves[0].clone()));
629 assert!(!proof.push(leaves[1].clone()));
630 assert!(!proof.push(leaves[2].clone()));
631 assert!(proof.push(leaves[3].clone()));
632 assert_eq!(
633 proof
634 .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum::new(
635 LEAF_CHAIN_EPOCH_HEIGHT
636 )))
637 .await
638 .unwrap(),
639 leaves[0]
640 );
641 }
642
643 #[test_log::test(tokio::test(flavor = "multi_thread"))]
644 async fn test_legacy_hotstuff_two_chain_only() {
645 let mut proof = LeafProof::default();
646
647 let leaves = leaf_chain(1..=3, LEGACY_VERSION).await;
650 assert!(!proof.push(leaves[0].clone()));
651 assert!(!proof.push(leaves[1].clone()));
652 assert!(!proof.push(leaves[2].clone()));
653 proof
654 .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum::new(
655 LEAF_CHAIN_EPOCH_HEIGHT,
656 )))
657 .await
658 .unwrap_err();
659 }
660
661 #[test_log::test(tokio::test(flavor = "multi_thread"))]
662 async fn test_new_protocol_later_cert2_only_for_last_block_of_epoch() {
663 let leaves = leaf_chain(1..=3, NEW_PROTOCOL_VERSION).await;
665 let quorum = AlwaysTrueQuorum::new(3);
666
667 let last = &leaves[2];
669 let revote = cert2_proof(last, *last.leaf().view_number() + 2, 1);
670 let verified = revote.verify(LeafProofHint::Quorum(&quorum)).await.unwrap();
671 assert_eq!(verified.leaf(), last.leaf());
672
673 let middle = &leaves[1];
675 let proof = cert2_proof(middle, *middle.leaf().view_number() + 2, 1);
676 let err = proof
677 .verify(LeafProofHint::Quorum(&quorum))
678 .await
679 .unwrap_err();
680 assert!(
681 err.to_string().contains("not the last block of its epoch"),
682 "{err:#}"
683 );
684 }
685}