hotshot_task_impls/quorum_proposal_recv/
handlers.rs1#![allow(dead_code)]
8
9use std::sync::Arc;
10
11use async_broadcast::{Receiver, Sender, broadcast};
12use async_lock::{RwLock, RwLockUpgradableReadGuard};
13use committable::Committable;
14use hotshot_contract_adapter::light_client::validate_light_client_state_update_certificate;
15use hotshot_types::{
16 consensus::OuterConsensus,
17 data::{Leaf2, QuorumProposal, QuorumProposalWrapper, ViewNumber},
18 epoch_membership::EpochMembershipCoordinator,
19 message::Proposal,
20 simple_certificate::{
21 QuorumCertificate, QuorumCertificate2, check_qc_state_cert_correspondence,
22 },
23 simple_vote::HasEpoch,
24 traits::{
25 ValidatedState,
26 block_contents::{BlockHeader, BlockPayload},
27 election::Membership,
28 node_implementation::{NodeImplementation, NodeType},
29 signature_key::SignatureKey,
30 storage::Storage,
31 },
32 utils::{
33 View, ViewInner, epoch_from_block_number, is_epoch_root, is_epoch_transition,
34 is_transition_block, option_epoch_from_block_number,
35 },
36 vote::{Certificate, HasViewNumber},
37};
38use hotshot_utils::anytrace::*;
39use tokio::spawn;
40use tracing::instrument;
41use versions::EPOCH_VERSION;
42
43use super::{QuorumProposalRecvTaskState, ValidationInfo};
44use crate::{
45 events::HotShotEvent,
46 helpers::{
47 broadcast_event, broadcast_view_change, fetch_proposal, update_high_qc,
48 validate_epoch_transition_qc, validate_proposal_safety_and_liveness,
49 validate_proposal_view_and_certs, validate_qc_and_next_epoch_qc, verify_drb_result,
50 },
51 quorum_proposal_recv::UpgradeLock,
52};
53
54#[allow(clippy::too_many_arguments)]
56fn spawn_fetch_proposal<TYPES: NodeType>(
57 qc: &QuorumCertificate2<TYPES>,
58 event_sender: Sender<Arc<HotShotEvent<TYPES>>>,
59 event_receiver: Receiver<Arc<HotShotEvent<TYPES>>>,
60 membership: EpochMembershipCoordinator<TYPES>,
61 consensus: OuterConsensus<TYPES>,
62 sender_public_key: TYPES::SignatureKey,
63 sender_private_key: <TYPES::SignatureKey as SignatureKey>::PrivateKey,
64 upgrade_lock: UpgradeLock<TYPES>,
65 epoch_height: u64,
66) {
67 let qc = qc.clone();
68 spawn(async move {
69 let lock = upgrade_lock;
70
71 let _ = fetch_proposal(
72 &qc,
73 event_sender,
74 event_receiver,
75 membership,
76 consensus,
77 sender_public_key,
78 sender_private_key,
79 &lock,
80 epoch_height,
81 )
82 .await;
83 });
84}
85
86#[instrument(skip_all)]
88pub async fn validate_proposal_liveness<TYPES: NodeType, I: NodeImplementation<TYPES>>(
89 proposal: &Proposal<TYPES, QuorumProposalWrapper<TYPES>>,
90 validation_info: &ValidationInfo<TYPES, I>,
91) -> Result<()> {
92 let mut valid_epoch_transition = false;
93 if validation_info
94 .upgrade_lock
95 .version(proposal.data.view_number())
96 .is_ok_and(|v| v >= EPOCH_VERSION)
97 {
98 let Some(block_number) = proposal.data.justify_qc().data.block_number else {
99 bail!("Quorum Proposal has no block number but it's after the epoch upgrade");
100 };
101 if is_epoch_transition(block_number, validation_info.epoch_height) {
102 validate_epoch_transition_qc(proposal, validation_info).await?;
103 valid_epoch_transition = true;
104 }
105 }
106 let mut consensus_writer = validation_info.consensus.write().await;
107
108 let leaf = Leaf2::from_quorum_proposal(&proposal.data);
109
110 let state = Arc::new(
111 <TYPES::ValidatedState as ValidatedState<TYPES>>::from_header(proposal.data.block_header()),
112 );
113
114 if let Err(e) = consensus_writer.update_leaf(leaf.clone(), state, None) {
115 tracing::trace!("{e:?}");
116 }
117
118 let liveness_check = proposal.data.justify_qc().view_number() > consensus_writer.locked_view();
119 if liveness_check
121 && validation_info
122 .upgrade_lock
123 .version(leaf.view_number())
124 .is_ok_and(|v| v >= EPOCH_VERSION)
125 {
126 consensus_writer.update_locked_view(proposal.data.justify_qc().view_number())?;
127 }
128
129 drop(consensus_writer);
130
131 if !liveness_check && !valid_epoch_transition {
132 bail!("Quorum Proposal failed the liveness check");
133 }
134
135 Ok(())
136}
137
138async fn validate_epoch_transition_block<TYPES: NodeType, I: NodeImplementation<TYPES>>(
139 proposal: &Proposal<TYPES, QuorumProposalWrapper<TYPES>>,
140 validation_info: &ValidationInfo<TYPES, I>,
141) -> Result<()> {
142 if !validation_info
143 .upgrade_lock
144 .epochs_enabled(proposal.data.view_number())
145 {
146 return Ok(());
147 }
148 if !is_epoch_transition(
149 proposal.data.block_header().block_number(),
150 validation_info.epoch_height,
151 ) {
152 return Ok(());
153 }
154 if is_transition_block(
156 proposal.data.block_header().block_number(),
157 validation_info.epoch_height,
158 ) {
159 return Ok(());
160 }
161 let (empty_payload, metadata) = <TYPES as NodeType>::BlockPayload::empty();
163 let header = proposal.data.block_header();
164 ensure!(
165 empty_payload.builder_commitment(&metadata) == header.builder_commitment()
166 && &metadata == header.metadata(),
167 "Block is not empty"
168 );
169 Ok(())
170}
171
172async fn validate_current_epoch<TYPES: NodeType, I: NodeImplementation<TYPES>>(
173 proposal: &Proposal<TYPES, QuorumProposalWrapper<TYPES>>,
174 validation_info: &ValidationInfo<TYPES, I>,
175) -> Result<()> {
176 let upgrade_view = validation_info
177 .upgrade_lock
178 .upgrade_view()
179 .unwrap_or(ViewNumber::new(0));
180 if !validation_info
181 .upgrade_lock
182 .epochs_enabled(proposal.data.view_number())
183 || proposal.data.justify_qc().view_number() <= upgrade_view
184 {
185 return Ok(());
186 }
187 if validation_info
188 .consensus
189 .read()
190 .await
191 .high_qc()
192 .view_number()
193 <= upgrade_view
194 {
195 return Ok(());
196 }
197
198 let block_number = proposal.data.block_header().block_number();
199
200 let Some(high_block_number) = validation_info
201 .consensus
202 .read()
203 .await
204 .high_qc()
205 .data
206 .block_number
207 else {
208 return Ok(());
209 };
210
211 ensure!(
212 epoch_from_block_number(block_number, validation_info.epoch_height)
213 >= epoch_from_block_number(high_block_number + 1, validation_info.epoch_height),
214 "Quorum proposal has an inconsistent epoch"
215 );
216
217 Ok(())
218}
219
220async fn validate_block_height<TYPES: NodeType>(
222 proposal: &Proposal<TYPES, QuorumProposalWrapper<TYPES>>,
223) -> Result<()> {
224 let Some(qc_block_number) = proposal.data.justify_qc().data.block_number else {
225 return Ok(());
226 };
227 ensure!(
228 qc_block_number + 1 == proposal.data.block_header().block_number(),
229 "Quorum proposal has an inconsistent block height"
230 );
231 Ok(())
232}
233
234#[allow(clippy::too_many_lines)]
242#[instrument(skip_all)]
243pub(crate) async fn handle_quorum_proposal_recv<TYPES: NodeType, I: NodeImplementation<TYPES>>(
244 proposal: &Proposal<TYPES, QuorumProposalWrapper<TYPES>>,
245 quorum_proposal_sender_key: &TYPES::SignatureKey,
246 event_sender: &Sender<Arc<HotShotEvent<TYPES>>>,
247 event_receiver: &Receiver<Arc<HotShotEvent<TYPES>>>,
248 validation_info: ValidationInfo<TYPES, I>,
249) -> Result<()> {
250 proposal
251 .data
252 .validate_epoch(&validation_info.upgrade_lock, validation_info.epoch_height)
253 .await?;
254 validate_current_epoch(proposal, &validation_info).await?;
256 let quorum_proposal_sender_key = quorum_proposal_sender_key.clone();
257
258 validate_proposal_view_and_certs(proposal, &validation_info)
259 .await
260 .context(warn!("Failed to validate proposal view or attached certs"))?;
261
262 validate_block_height(proposal).await?;
263
264 let version = validation_info
265 .upgrade_lock
266 .version(proposal.data.view_number())?;
267
268 if version >= EPOCH_VERSION {
269 verify_drb_result(&proposal.data, &validation_info).await?;
271 }
272
273 let view_number = proposal.data.view_number();
274
275 let justify_qc = proposal.data.justify_qc().clone();
276 let maybe_next_epoch_justify_qc = proposal.data.next_epoch_justify_qc().clone();
277
278 let proposal_block_number = proposal.data.block_header().block_number();
279 let proposal_epoch = option_epoch_from_block_number(
280 proposal.data.epoch().is_some(),
281 proposal_block_number,
282 validation_info.epoch_height,
283 );
284
285 if justify_qc
286 .data
287 .block_number
288 .is_some_and(|bn| is_epoch_root(bn, validation_info.epoch_height))
289 {
290 let Some(state_cert) = proposal.data.state_cert() else {
291 bail!("Epoch root QC has no state cert");
292 };
293 ensure!(
294 check_qc_state_cert_correspondence(
295 &justify_qc,
296 state_cert,
297 validation_info.epoch_height
298 ),
299 "Epoch root QC has no corresponding state cert"
300 );
301 validate_light_client_state_update_certificate(
302 state_cert,
303 &validation_info.membership.coordinator,
304 &validation_info.upgrade_lock,
305 )
306 .await?;
307 }
308
309 validate_epoch_transition_block(proposal, &validation_info).await?;
310
311 validate_qc_and_next_epoch_qc(
312 &justify_qc,
313 maybe_next_epoch_justify_qc.as_ref(),
314 &validation_info.consensus,
315 &validation_info.membership.coordinator,
316 &validation_info.upgrade_lock,
317 validation_info.epoch_height,
318 )
319 .await?;
320
321 broadcast_event(
322 Arc::new(HotShotEvent::QuorumProposalPreliminarilyValidated(
323 proposal.clone(),
324 )),
325 event_sender,
326 )
327 .await;
328
329 let parent_leaf = validation_info
331 .consensus
332 .read()
333 .await
334 .saved_leaves()
335 .get(&justify_qc.data.leaf_commit)
336 .cloned();
337
338 if parent_leaf.is_none() {
339 spawn_fetch_proposal(
340 &justify_qc,
341 event_sender.clone(),
342 event_receiver.clone(),
343 validation_info.membership.coordinator.clone(),
344 OuterConsensus::new(Arc::clone(&validation_info.consensus.inner_consensus)),
345 validation_info.public_key.clone(),
349 validation_info.private_key.clone(),
350 validation_info.upgrade_lock.clone(),
351 validation_info.epoch_height,
352 );
353 }
354 let consensus_reader = validation_info.consensus.read().await;
355
356 let parent = match parent_leaf {
357 Some(leaf) => {
358 if let (Some(state), _) = consensus_reader.state_and_delta(leaf.view_number()) {
359 Some((leaf, Arc::clone(&state)))
360 } else {
361 bail!("Parent state not found! Consensus internally inconsistent");
362 }
363 },
364 None => None,
365 };
366 drop(consensus_reader);
367 if justify_qc.view_number()
368 > validation_info
369 .consensus
370 .read()
371 .await
372 .high_qc()
373 .view_number()
374 {
375 update_high_qc(proposal, &validation_info).await?;
376 }
377
378 let Some((parent_leaf, _parent_state)) = parent else {
379 tracing::warn!(
380 "Proposal's parent missing from storage with commitment: {:?}",
381 justify_qc.data.leaf_commit
382 );
383 validate_proposal_liveness(proposal, &validation_info).await?;
384 validation_info
385 .consensus
386 .write()
387 .await
388 .update_highest_block(proposal_block_number);
389 broadcast_view_change(
390 event_sender,
391 view_number,
392 proposal_epoch,
393 validation_info.first_epoch,
394 )
395 .await;
396 return Ok(());
397 };
398
399 validate_proposal_safety_and_liveness::<TYPES, I>(
401 proposal.clone(),
402 parent_leaf,
403 &validation_info,
404 event_sender.clone(),
405 quorum_proposal_sender_key,
406 )
407 .await?;
408
409 validation_info
410 .consensus
411 .write()
412 .await
413 .update_highest_block(proposal_block_number);
414 {
415 validation_info.consensus.write().await.highest_block = proposal_block_number;
416 }
417 broadcast_view_change(
418 event_sender,
419 view_number,
420 proposal_epoch,
421 validation_info.first_epoch,
422 )
423 .await;
424
425 Ok(())
426}