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
25pub struct ValidatedProposal<T: NodeType> {
27 pub sender: T::SignatureKey,
28 pub message: ProposalMessage<T, Validated>,
29}
30
31pub struct ProposalValidator<T: NodeType> {
33 tasks: JoinSet<Result<ValidatedProposal<T>>>,
35
36 validator: Arc<Validator<T>>,
38}
39
40pub struct VidShareValidator<T: NodeType> {
46 tasks: JoinSet<Result<VidDisperseShare2<T>>>,
48
49 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 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 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 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 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 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) .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}