hotshot_task_impls/quorum_proposal_recv/
handlers.rs

1// Copyright (c) 2021-2024 Espresso Systems (espressosys.com)
2// This file is part of the HotShot repository.
3
4// You should have received a copy of the MIT License
5// along with the HotShot repository. If not, see <https://mit-license.org/>.
6
7#![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/// Spawn a task which will fire a request to get a proposal, and store it.
53#[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/// Update states in the event that the parent state is not found for a given `proposal`.
85#[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 we are using HS2 we update our locked view for any QC from a leader greater than our current lock
123    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    // transition block does not have to be empty
164    if is_transition_block(
165        proposal.data.block_header().block_number(),
166        validation_info.epoch_height,
167    ) {
168        return Ok(());
169    }
170    // TODO: Is this the best way to do this?
171    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
231/// Validate that the proposal's block height is one greater than the justification QC's block height.
232async 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/// Handles the `QuorumProposalRecv` event by first validating the cert itself for the view, and then
246/// updating the states, which runs when the proposal cannot be found in the internal state map.
247///
248/// This code can fail when:
249/// - The justify qc is invalid.
250/// - The task is internally inconsistent.
251/// - The sequencer storage update fails.
252#[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 the proposal's epoch matches ours
270    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        // Don't vote if the DRB result verification fails.
286        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    // Get the parent leaf and state.
345    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            // Note that we explicitly use the node key here instead of the provided key in the signature.
361            // This is because the key that we receive is for the prior leader, so the payload would be routed
362            // incorrectly.
363            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 the proposal
415    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}