1use std::sync::Arc;
2
3use alloy::primitives::U256;
4use committable::Committable;
5use hotshot_contract_adapter::light_client::validate_light_client_state_update_certificate;
6use hotshot_types::{
7 data::{EpochNumber, Leaf2, VidDisperseShare2, ViewNumber, vid_disperse::vid_total_weight},
8 epoch_membership::{EpochMembership, EpochMembershipCoordinator},
9 message::{Proposal as SignedProposal, UpgradeLock},
10 simple_certificate::{
11 LightClientStateUpdateCertificateV2, SimpleCertificate, SuccessThreshold, TimeoutEvidence,
12 check_qc_state_cert_correspondence,
13 },
14 simple_vote::{HasEpoch, QuorumData2, QuorumMarker, Voteable},
15 stake_table::StakeTableEntries,
16 traits::{
17 block_contents::BlockHeader, node_implementation::NodeType, signature_key::SignatureKey,
18 },
19 utils::{epoch_from_block_number, is_epoch_root, is_last_block},
20 vote::HasViewNumber,
21};
22use hotshot_utils::anytrace::{self, ensure};
23use tokio::task::JoinSet;
24use tracing::error;
25
26use crate::{
27 cert_verifier::{verify_signatures, verify_timeout3},
28 message::{
29 Certificate1, Certificate2, Proposal, ProposalMessage, Unchecked, Validated,
30 VidShareMessage,
31 },
32 upgrade::expected_upgrade_data,
33};
34
35type Result<T, E = ValidationError> = std::result::Result<T, E>;
36
37pub struct ValidatedProposal<T: NodeType> {
39 pub sender: T::SignatureKey,
40 pub message: ProposalMessage<T, Validated>,
41 pub fetched: bool,
43}
44
45pub struct ProposalValidator<T: NodeType> {
47 tasks: JoinSet<Result<ValidatedProposal<T>>>,
49
50 validator: Arc<Validator<T>>,
52}
53
54pub struct VidShareValidator<T: NodeType> {
60 tasks: JoinSet<Result<VidDisperseShare2<T>>>,
62
63 validator: Arc<Validator<T>>,
65}
66
67struct Validator<T: NodeType> {
68 membership_coordinator: EpochMembershipCoordinator<T>,
69 epoch_height: u64,
70 upgrade_lock: UpgradeLock<T>,
71
72 genesis_qc: Option<QuorumData2<T>>,
78}
79
80impl<T: NodeType> ProposalValidator<T> {
81 pub fn new(
84 c: EpochMembershipCoordinator<T>,
85 epoch_height: u64,
86 upgrade_lock: UpgradeLock<T>,
87 genesis_qc: Option<&Certificate1<T>>,
88 ) -> Self {
89 Self {
90 tasks: JoinSet::new(),
91 validator: Arc::new(Validator {
92 membership_coordinator: c,
93 epoch_height,
94 upgrade_lock,
95 genesis_qc: genesis_qc.map(|qc| qc.data),
96 }),
97 }
98 }
99
100 pub fn validate(&mut self, p: ProposalMessage<T, Unchecked>) {
101 self.spawn_validation(p, false)
102 }
103
104 pub fn validate_fetched(&mut self, p: ProposalMessage<T, Unchecked>) {
108 self.spawn_validation(p, true)
109 }
110
111 fn spawn_validation(&mut self, p: ProposalMessage<T, Unchecked>, fetched: bool) {
112 let v = self.validator.clone();
113 self.tasks.spawn(async move {
114 let parts = well_formed(&p.proposal.data, v.epoch_height)?;
115 let sender = v.signature(&p.proposal).await?;
116 v.certificates(&p.proposal.data, &parts).await?;
117 v.state_cert(&p.proposal.data, &parts).await?;
118 v.upgrade_certificate(&p.proposal.data).await?;
119 let validated_proposal = ValidatedProposal {
120 sender,
121 message: ProposalMessage::validated(p.proposal),
122 fetched,
123 };
124 Ok(validated_proposal)
125 });
126 }
127
128 pub async fn next(&mut self) -> Option<Result<ValidatedProposal<T>>> {
129 loop {
130 match self.tasks.join_next().await {
131 Some(Ok(prop)) => return Some(prop),
132 Some(Err(err)) => {
133 error!(%err, "proposal validation task panic");
134 },
135 None => return None,
136 }
137 }
138 }
139}
140
141impl<T: NodeType> VidShareValidator<T> {
142 pub fn new(
143 c: EpochMembershipCoordinator<T>,
144 epoch_height: u64,
145 upgrade_lock: UpgradeLock<T>,
146 ) -> Self {
147 Self {
148 tasks: JoinSet::new(),
149 validator: Arc::new(Validator {
150 membership_coordinator: c,
151 epoch_height,
152 upgrade_lock,
153 genesis_qc: None,
154 }),
155 }
156 }
157
158 pub fn validate(&mut self, share: VidShareMessage<T>) {
159 let v = self.validator.clone();
160 self.tasks.spawn(async move {
161 v.vid_share_proposal(&share).await?;
162 Ok(share.data)
163 });
164 }
165
166 pub async fn next(&mut self) -> Option<Result<VidDisperseShare2<T>>> {
167 loop {
168 match self.tasks.join_next().await {
169 Some(Ok(share)) => return Some(share),
170 Some(Err(err)) => {
171 error!(%err, "vid share validation task panic");
172 },
173 None => return None,
174 }
175 }
176 }
177}
178
179pub(crate) fn well_formed<T: NodeType>(
185 proposal: &Proposal<T>,
186 epoch_height: u64,
187) -> Result<Parts<'_, T>, MalformedProposal> {
188 epoch_matches_height(proposal, epoch_height)?;
189 let justify_qc_epoch = justify_qc_matches_parent(proposal, epoch_height)?;
190 Ok(Parts {
191 justify_qc_epoch,
192 state_cert: state_cert_matches_parent(proposal, epoch_height)?,
193 next_epoch_justify_qc: next_epoch_justify_qc_matches_parent(
194 proposal,
195 epoch_height,
196 justify_qc_epoch,
197 )?,
198 view_change_evidence: view_change_evidence_matches_parent(proposal)?,
199 })
200}
201
202pub(crate) struct Parts<'a, T: NodeType> {
204 pub(crate) justify_qc_epoch: EpochNumber,
206
207 pub(crate) state_cert: Option<&'a LightClientStateUpdateCertificateV2<T>>,
209
210 pub(crate) next_epoch_justify_qc: Option<&'a Certificate2<T>>,
212
213 pub(crate) view_change_evidence: Option<&'a TimeoutEvidence<T>>,
215}
216
217pub(crate) fn epoch_matches_height<T: NodeType>(
219 proposal: &Proposal<T>,
220 epoch_height: u64,
221) -> Result<(), MalformedProposal> {
222 let block_number = proposal.block_header.block_number();
223 let expected = EpochNumber::new(epoch_from_block_number(block_number, epoch_height));
224 if proposal.epoch != expected {
225 return Err(MalformedProposal::Epoch {
226 view: proposal.view_number(),
227 block_number,
228 expected,
229 claimed: proposal.epoch,
230 });
231 }
232 Ok(())
233}
234
235pub(crate) fn justify_qc_matches_parent<T: NodeType>(
250 proposal: &Proposal<T>,
251 epoch_height: u64,
252) -> Result<EpochNumber, MalformedProposal> {
253 let view = proposal.view_number();
254 let Some(claimed_epoch) = proposal.justify_qc.epoch() else {
255 return Err(MalformedProposal::JustifyQcWithoutEpoch(view));
256 };
257 let parent_block = proposal.block_header.block_number().saturating_sub(1);
258 let expected_epoch = EpochNumber::new(epoch_from_block_number(parent_block, epoch_height));
259 if claimed_epoch != expected_epoch {
260 return Err(MalformedProposal::JustifyQcEpoch {
261 view,
262 parent_block,
263 expected: expected_epoch,
264 claimed: claimed_epoch,
265 });
266 }
267 let Some(claimed_block) = proposal.justify_qc.data.block_number else {
268 return Err(MalformedProposal::JustifyQcWithoutBlockNumber(view));
269 };
270 if claimed_block != parent_block {
271 return Err(MalformedProposal::JustifyQcBlockNumber {
272 view,
273 expected: parent_block,
274 claimed: claimed_block,
275 });
276 }
277 if proposal.justify_qc.view_number() == ViewNumber::genesis() && claimed_block != 0 {
278 return Err(MalformedProposal::GenesisJustifyQcBlockNumber {
279 view,
280 claimed: claimed_block,
281 });
282 }
283 Ok(claimed_epoch)
284}
285
286pub(crate) fn state_cert_matches_parent<T: NodeType>(
298 proposal: &Proposal<T>,
299 epoch_height: u64,
300) -> Result<Option<&LightClientStateUpdateCertificateV2<T>>, MalformedProposal> {
301 let view = proposal.view_number();
302 let requires_state_cert = proposal
303 .justify_qc
304 .data
305 .block_number
306 .is_some_and(|bn| is_epoch_root(bn, epoch_height));
307 match (proposal.state_cert.as_ref(), requires_state_cert) {
308 (Some(_), false) => Err(MalformedProposal::StateCertUnexpected(view)),
309 (None, true) => Err(MalformedProposal::StateCertMissing(view)),
310 (state_cert, _) => Ok(state_cert),
311 }
312}
313
314pub(crate) fn next_epoch_justify_qc_matches_parent<T: NodeType>(
328 proposal: &Proposal<T>,
329 epoch_height: u64,
330 justify_qc_epoch: EpochNumber,
331) -> Result<Option<&Certificate2<T>>, MalformedProposal> {
332 let view = proposal.view_number();
333 let parent_block = proposal.block_header.block_number().saturating_sub(1);
334 if !is_last_block(parent_block, epoch_height) {
335 if proposal.next_epoch_justify_qc.is_some() {
336 return Err(MalformedProposal::NextEpochJustifyQcUnexpected(view));
337 }
338 return Ok(None);
339 }
340 let Some(cert2) = proposal.next_epoch_justify_qc.as_ref() else {
341 return Err(MalformedProposal::NextEpochJustifyQcMissing(view));
342 };
343 if cert2.data.leaf_commit != proposal.justify_qc.data.leaf_commit {
344 return Err(MalformedProposal::NextEpochJustifyQcLeafCommit(view));
345 }
346 let parent_view = proposal.justify_qc.view_number();
347 if cert2.view_number() < parent_view
348 || cert2.data.epoch != justify_qc_epoch
349 || cert2.data.block_number != parent_block
350 {
351 return Err(MalformedProposal::NextEpochJustifyQcParent {
352 view,
353 claimed_view: cert2.view_number(),
354 claimed_epoch: cert2.data.epoch,
355 claimed_block: cert2.data.block_number,
356 parent_view,
357 parent_epoch: justify_qc_epoch,
358 parent_block,
359 });
360 }
361 if cert2.view_number() >= view {
363 return Err(MalformedProposal::NextEpochJustifyQcNotEarlier {
364 view,
365 claimed_view: cert2.view_number(),
366 });
367 }
368 Ok(Some(cert2))
369}
370
371pub(crate) fn view_change_evidence_matches_parent<T: NodeType>(
385 proposal: &Proposal<T>,
386) -> Result<Option<&TimeoutEvidence<T>>, MalformedProposal> {
387 let view = proposal.view_number();
388 let parent_view = proposal.justify_qc.view_number();
389 if parent_view >= view {
390 return Err(MalformedProposal::ParentNotEarlier { view, parent_view });
391 }
392 let Some(tc) = proposal.view_change_evidence.as_ref() else {
393 if parent_view + 1 == view {
394 return Ok(None);
395 }
396 return Err(MalformedProposal::ViewChangeEvidenceMissing(view));
397 };
398 let evidence_view = tc.view_number();
400 if evidence_view + 1 != view {
401 return Err(MalformedProposal::ViewChangeEvidenceView {
402 view,
403 evidence_view,
404 });
405 }
406 Ok(Some(tc))
407}
408
409impl<T: NodeType> Validator<T> {
410 async fn signature(
412 &self,
413 proposal: &SignedProposal<T, Proposal<T>>,
414 ) -> Result<T::SignatureKey> {
415 let view = proposal.data.view_number();
416 let epoch = proposal.data.epoch;
417 let membership = self.membership(epoch).await?;
418 let leader = match membership.leader(view) {
419 Ok(leader) => leader,
420 Err(err) => return Err(ValidationError::NoLeader(view, epoch, err)),
421 };
422 let leaf: Leaf2<T> = proposal.data.clone().into();
423 if leader.validate(&proposal.signature, leaf.commit().as_ref()) {
424 Ok(leader)
425 } else {
426 Err(ValidationError::InvalidProposalSignature)
427 }
428 }
429
430 async fn vid_share_proposal(
431 &self,
432 vid_proposal: &SignedProposal<T, VidDisperseShare2<T>>,
433 ) -> Result<()> {
434 let view = vid_proposal.data.view_number();
435 let epoch = vid_proposal
436 .data
437 .epoch
438 .ok_or(ValidationError::MissingEpoch(view, "vid share"))?;
439 let membership = self.membership(epoch).await?;
440 let stake_table = membership.stake_table();
441 let leader = match membership.leader(view) {
442 Ok(leader) => leader,
443 Err(err) => return Err(ValidationError::NoLeader(view, epoch, err)),
444 };
445 let total_weight = vid_total_weight(stake_table, Some(epoch));
447 if !leader.validate(
448 &vid_proposal.signature,
449 vid_proposal.data.payload_commitment.as_ref(),
450 ) {
451 return Err(ValidationError::InvalidVidShareProposalSignature);
452 }
453 if vid_proposal.data.verify(total_weight) {
454 Ok(())
455 } else {
456 Err(ValidationError::VidShareNotVerified)
457 }
458 }
459
460 async fn certificates(&self, proposal: &Proposal<T>, parts: &Parts<'_, T>) -> Result<()> {
467 let epoch = parts.justify_qc_epoch;
468 if proposal.justify_qc.view_number() == ViewNumber::genesis() {
469 if self.genesis_qc.as_ref() != Some(&proposal.justify_qc.data) {
471 return Err(ValidationError::NotGenesisQc(proposal.view_number()));
472 }
473 } else {
474 self.verify_cert(
475 &proposal.justify_qc,
476 epoch,
477 ValidationError::InvalidJustifyQc,
478 )
479 .await?;
480 }
481 if let Some(cert2) = parts.next_epoch_justify_qc {
482 self.verify_cert(cert2, epoch, ValidationError::InvalidNextEpochJustifyQc)
483 .await?;
484 }
485 if let Some(tc) = parts.view_change_evidence {
486 let view = proposal.view_number();
487 let Some(tc_epoch) = tc.epoch() else {
488 return Err(ValidationError::MissingEpoch(view, "view_change_evidence"));
489 };
490 let admitted = if tc.binds_epoch() {
491 tc_epoch == proposal.epoch
492 } else {
493 tc_epoch <= proposal.epoch
494 };
495 if !admitted {
496 return Err(ValidationError::ViewChangeEvidenceEpoch {
497 view,
498 evidence_epoch: tc_epoch,
499 epoch: proposal.epoch,
500 });
501 }
502 if let TimeoutEvidence::V3(cert) = tc {
503 cert.check_lock()
504 .map_err(ValidationError::InvalidViewChangeEvidence)?;
505 }
506 let membership = self.membership(tc_epoch).await?;
507 if let Some(lock_epoch) = tc.timeout_lock().verified_in() {
508 self.membership(lock_epoch).await?;
509 }
510 let entries = StakeTableEntries::from_iter(membership.stake_table()).0;
511 verify_timeout_evidence(
512 tc,
513 &entries,
514 membership.success_threshold(),
515 self.epoch_height,
516 &self.upgrade_lock,
517 &self.membership_coordinator,
518 )
519 .map_err(ValidationError::InvalidViewChangeEvidence)?;
520 }
521 Ok(())
522 }
523
524 async fn verify_cert<D>(
529 &self,
530 cert: &SimpleCertificate<T, D, SuccessThreshold>,
531 epoch: EpochNumber,
532 invalid: fn(anytrace::Error) -> ValidationError,
533 ) -> Result<()>
534 where
535 D: Voteable<T> + QuorumMarker + 'static,
536 {
537 let membership = self.membership(epoch).await?;
538 let entries = StakeTableEntries::from_iter(membership.stake_table()).0;
539 verify_signatures(
540 cert,
541 &entries,
542 membership.success_threshold(),
543 &self.upgrade_lock,
544 )
545 .map_err(invalid)
546 }
547
548 async fn state_cert(&self, proposal: &Proposal<T>, parts: &Parts<'_, T>) -> Result<()> {
555 let Some(state_cert) = parts.state_cert else {
556 return Ok(());
557 };
558 if !check_qc_state_cert_correspondence(&proposal.justify_qc, state_cert, self.epoch_height)
559 {
560 return Err(ValidationError::StateCertCorrespondence);
561 }
562 validate_light_client_state_update_certificate(
563 state_cert,
564 &self.membership_coordinator,
565 &self.upgrade_lock,
566 )
567 .await
568 .map_err(ValidationError::InvalidStateCert)
569 }
570
571 async fn upgrade_certificate(&self, proposal: &Proposal<T>) -> Result<()> {
577 let Some(cert) = proposal.upgrade_certificate.as_ref() else {
578 return Ok(());
579 };
580 let expected = expected_upgrade_data(
581 &self.upgrade_lock.upgrade(),
582 cert.view_number(),
583 proposal.epoch,
584 );
585 if expected.as_ref() != Some(&cert.data) {
586 return Err(ValidationError::UnexpectedUpgradeCertificateData);
587 }
588 if proposal.view_number > cert.data.decide_by {
589 return Err(ValidationError::ExpiredUpgradeCertificate(
590 proposal.view_number,
591 cert.data.decide_by,
592 ));
593 }
594 let membership = self.membership(cert.data.epoch).await?;
595 let entries = StakeTableEntries::from_iter(membership.stake_table()).0;
596 let threshold = membership.upgrade_threshold();
597 verify_signatures(cert, &entries, threshold, &self.upgrade_lock)
598 .map_err(ValidationError::InvalidUpgradeCertificate)
599 }
600
601 async fn membership(&self, epoch: EpochNumber) -> Result<EpochMembership<T>> {
602 match self
603 .membership_coordinator
604 .membership_for_epoch(Some(epoch))
605 {
606 Ok(m) => Ok(m),
607 Err(_) => self
608 .membership_coordinator
609 .wait_for_catchup(epoch) .await
611 .map_err(|e| ValidationError::NoMembershipForEpoch(epoch, e)),
612 }
613 }
614}
615
616#[derive(Debug, thiserror::Error)]
617pub enum ValidationError {
618 #[error(transparent)]
619 Malformed(#[from] MalformedProposal),
620
621 #[error("invalid proposal signature")]
622 InvalidProposalSignature,
623
624 #[error("invalid proposal justify qc: {0}")]
625 InvalidJustifyQc(#[source] anytrace::Error),
626
627 #[error("justify_qc of proposal at view {0} is at the genesis view but is not the genesis QC")]
628 NotGenesisQc(ViewNumber),
629
630 #[error("invalid next_epoch_justify_qc: {0}")]
631 InvalidNextEpochJustifyQc(#[source] anytrace::Error),
632
633 #[error("vid share does not match proposal")]
634 VidCommitmentDoesNotMatchProposal,
635
636 #[error("failed to verify vid share")]
637 VidShareNotVerified,
638
639 #[error("vid commitment not v2")]
640 InvalidVidCommitmentVersion,
641
642 #[error("missing epoch number in view {0} ({1})")]
643 MissingEpoch(ViewNumber, &'static str),
644
645 #[error(
646 "view change evidence of proposal at view {view} is of epoch {evidence_epoch}, which the \
647 proposal's epoch {epoch} does not admit"
648 )]
649 ViewChangeEvidenceEpoch {
650 view: ViewNumber,
651 evidence_epoch: EpochNumber,
652 epoch: EpochNumber,
653 },
654
655 #[error("failed to get membership for epoch {0}: {1}")]
656 NoMembershipForEpoch(EpochNumber, #[source] anytrace::Error),
657
658 #[error("failed to get leader for view {0}, epoch {1}: {2}")]
659 NoLeader(ViewNumber, EpochNumber, #[source] anytrace::Error),
660
661 #[error("state_cert does not correspond to justify_qc")]
662 StateCertCorrespondence,
663
664 #[error("state_cert signature validation failed: {0}")]
665 InvalidStateCert(#[source] anytrace::Error),
666
667 #[error("invalid vid share proposal signature")]
668 InvalidVidShareProposalSignature,
669
670 #[error("view-change evidence (timeout certificate) is invalid: {0}")]
671 InvalidViewChangeEvidence(#[source] anytrace::Error),
672
673 #[error("upgrade certificate data differs from the expected upgrade data")]
674 UnexpectedUpgradeCertificateData,
675
676 #[error("proposal at view {0} carries an upgrade certificate expired at view {1}")]
677 ExpiredUpgradeCertificate(ViewNumber, ViewNumber),
678
679 #[error("invalid upgrade certificate: {0}")]
680 InvalidUpgradeCertificate(#[source] anytrace::Error),
681}
682
683fn verify_timeout_evidence<T: NodeType>(
692 tc: &TimeoutEvidence<T>,
693 stake_table: &[<T::SignatureKey as SignatureKey>::StakeTableEntry],
694 threshold: U256,
695 epoch_height: u64,
696 upgrade_lock: &UpgradeLock<T>,
697 memberships: &EpochMembershipCoordinator<T>,
698) -> anytrace::Result<()> {
699 let view = tc.view_number();
700 ensure!(
701 tc.binds_epoch() == upgrade_lock.timeout_epoch_bound(view),
702 "timeout certificate for view {view} has the wrong form for its version"
703 );
704 match tc {
705 TimeoutEvidence::V2(cert) => {
706 ensure!(
707 view == cert.data.view,
708 "timeout certificate view {view} != data view {}",
709 cert.data.view
710 );
711 verify_signatures(cert, stake_table, threshold, upgrade_lock)
712 },
713 TimeoutEvidence::V3(cert) => verify_timeout3(
714 cert,
715 stake_table,
716 threshold,
717 epoch_height,
718 upgrade_lock,
719 memberships,
720 ),
721 }
722}
723
724#[derive(Copy, Clone, Debug, thiserror::Error)]
726pub enum MalformedProposal {
727 #[error(
728 "proposal at view {view} claims epoch {claimed}, but block number {block_number} falls in \
729 epoch {expected}"
730 )]
731 Epoch {
732 view: ViewNumber,
733 block_number: u64,
734 expected: EpochNumber,
735 claimed: EpochNumber,
736 },
737
738 #[error("justify_qc of proposal at view {0} names no epoch")]
739 JustifyQcWithoutEpoch(ViewNumber),
740
741 #[error("justify_qc of proposal at view {0} names no block number")]
742 JustifyQcWithoutBlockNumber(ViewNumber),
743
744 #[error(
745 "justify_qc of proposal at view {view} is at the genesis view but certifies block \
746 {claimed}, not the genesis block"
747 )]
748 GenesisJustifyQcBlockNumber { view: ViewNumber, claimed: u64 },
749
750 #[error(
751 "justify_qc of proposal at view {view} claims epoch {claimed}, but its parent block \
752 {parent_block} falls in epoch {expected}"
753 )]
754 JustifyQcEpoch {
755 view: ViewNumber,
756 parent_block: u64,
757 expected: EpochNumber,
758 claimed: EpochNumber,
759 },
760
761 #[error(
762 "justify_qc of proposal at view {view} certifies block {claimed}, not its parent block \
763 {expected}"
764 )]
765 JustifyQcBlockNumber {
766 view: ViewNumber,
767 expected: u64,
768 claimed: u64,
769 },
770
771 #[error(
772 "next_epoch_justify_qc on proposal at view {0}, which does not follow a boundary block"
773 )]
774 NextEpochJustifyQcUnexpected(ViewNumber),
775
776 #[error("first proposal of an epoch at view {0} is missing next_epoch_justify_qc")]
777 NextEpochJustifyQcMissing(ViewNumber),
778
779 #[error(
780 "next_epoch_justify_qc of proposal at view {0} certifies another leaf than its justify_qc"
781 )]
782 NextEpochJustifyQcLeafCommit(ViewNumber),
783
784 #[error(
785 "next_epoch_justify_qc of proposal at view {view} is at view {claimed_view}, which is not \
786 earlier"
787 )]
788 NextEpochJustifyQcNotEarlier {
789 view: ViewNumber,
790 claimed_view: ViewNumber,
791 },
792
793 #[error(
794 "next_epoch_justify_qc of proposal at view {view} certifies view {claimed_view} of epoch \
795 {claimed_epoch} at block {claimed_block}, not view {parent_view} or later of epoch \
796 {parent_epoch} at block {parent_block}"
797 )]
798 NextEpochJustifyQcParent {
799 view: ViewNumber,
800 claimed_view: ViewNumber,
801 claimed_epoch: EpochNumber,
802 claimed_block: u64,
803 parent_view: ViewNumber,
804 parent_epoch: EpochNumber,
805 parent_block: u64,
806 },
807
808 #[error(
809 "proposal at view {view} has a justify_qc for view {parent_view}, which is not earlier"
810 )]
811 ParentNotEarlier {
812 view: ViewNumber,
813 parent_view: ViewNumber,
814 },
815
816 #[error("proposal at view {0} skips views but carries no view-change evidence")]
817 ViewChangeEvidenceMissing(ViewNumber),
818
819 #[error(
820 "view-change evidence for proposal at view {view} certifies view {evidence_view}, not the \
821 immediately preceding view"
822 )]
823 ViewChangeEvidenceView {
824 view: ViewNumber,
825 evidence_view: ViewNumber,
826 },
827
828 #[error("state_cert on proposal at view {0}, whose justify_qc is not at an epoch-root block")]
829 StateCertUnexpected(ViewNumber),
830
831 #[error("epoch-root-parent proposal at view {0} is missing state_cert")]
832 StateCertMissing(ViewNumber),
833}