Skip to main content

hotshot_new_protocol/
proposal.rs

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
37/// A validated proposal.
38pub struct ValidatedProposal<T: NodeType> {
39    pub sender: T::SignatureKey,
40    pub message: ProposalMessage<T, Validated>,
41    /// True if this proposal was fetched from a peer
42    pub fetched: bool,
43}
44
45/// A proposal validator checks proposal signature and integrity.
46pub struct ProposalValidator<T: NodeType> {
47    /// Validation tasks.
48    tasks: JoinSet<Result<ValidatedProposal<T>>>,
49
50    /// The actual validation logic.
51    validator: Arc<Validator<T>>,
52}
53
54/// A validator dedicated to VID share messages.
55///
56/// Lives as a separate field on `Coordinator` so that its `next()` and
57/// `ProposalValidator::next()` can be polled in the same `tokio::select!`
58/// without conflicting `&mut self` borrows on a single field.
59pub struct VidShareValidator<T: NodeType> {
60    /// VID share validation tasks.
61    tasks: JoinSet<Result<VidDisperseShare2<T>>>,
62
63    /// Shared validation logic (same shape as `ProposalValidator`'s).
64    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    /// The data of the genesis QC, the only `justify_qc` a proposal may carry
73    /// at the genesis view.
74    ///
75    /// `None` when the node did not start from genesis, which then refuses
76    /// every such proposal.
77    genesis_qc: Option<QuorumData2<T>>,
78}
79
80impl<T: NodeType> ProposalValidator<T> {
81    /// Create a validator accepting `genesis_qc` as a proposal's parent at the
82    /// genesis view, and no other certificate there.
83    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    /// Validate a proposal fetched from a peer.
105    /// The coordinator routes it into the decide
106    /// backfill path instead of the vote path.
107    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
179/// Everything a proposal claims about its own block and its parent, checked
180/// without signatures.
181///
182/// Returns the certificates whose signatures a validator verifies next, and the
183/// epoch to resolve their committee through.
184pub(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
202/// The certificates of a [well-formed](well_formed) proposal.
203pub(crate) struct Parts<'a, T: NodeType> {
204    /// The justify QC's epoch.
205    pub(crate) justify_qc_epoch: EpochNumber,
206
207    /// The state_cert an epoch-root-parent proposal carries.
208    pub(crate) state_cert: Option<&'a LightClientStateUpdateCertificateV2<T>>,
209
210    /// The boundary block's Cert2.
211    pub(crate) next_epoch_justify_qc: Option<&'a Certificate2<T>>,
212
213    /// The timeout certificate.
214    pub(crate) view_change_evidence: Option<&'a TimeoutEvidence<T>>,
215}
216
217/// The proposal's epoch must be the one its block number falls in.
218pub(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
235/// The justify QC must certify the block before this proposal, in the epoch that
236/// block's height falls in.
237///
238/// A certificate's epoch selects the committee whose stake table and threshold
239/// its signatures are weighed against. Unlike the proposal's own epoch, it is
240/// covered by those signatures, so a mismatch is not something a proposer can
241/// produce by relabelling a genuine certificate.
242///
243/// A justify QC at the genesis view must certify the genesis block. Such a
244/// certificate is not signed, so its epoch and block number are otherwise
245/// whatever the proposer wrote, and together they would let it claim any
246/// epoch for the proposal.
247///
248/// Returns that epoch, which the QC checks resolve their membership through.
249pub(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
286/// A proposal must carry `state_cert` exactly when its justify QC certifies an
287/// epoch-root block.
288///
289/// Returns the certificate to verify next, or `None` when the proposal's
290/// parent is not an epoch root and it carries none.
291///
292/// Unlike the rest of what a proposal claims about its parent, this field is
293/// not covered by the leaf commitment the proposer signs
294/// (`Leaf2::from_quorum_proposal` drops it). Its correspondence to the
295/// justify QC is checked separately (see [`Validator::state_cert`]); this
296/// function is what enforces its presence, which nothing else does.
297pub(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
314/// The first proposal of an epoch must carry the boundary block's Cert2, which
315/// certifies the same block as its justify QC.
316///
317/// Returns the certificate to verify the signatures on, or `None` when the
318/// proposal does not follow a boundary block and carries none.
319///
320/// `justify_qc_epoch` is the justify QC's own, as returned by
321/// [`justify_qc_matches_parent`]. The certificate names the same epoch and
322/// block as that QC because both certify the same leaf, and the leaf
323/// commitment covers neither, so agreement otherwise rests on an honest signer
324/// being in the quorum that formed it. Its view is the QC's or later: the
325/// justify QC is the block's own `Certificate1`, at the block's view, and a
326/// re-vote commits the block at a view of its own.
327pub(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    // The block comes after the commit of its parent (rule 1).
362    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
371/// A proposal extends an earlier view, and skips views only with a timeout
372/// certificate for the view immediately before it.
373///
374/// Returns the certificate to verify the signatures on, or `None` when the
375/// proposal follows its parent directly and carries none.
376///
377/// A proposal may carry a certificate it does not need: a leader holding one for
378/// the view before builds on its lock, which can be that view's own certificate.
379/// It is checked all the same, since it becomes part of the block.
380///
381/// The parent view being earlier is what the walks over stored proposals
382/// descend on, and, like the rest of what a proposal says about its parent, it
383/// is covered by no signature of the proposer's own.
384pub(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    // The timeout certificate must certify the immediately preceding view.
399    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    /// Verify the proposal signature and return the leader
411    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        // TODO(Chengyu): this also check the consistency of vid common and vid commitment.
446        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    /// Verify the signatures on the certificates the proposal carries.
461    ///
462    /// `parts` is what [`well_formed`] found the proposal must carry, and the
463    /// epoch whose committee formed its justify QC and the boundary Cert2
464    /// beside it. The view-change evidence names its own epoch, covered by the
465    /// signatures over it.
466    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            // The genesis QC is unsigned, so it is recognised by its data.
470            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    /// Verify a certificate's signatures against the stake table and threshold
525    /// of `epoch`'s committee, labelling an invalid one with `invalid`.
526    ///
527    /// Unlike `is_valid_cert`, this checks them at the genesis view too.
528    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    /// Verify the signature on the state_cert a [well-formed](well_formed)
549    /// proposal's parts say it carries.
550    ///
551    /// Presence is already settled by [`state_cert_matches_parent`]; this
552    /// only checks a present one's correspondence to the justify QC and its
553    /// threshold signature.
554    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    /// Validate an attached upgrade certificate: exactly the expected data
572    /// for its view and the carrying proposal's epoch (the `Leaf2` carries
573    /// the certificate without its epoch, so it must be the carrier's),
574    /// unexpired for the carrying proposal, and signed at the upgrade
575    /// threshold under the epoch it binds.
576    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) // TODO: timeout?
610                .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
683/// Check that a timeout certificate has the form its view requires, names that
684/// view in its data, and carries a quorum's signatures.
685///
686/// `TimeoutEvidence::is_valid_cert` checks the same, except that it skips the
687/// signatures at the genesis view. Every timeout certificate is formed from
688/// signed votes, at the genesis view as at any other, so none is exempt here.
689/// A certificate that carries a lock has its lock certificate checked against
690/// the lock's own epoch's committee.
691fn 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/// Reason a proposal is not [well-formed](well_formed).
725#[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}