Skip to main content

hotshot_new_protocol/
proposal.rs

1use std::sync::Arc;
2
3use committable::Committable;
4use hotshot::types::SignatureKey;
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::check_qc_state_cert_correspondence,
11    simple_vote::HasEpoch,
12    stake_table::StakeTableEntries,
13    traits::node_implementation::NodeType,
14    utils::is_epoch_root,
15    vote::{Certificate, HasViewNumber},
16};
17use hotshot_utils::anytrace;
18use tokio::task::JoinSet;
19use tracing::error;
20
21use crate::message::{Proposal, ProposalMessage, Unchecked, Validated, VidShareMessage};
22
23type Result<T> = std::result::Result<T, ValidationError>;
24
25/// A validated proposal.
26pub struct ValidatedProposal<T: NodeType> {
27    pub sender: T::SignatureKey,
28    pub message: ProposalMessage<T, Validated>,
29}
30
31/// A proposal validator checks proposal signature and integrity.
32pub struct ProposalValidator<T: NodeType> {
33    /// Validation tasks.
34    tasks: JoinSet<Result<ValidatedProposal<T>>>,
35
36    /// The actual validation logic.
37    validator: Arc<Validator<T>>,
38}
39
40/// A validator dedicated to VID share messages.
41///
42/// Lives as a separate field on `Coordinator` so that its `next()` and
43/// `ProposalValidator::next()` can be polled in the same `tokio::select!`
44/// without conflicting `&mut self` borrows on a single field.
45pub struct VidShareValidator<T: NodeType> {
46    /// VID share validation tasks.
47    tasks: JoinSet<Result<VidDisperseShare2<T>>>,
48
49    /// Shared validation logic (same shape as `ProposalValidator`'s).
50    validator: Arc<Validator<T>>,
51}
52
53struct Validator<T: NodeType> {
54    membership_coordinator: EpochMembershipCoordinator<T>,
55    epoch_height: u64,
56    upgrade_lock: UpgradeLock<T>,
57}
58
59impl<T: NodeType> ProposalValidator<T> {
60    pub fn new(
61        c: EpochMembershipCoordinator<T>,
62        epoch_height: u64,
63        upgrade_lock: UpgradeLock<T>,
64    ) -> Self {
65        Self {
66            tasks: JoinSet::new(),
67            validator: Arc::new(Validator {
68                membership_coordinator: c,
69                epoch_height,
70                upgrade_lock,
71            }),
72        }
73    }
74
75    pub fn validate(&mut self, p: ProposalMessage<T, Unchecked>) {
76        let v = self.validator.clone();
77        self.tasks.spawn(async move {
78            let sender = v.signature(&p.proposal).await?;
79            v.justify_qc(&p.proposal.data).await?;
80            v.state_cert(&p.proposal.data).await?;
81            let validated_proposal = ValidatedProposal {
82                sender,
83                message: ProposalMessage::validated(p.proposal),
84            };
85            Ok(validated_proposal)
86        });
87    }
88
89    pub async fn next(&mut self) -> Option<Result<ValidatedProposal<T>>> {
90        loop {
91            match self.tasks.join_next().await {
92                Some(Ok(prop)) => return Some(prop),
93                Some(Err(err)) => {
94                    error!(%err, "proposal validation task panic");
95                },
96                None => return None,
97            }
98        }
99    }
100}
101
102impl<T: NodeType> VidShareValidator<T> {
103    pub fn new(
104        c: EpochMembershipCoordinator<T>,
105        epoch_height: u64,
106        upgrade_lock: UpgradeLock<T>,
107    ) -> Self {
108        Self {
109            tasks: JoinSet::new(),
110            validator: Arc::new(Validator {
111                membership_coordinator: c,
112                epoch_height,
113                upgrade_lock,
114            }),
115        }
116    }
117
118    pub fn validate(&mut self, share: VidShareMessage<T>) {
119        let v = self.validator.clone();
120        self.tasks.spawn(async move {
121            v.vid_share_proposal(&share).await?;
122            Ok(share.data)
123        });
124    }
125
126    pub async fn next(&mut self) -> Option<Result<VidDisperseShare2<T>>> {
127        loop {
128            match self.tasks.join_next().await {
129                Some(Ok(share)) => return Some(share),
130                Some(Err(err)) => {
131                    error!(%err, "vid share validation task panic");
132                },
133                None => return None,
134            }
135        }
136    }
137}
138
139impl<T: NodeType> Validator<T> {
140    /// Verify the proposal signature and return the leader
141    async fn signature(
142        &self,
143        proposal: &SignedProposal<T, Proposal<T>>,
144    ) -> Result<T::SignatureKey> {
145        let view = proposal.data.view_number();
146        let epoch = proposal.data.epoch;
147        let membership = self.membership(epoch).await?;
148        let leader = match membership.leader(view) {
149            Ok(leader) => leader,
150            Err(err) => return Err(ValidationError::NoLeader(view, epoch, err)),
151        };
152        let leaf: Leaf2<T> = proposal.data.clone().into();
153        if leader.validate(&proposal.signature, leaf.commit().as_ref()) {
154            Ok(leader)
155        } else {
156            Err(ValidationError::InvalidProposalSignature)
157        }
158    }
159
160    async fn vid_share_proposal(
161        &self,
162        vid_proposal: &SignedProposal<T, VidDisperseShare2<T>>,
163    ) -> Result<()> {
164        let view = vid_proposal.data.view_number();
165        let epoch = vid_proposal
166            .data
167            .epoch
168            .ok_or(ValidationError::MissingEpoch(view, "vid share"))?;
169        let membership = self.membership(epoch).await?;
170        let stake_table = membership.stake_table();
171        let leader = match membership.leader(view) {
172            Ok(leader) => leader,
173            Err(err) => return Err(ValidationError::NoLeader(view, epoch, err)),
174        };
175        // TODO(Chengyu): this also check the consistency of vid common and vid commitment.
176        let total_weight = vid_total_weight(stake_table, Some(epoch));
177        if !leader.validate(
178            &vid_proposal.signature,
179            vid_proposal.data.payload_commitment.as_ref(),
180        ) {
181            return Err(ValidationError::InvalidVidShareProposalSignature);
182        }
183        if vid_proposal.data.verify(total_weight) {
184            Ok(())
185        } else {
186            Err(ValidationError::VidShareNotVerified)
187        }
188    }
189
190    /// Verify the QC of the proposal
191    async fn justify_qc(&self, proposal: &Proposal<T>) -> Result<()> {
192        let Some(epoch) = proposal.justify_qc.epoch() else {
193            return Err(ValidationError::MissingEpoch(
194                proposal.view_number,
195                "justify_qc",
196            ));
197        };
198        let membership = self.membership(epoch).await?;
199        let entries = StakeTableEntries::from_iter(membership.stake_table()).0;
200        let threshold = membership.success_threshold();
201        match proposal
202            .justify_qc
203            .is_valid_cert(&entries, threshold, &self.upgrade_lock)
204        {
205            Ok(()) => Ok(()),
206            Err(e) => Err(ValidationError::InvalidJustifyQc(e)),
207        }
208    }
209
210    /// Validate the state_cert on an epoch-root proposal.
211    ///
212    /// If the justify_qc points at an epoch-root block, the proposal MUST
213    /// carry a matching `LightClientStateUpdateCertificateV2`.
214    async fn state_cert(&self, proposal: &Proposal<T>) -> Result<()> {
215        let Some(qc_block_number) = proposal.justify_qc.data.block_number else {
216            return Ok(());
217        };
218        if !is_epoch_root(qc_block_number, self.epoch_height) {
219            // Non-epoch-root parent → no state_cert required.
220            return Ok(());
221        }
222        let Some(state_cert) = proposal.state_cert.as_ref() else {
223            return Err(ValidationError::MissingStateCert);
224        };
225        if !check_qc_state_cert_correspondence(&proposal.justify_qc, state_cert, self.epoch_height)
226        {
227            return Err(ValidationError::StateCertCorrespondence);
228        }
229        validate_light_client_state_update_certificate(
230            state_cert,
231            &self.membership_coordinator,
232            &self.upgrade_lock,
233        )
234        .await
235        .map_err(ValidationError::InvalidStateCert)
236    }
237
238    async fn membership(&self, epoch: EpochNumber) -> Result<EpochMembership<T>> {
239        match self
240            .membership_coordinator
241            .membership_for_epoch(Some(epoch))
242        {
243            Ok(m) => Ok(m),
244            Err(_) => self
245                .membership_coordinator
246                .wait_for_catchup(epoch) // TODO: timeout?
247                .await
248                .map_err(|e| ValidationError::NoMembershipForEpoch(epoch, e)),
249        }
250    }
251}
252
253#[derive(Debug, thiserror::Error)]
254pub enum ValidationError {
255    #[error("invalid proposal signature")]
256    InvalidProposalSignature,
257
258    #[error("invalid proposal justify qc: {0}")]
259    InvalidJustifyQc(#[source] anytrace::Error),
260
261    #[error("vid share does not match proposal")]
262    VidCommitmentDoesNotMatchProposal,
263
264    #[error("failed to verify vid share")]
265    VidShareNotVerified,
266
267    #[error("vid commitment not v2")]
268    InvalidVidCommitmentVersion,
269
270    #[error("missing epoch number in view {0} ({1})")]
271    MissingEpoch(ViewNumber, &'static str),
272
273    #[error("failed to get membership for epoch {0}: {1}")]
274    NoMembershipForEpoch(EpochNumber, #[source] anytrace::Error),
275
276    #[error("failed to get leader for view {0}, epoch {1}: {2}")]
277    NoLeader(ViewNumber, EpochNumber, #[source] anytrace::Error),
278
279    #[error("proposal justify_qc is epoch-root but state_cert is missing")]
280    MissingStateCert,
281
282    #[error("state_cert does not correspond to justify_qc")]
283    StateCertCorrespondence,
284
285    #[error("state_cert signature validation failed: {0}")]
286    InvalidStateCert(#[source] anytrace::Error),
287
288    #[error("invalid vid share proposal signature")]
289    InvalidVidShareProposalSignature,
290}