Skip to main content

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::{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/// Spawn a task which will fire a request to get a proposal, and store it.
55#[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/// Update states in the event that the parent state is not found for a given `proposal`.
87#[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 we are using HS2 we update our locked view for any QC from a leader greater than our current lock
120    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    // transition block does not have to be empty
155    if is_transition_block(
156        proposal.data.block_header().block_number(),
157        validation_info.epoch_height,
158    ) {
159        return Ok(());
160    }
161    // TODO: Is this the best way to do this?
162    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
220/// Validate that the proposal's block height is one greater than the justification QC's block height.
221async 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/// Handles the `QuorumProposalRecv` event by first validating the cert itself for the view, and then
235/// updating the states, which runs when the proposal cannot be found in the internal state map.
236///
237/// This code can fail when:
238/// - The justify qc is invalid.
239/// - The task is internally inconsistent.
240/// - The sequencer storage update fails.
241#[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 the proposal's epoch matches ours
255    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        // Don't vote if the DRB result verification fails.
270        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    // Get the parent leaf and state.
330    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            // Note that we explicitly use the node key here instead of the provided key in the signature.
346            // This is because the key that we receive is for the prior leader, so the payload would be routed
347            // incorrectly.
348            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 the proposal
400    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}