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,
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 view_number = proposal.data.view_number();
280
281 let justify_qc = proposal.data.justify_qc().clone();
282 let maybe_next_epoch_justify_qc = proposal.data.next_epoch_justify_qc().clone();
283
284 let proposal_block_number = proposal.data.block_header().block_number();
285 let proposal_epoch = option_epoch_from_block_number::<TYPES>(
286 proposal.data.epoch().is_some(),
287 proposal_block_number,
288 validation_info.epoch_height,
289 );
290
291 if justify_qc
292 .data
293 .block_number
294 .is_some_and(|bn| is_epoch_root(bn, validation_info.epoch_height))
295 {
296 let Some(state_cert) = proposal.data.state_cert() else {
297 bail!("Epoch root QC has no state cert");
298 };
299 ensure!(
300 check_qc_state_cert_correspondence(
301 &justify_qc,
302 state_cert,
303 validation_info.epoch_height
304 ),
305 "Epoch root QC has no corresponding state cert"
306 );
307 validate_light_client_state_update_certificate(
308 state_cert,
309 &validation_info.membership.coordinator,
310 )
311 .await?;
312 }
313
314 validate_epoch_transition_block(proposal, &validation_info).await?;
315
316 validate_qc_and_next_epoch_qc(
317 &justify_qc,
318 maybe_next_epoch_justify_qc.as_ref(),
319 &validation_info.consensus,
320 &validation_info.membership.coordinator,
321 &validation_info.upgrade_lock,
322 validation_info.epoch_height,
323 )
324 .await?;
325
326 broadcast_event(
327 Arc::new(HotShotEvent::QuorumProposalPreliminarilyValidated(
328 proposal.clone(),
329 )),
330 event_sender,
331 )
332 .await;
333
334 let parent_leaf = validation_info
336 .consensus
337 .read()
338 .await
339 .saved_leaves()
340 .get(&justify_qc.data.leaf_commit)
341 .cloned();
342
343 if parent_leaf.is_none() {
344 spawn_fetch_proposal(
345 &justify_qc,
346 event_sender.clone(),
347 event_receiver.clone(),
348 validation_info.membership.coordinator.clone(),
349 OuterConsensus::new(Arc::clone(&validation_info.consensus.inner_consensus)),
350 validation_info.public_key.clone(),
354 validation_info.private_key.clone(),
355 validation_info.upgrade_lock.clone(),
356 validation_info.epoch_height,
357 );
358 }
359 let consensus_reader = validation_info.consensus.read().await;
360
361 let parent = match parent_leaf {
362 Some(leaf) => {
363 if let (Some(state), _) = consensus_reader.state_and_delta(leaf.view_number()) {
364 Some((leaf, Arc::clone(&state)))
365 } else {
366 bail!("Parent state not found! Consensus internally inconsistent");
367 }
368 },
369 None => None,
370 };
371 drop(consensus_reader);
372 if justify_qc.view_number()
373 > validation_info
374 .consensus
375 .read()
376 .await
377 .high_qc()
378 .view_number()
379 {
380 update_high_qc(proposal, &validation_info).await?;
381 }
382
383 let Some((parent_leaf, _parent_state)) = parent else {
384 tracing::warn!(
385 "Proposal's parent missing from storage with commitment: {:?}",
386 justify_qc.data.leaf_commit
387 );
388 validate_proposal_liveness(proposal, &validation_info).await?;
389 validation_info
390 .consensus
391 .write()
392 .await
393 .update_highest_block(proposal_block_number);
394 broadcast_view_change(
395 event_sender,
396 view_number,
397 proposal_epoch,
398 validation_info.first_epoch,
399 )
400 .await;
401 return Ok(());
402 };
403
404 validate_proposal_safety_and_liveness::<TYPES, I, V>(
406 proposal.clone(),
407 parent_leaf,
408 &validation_info,
409 event_sender.clone(),
410 quorum_proposal_sender_key,
411 )
412 .await?;
413
414 validation_info
415 .consensus
416 .write()
417 .await
418 .update_highest_block(proposal_block_number);
419 {
420 validation_info.consensus.write().await.highest_block = proposal_block_number;
421 }
422 broadcast_view_change(
423 event_sender,
424 view_number,
425 proposal_epoch,
426 validation_info.first_epoch,
427 )
428 .await;
429
430 Ok(())
431}