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