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::{block_contents::BlockHeader, node_implementation::NodeType},
14 utils::{is_epoch_root, is_last_block},
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
25pub struct ValidatedProposal<T: NodeType> {
27 pub sender: T::SignatureKey,
28 pub message: ProposalMessage<T, Validated>,
29 pub fetched: bool,
31}
32
33pub struct ProposalValidator<T: NodeType> {
35 tasks: JoinSet<Result<ValidatedProposal<T>>>,
37
38 validator: Arc<Validator<T>>,
40}
41
42pub struct VidShareValidator<T: NodeType> {
48 tasks: JoinSet<Result<VidDisperseShare2<T>>>,
50
51 validator: Arc<Validator<T>>,
53}
54
55struct Validator<T: NodeType> {
56 membership_coordinator: EpochMembershipCoordinator<T>,
57 epoch_height: u64,
58 upgrade_lock: UpgradeLock<T>,
59}
60
61impl<T: NodeType> ProposalValidator<T> {
62 pub fn new(
63 c: EpochMembershipCoordinator<T>,
64 epoch_height: u64,
65 upgrade_lock: UpgradeLock<T>,
66 ) -> Self {
67 Self {
68 tasks: JoinSet::new(),
69 validator: Arc::new(Validator {
70 membership_coordinator: c,
71 epoch_height,
72 upgrade_lock,
73 }),
74 }
75 }
76
77 pub fn validate(&mut self, p: ProposalMessage<T, Unchecked>) {
78 self.spawn_validation(p, false)
79 }
80
81 pub fn validate_fetched(&mut self, p: ProposalMessage<T, Unchecked>) {
85 self.spawn_validation(p, true)
86 }
87
88 fn spawn_validation(&mut self, p: ProposalMessage<T, Unchecked>, fetched: bool) {
89 let v = self.validator.clone();
90 self.tasks.spawn(async move {
91 let sender = v.signature(&p.proposal).await?;
92 v.justify_qc(&p.proposal.data).await?;
93 v.next_epoch_justify_qc(&p.proposal.data).await?;
94 v.view_change_evidence(&p.proposal.data).await?;
95 v.state_cert(&p.proposal.data).await?;
96 let validated_proposal = ValidatedProposal {
97 sender,
98 message: ProposalMessage::validated(p.proposal),
99 fetched,
100 };
101 Ok(validated_proposal)
102 });
103 }
104
105 pub async fn next(&mut self) -> Option<Result<ValidatedProposal<T>>> {
106 loop {
107 match self.tasks.join_next().await {
108 Some(Ok(prop)) => return Some(prop),
109 Some(Err(err)) => {
110 error!(%err, "proposal validation task panic");
111 },
112 None => return None,
113 }
114 }
115 }
116}
117
118impl<T: NodeType> VidShareValidator<T> {
119 pub fn new(
120 c: EpochMembershipCoordinator<T>,
121 epoch_height: u64,
122 upgrade_lock: UpgradeLock<T>,
123 ) -> Self {
124 Self {
125 tasks: JoinSet::new(),
126 validator: Arc::new(Validator {
127 membership_coordinator: c,
128 epoch_height,
129 upgrade_lock,
130 }),
131 }
132 }
133
134 pub fn validate(&mut self, share: VidShareMessage<T>) {
135 let v = self.validator.clone();
136 self.tasks.spawn(async move {
137 v.vid_share_proposal(&share).await?;
138 Ok(share.data)
139 });
140 }
141
142 pub async fn next(&mut self) -> Option<Result<VidDisperseShare2<T>>> {
143 loop {
144 match self.tasks.join_next().await {
145 Some(Ok(share)) => return Some(share),
146 Some(Err(err)) => {
147 error!(%err, "vid share validation task panic");
148 },
149 None => return None,
150 }
151 }
152 }
153}
154
155impl<T: NodeType> Validator<T> {
156 async fn signature(
158 &self,
159 proposal: &SignedProposal<T, Proposal<T>>,
160 ) -> Result<T::SignatureKey> {
161 let view = proposal.data.view_number();
162 let epoch = proposal.data.epoch;
163 let membership = self.membership(epoch).await?;
164 let leader = match membership.leader(view) {
165 Ok(leader) => leader,
166 Err(err) => return Err(ValidationError::NoLeader(view, epoch, err)),
167 };
168 let leaf: Leaf2<T> = proposal.data.clone().into();
169 if leader.validate(&proposal.signature, leaf.commit().as_ref()) {
170 Ok(leader)
171 } else {
172 Err(ValidationError::InvalidProposalSignature)
173 }
174 }
175
176 async fn vid_share_proposal(
177 &self,
178 vid_proposal: &SignedProposal<T, VidDisperseShare2<T>>,
179 ) -> Result<()> {
180 let view = vid_proposal.data.view_number();
181 let epoch = vid_proposal
182 .data
183 .epoch
184 .ok_or(ValidationError::MissingEpoch(view, "vid share"))?;
185 let membership = self.membership(epoch).await?;
186 let stake_table = membership.stake_table();
187 let leader = match membership.leader(view) {
188 Ok(leader) => leader,
189 Err(err) => return Err(ValidationError::NoLeader(view, epoch, err)),
190 };
191 let total_weight = vid_total_weight(stake_table, Some(epoch));
193 if !leader.validate(
194 &vid_proposal.signature,
195 vid_proposal.data.payload_commitment.as_ref(),
196 ) {
197 return Err(ValidationError::InvalidVidShareProposalSignature);
198 }
199 if vid_proposal.data.verify(total_weight) {
200 Ok(())
201 } else {
202 Err(ValidationError::VidShareNotVerified)
203 }
204 }
205
206 async fn justify_qc(&self, proposal: &Proposal<T>) -> Result<()> {
208 let Some(epoch) = proposal.justify_qc.epoch() else {
209 return Err(ValidationError::MissingEpoch(
210 proposal.view_number,
211 "justify_qc",
212 ));
213 };
214 let membership = self.membership(epoch).await?;
215 let entries = StakeTableEntries::from_iter(membership.stake_table()).0;
216 let threshold = membership.success_threshold();
217 match proposal
218 .justify_qc
219 .is_valid_cert(&entries, threshold, &self.upgrade_lock)
220 {
221 Ok(()) => Ok(()),
222 Err(e) => Err(ValidationError::InvalidJustifyQc(e)),
223 }
224 }
225
226 async fn next_epoch_justify_qc(&self, proposal: &Proposal<T>) -> Result<()> {
232 let block_number = proposal.block_header.block_number();
233 if !is_last_block(block_number.saturating_sub(1), self.epoch_height) {
234 return Ok(());
235 }
236 let Some(cert2) = proposal.next_epoch_justify_qc.as_ref() else {
237 return Err(ValidationError::MissingNextEpochJustifyQc);
238 };
239 if cert2.data.leaf_commit != proposal.justify_qc.data.leaf_commit {
240 return Err(ValidationError::NextEpochJustifyQcMismatch);
241 }
242 let Some(epoch) = proposal.justify_qc.epoch() else {
243 return Err(ValidationError::MissingEpoch(
244 proposal.view_number,
245 "justify_qc",
246 ));
247 };
248 let membership = self.membership(epoch).await?;
249 let entries = StakeTableEntries::from_iter(membership.stake_table()).0;
250 let threshold = membership.success_threshold();
251 cert2
252 .is_valid_cert(&entries, threshold, &self.upgrade_lock)
253 .map_err(ValidationError::InvalidNextEpochJustifyQc)
254 }
255
256 async fn view_change_evidence(&self, proposal: &Proposal<T>) -> Result<()> {
258 let view = proposal.view_number();
259
260 if proposal.justify_qc.view_number() + 1 == view {
262 return Ok(());
263 }
264
265 let Some(tc) = proposal.view_change_evidence.as_ref() else {
266 return Err(ValidationError::MissingViewChangeEvidence(view));
267 };
268
269 if tc.data().view + 1 != view {
271 return Err(ValidationError::ViewChangeEvidenceWrongView {
272 proposal_view: view,
273 evidence_view: tc.data().view,
274 });
275 }
276
277 let Some(tc_epoch) = tc.epoch() else {
278 return Err(ValidationError::MissingEpoch(view, "view_change_evidence"));
279 };
280 let membership = self.membership(tc_epoch).await?;
281 let entries = StakeTableEntries::from_iter(membership.stake_table()).0;
282 let threshold = membership.success_threshold();
283 tc.is_valid_cert(&entries, threshold, &self.upgrade_lock)
284 .map_err(ValidationError::InvalidViewChangeEvidence)
285 }
286
287 async fn state_cert(&self, proposal: &Proposal<T>) -> Result<()> {
292 let Some(qc_block_number) = proposal.justify_qc.data.block_number else {
293 return Ok(());
294 };
295 if !is_epoch_root(qc_block_number, self.epoch_height) {
296 return Ok(());
298 }
299 let Some(state_cert) = proposal.state_cert.as_ref() else {
300 return Err(ValidationError::MissingStateCert);
301 };
302 if !check_qc_state_cert_correspondence(&proposal.justify_qc, state_cert, self.epoch_height)
303 {
304 return Err(ValidationError::StateCertCorrespondence);
305 }
306 validate_light_client_state_update_certificate(
307 state_cert,
308 &self.membership_coordinator,
309 &self.upgrade_lock,
310 )
311 .await
312 .map_err(ValidationError::InvalidStateCert)
313 }
314
315 async fn membership(&self, epoch: EpochNumber) -> Result<EpochMembership<T>> {
316 match self
317 .membership_coordinator
318 .membership_for_epoch(Some(epoch))
319 {
320 Ok(m) => Ok(m),
321 Err(_) => self
322 .membership_coordinator
323 .wait_for_catchup(epoch) .await
325 .map_err(|e| ValidationError::NoMembershipForEpoch(epoch, e)),
326 }
327 }
328}
329
330#[derive(Debug, thiserror::Error)]
331pub enum ValidationError {
332 #[error("invalid proposal signature")]
333 InvalidProposalSignature,
334
335 #[error("invalid proposal justify qc: {0}")]
336 InvalidJustifyQc(#[source] anytrace::Error),
337
338 #[error("first proposal of an epoch is missing next_epoch_justify_qc")]
339 MissingNextEpochJustifyQc,
340
341 #[error("next_epoch_justify_qc does not match the justify qc leaf commitment")]
342 NextEpochJustifyQcMismatch,
343
344 #[error("invalid next_epoch_justify_qc: {0}")]
345 InvalidNextEpochJustifyQc(#[source] anytrace::Error),
346
347 #[error("vid share does not match proposal")]
348 VidCommitmentDoesNotMatchProposal,
349
350 #[error("failed to verify vid share")]
351 VidShareNotVerified,
352
353 #[error("vid commitment not v2")]
354 InvalidVidCommitmentVersion,
355
356 #[error("missing epoch number in view {0} ({1})")]
357 MissingEpoch(ViewNumber, &'static str),
358
359 #[error("failed to get membership for epoch {0}: {1}")]
360 NoMembershipForEpoch(EpochNumber, #[source] anytrace::Error),
361
362 #[error("failed to get leader for view {0}, epoch {1}: {2}")]
363 NoLeader(ViewNumber, EpochNumber, #[source] anytrace::Error),
364
365 #[error("proposal justify_qc is epoch-root but state_cert is missing")]
366 MissingStateCert,
367
368 #[error("state_cert does not correspond to justify_qc")]
369 StateCertCorrespondence,
370
371 #[error("state_cert signature validation failed: {0}")]
372 InvalidStateCert(#[source] anytrace::Error),
373
374 #[error("invalid vid share proposal signature")]
375 InvalidVidShareProposalSignature,
376
377 #[error("proposal at view {0} skips views but carries no view-change evidence")]
378 MissingViewChangeEvidence(ViewNumber),
379
380 #[error(
381 "view-change evidence for proposal view {proposal_view} certifies view {evidence_view}, \
382 not the immediately preceding view"
383 )]
384 ViewChangeEvidenceWrongView {
385 proposal_view: ViewNumber,
386 evidence_view: ViewNumber,
387 },
388
389 #[error("view-change evidence (timeout certificate) is invalid: {0}")]
390 InvalidViewChangeEvidence(#[source] anytrace::Error),
391}