Skip to main content

hotshot_types/
event.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//! Events that a `HotShot` instance can emit
8
9use std::sync::Arc;
10
11use hotshot_utils::anytrace::*;
12use serde::{Deserialize, Serialize};
13
14use crate::{
15    data::{
16        DaProposal, DaProposal2, Leaf, Leaf2, QuorumProposal, QuorumProposalWrapper,
17        UpgradeProposal, VidDisperseShare, VidDisperseShare0, ViewNumber,
18    },
19    error::HotShotError,
20    message::{Proposal, convert_proposal},
21    simple_certificate::{CertificatePair, LightClientStateUpdateCertificateV2, QuorumCertificate},
22    simple_vote::TimeoutVote2,
23    traits::{ValidatedState, node_implementation::NodeType},
24    vote::HasViewNumber,
25};
26
27/// A status event emitted by a `HotShot` instance
28///
29/// This includes some metadata, such as the stage and view number that the event was generated in,
30/// as well as an inner [`EventType`] describing the event proper.
31#[derive(Clone, Debug, Serialize, Deserialize)]
32#[serde(bound(deserialize = "TYPES: NodeType"))]
33pub struct Event<TYPES: NodeType> {
34    /// The view number that this event originates from
35    pub view_number: ViewNumber,
36    /// The underlying event
37    pub event: EventType<TYPES>,
38}
39
40impl<TYPES: NodeType> Event<TYPES> {
41    pub fn to_legacy(self) -> anyhow::Result<LegacyEvent<TYPES>> {
42        Ok(LegacyEvent {
43            view_number: self.view_number,
44            event: self.event.to_legacy()?,
45        })
46    }
47}
48
49/// The pre-epoch version of an Event
50#[derive(Clone, Debug, Serialize, Deserialize)]
51#[serde(bound(deserialize = "TYPES: NodeType"))]
52pub struct LegacyEvent<TYPES: NodeType> {
53    /// The view number that this event originates from
54    pub view_number: ViewNumber,
55    /// The underlying event
56    pub event: LegacyEventType<TYPES>,
57}
58
59/// Decided leaf with the corresponding state and VID info.
60#[derive(Clone, Debug, Serialize, Deserialize)]
61#[serde(bound(deserialize = "TYPES: NodeType"))]
62pub struct LeafInfo<TYPES: NodeType> {
63    /// Decided leaf.
64    pub leaf: Leaf2<TYPES>,
65    /// Validated state.
66    pub state: Arc<<TYPES as NodeType>::ValidatedState>,
67    /// Optional application-specific state delta.
68    pub delta: Option<Arc<<<TYPES as NodeType>::ValidatedState as ValidatedState<TYPES>>::Delta>>,
69    /// Optional VID share data.
70    pub vid_share: Option<VidDisperseShare<TYPES>>,
71    /// Optional light client state update certificate.
72    pub state_cert: Option<LightClientStateUpdateCertificateV2<TYPES>>,
73}
74
75impl<TYPES: NodeType> LeafInfo<TYPES> {
76    /// Constructor.
77    pub fn new(
78        leaf: Leaf2<TYPES>,
79        state: Arc<<TYPES as NodeType>::ValidatedState>,
80        delta: Option<Arc<<<TYPES as NodeType>::ValidatedState as ValidatedState<TYPES>>::Delta>>,
81        vid_share: Option<VidDisperseShare<TYPES>>,
82        state_cert: Option<LightClientStateUpdateCertificateV2<TYPES>>,
83    ) -> Self {
84        Self {
85            leaf,
86            state,
87            delta,
88            vid_share,
89            state_cert,
90        }
91    }
92
93    pub fn to_legacy_unsafe(self) -> anyhow::Result<LegacyLeafInfo<TYPES>> {
94        Ok(LegacyLeafInfo {
95            leaf: self.leaf.to_leaf_unsafe(),
96            state: self.state,
97            delta: self.delta,
98            vid_share: self
99                .vid_share
100                .map(|share| match share {
101                    VidDisperseShare::V0(share) => Ok(share),
102                    _ => Err(error!("VID share is post-epoch")),
103                })
104                .transpose()?,
105        })
106    }
107}
108
109/// Pre-epoch version of `LeafInfo`
110#[derive(Clone, Debug, Serialize, Deserialize)]
111#[serde(bound(deserialize = "TYPES: NodeType"))]
112pub struct LegacyLeafInfo<TYPES: NodeType> {
113    /// Decided leaf.
114    pub leaf: Leaf<TYPES>,
115    /// Validated state.
116    pub state: Arc<<TYPES as NodeType>::ValidatedState>,
117    /// Optional application-specific state delta.
118    pub delta: Option<Arc<<<TYPES as NodeType>::ValidatedState as ValidatedState<TYPES>>::Delta>>,
119    /// Optional VID share data.
120    pub vid_share: Option<VidDisperseShare0<TYPES>>,
121}
122
123impl<TYPES: NodeType> LegacyLeafInfo<TYPES> {
124    /// Constructor.
125    pub fn new(
126        leaf: Leaf<TYPES>,
127        state: Arc<<TYPES as NodeType>::ValidatedState>,
128        delta: Option<Arc<<<TYPES as NodeType>::ValidatedState as ValidatedState<TYPES>>::Delta>>,
129        vid_share: Option<VidDisperseShare0<TYPES>>,
130    ) -> Self {
131        Self {
132            leaf,
133            state,
134            delta,
135            vid_share,
136        }
137    }
138}
139
140/// The chain of decided leaves with its corresponding state and VID info.
141pub type LeafChain<TYPES> = Vec<LeafInfo<TYPES>>;
142
143/// Pre-epoch version of `LeafChain`
144pub type LegacyLeafChain<TYPES> = Vec<LegacyLeafInfo<TYPES>>;
145
146/// Utilities for converting between HotShotError and a string.
147pub mod error_adaptor {
148    use serde::{de::Deserializer, ser::Serializer};
149
150    use super::{Arc, Deserialize, HotShotError, NodeType};
151
152    /// Convert a HotShotError into a string
153    ///
154    /// # Errors
155    /// Returns `Err` if the serializer fails.
156    pub fn serialize<S: Serializer, TYPES: NodeType>(
157        elem: &Arc<HotShotError<TYPES>>,
158        serializer: S,
159    ) -> Result<S::Ok, S::Error> {
160        serializer.serialize_str(&format!("{elem}"))
161    }
162
163    /// Convert a string into a HotShotError
164    ///
165    /// # Errors
166    /// Returns `Err` if the string cannot be deserialized.
167    pub fn deserialize<'de, D: Deserializer<'de>, TYPES: NodeType>(
168        deserializer: D,
169    ) -> Result<Arc<HotShotError<TYPES>>, D::Error> {
170        let str = String::deserialize(deserializer)?;
171        Ok(Arc::new(HotShotError::FailedToDeserialize(str)))
172    }
173}
174
175/// The type and contents of a status event emitted by a `HotShot` instance
176///
177/// This enum does not include metadata shared among all variants, such as the stage and view
178/// number, and is thus always returned wrapped in an [`Event`].
179#[non_exhaustive]
180#[derive(Clone, Debug, Serialize, Deserialize)]
181#[serde(bound(deserialize = "TYPES: NodeType"))]
182#[allow(clippy::large_enum_variant)]
183pub enum EventType<TYPES: NodeType> {
184    /// A view encountered an error and was interrupted
185    Error {
186        /// The underlying error
187        #[serde(with = "error_adaptor")]
188        error: Arc<HotShotError<TYPES>>,
189    },
190    /// A new decision event was issued
191    Decide {
192        /// The chain of Leaves that were committed by this decision
193        ///
194        /// This list is sorted in reverse view number order, with the newest (highest view number)
195        /// block first in the list.
196        ///
197        /// This list may be incomplete if the node is currently performing catchup.
198        /// Vid Info for a decided view may be missing if this node never saw it's share.
199        leaf_chain: Arc<LeafChain<TYPES>>,
200        /// The QC signing the most recent leaf in `leaf_chain`.
201        ///
202        /// Note that the QC for each additional leaf in the chain can be obtained from the leaf
203        /// before it using
204        committing_qc: Arc<CertificatePair<TYPES>>,
205        /// A QC signing the leaf corresponding to `qc`.
206        ///
207        /// Together with `qc`, this forms a 2-chain, which is sufficient for a light client to
208        /// verify that the leaf chain contained in this event is in fact decided.
209        deciding_qc: Option<Arc<CertificatePair<TYPES>>>,
210        /// Optional information of the number of transactions in the block, for logging purposes.
211        block_size: Option<u64>,
212    },
213    /// A replica task was canceled by a timeout interrupt
214    ReplicaViewTimeout {
215        /// The view that timed out
216        view_number: ViewNumber,
217    },
218    /// The view has finished.  If values were decided on, a `Decide` event will also be emitted.
219    ViewFinished {
220        /// The view number that has just finished
221        view_number: ViewNumber,
222    },
223    /// The view timed out
224    ViewTimeout {
225        /// The view that timed out
226        view_number: ViewNumber,
227    },
228    /// New transactions were received from the network
229    /// or submitted to the network by us
230    Transactions {
231        /// The list of transactions
232        transactions: Vec<TYPES::Transaction>,
233    },
234    /// DA proposal was received from the network
235    /// or submitted to the network by us
236    DaProposal {
237        /// Contents of the proposal
238        proposal: Proposal<TYPES, DaProposal2<TYPES>>,
239        /// Public key of the leader submitting the proposal
240        sender: TYPES::SignatureKey,
241    },
242    /// Quorum proposal was received from the network
243    /// or submitted to the network by us
244    QuorumProposal {
245        /// Contents of the proposal
246        proposal: Proposal<TYPES, QuorumProposalWrapper<TYPES>>,
247        /// Public key of the leader submitting the proposal
248        sender: TYPES::SignatureKey,
249    },
250    /// Upgrade proposal was received from the network
251    /// or submitted to the network by us
252    UpgradeProposal {
253        /// Contents of the proposal
254        proposal: Proposal<TYPES, UpgradeProposal>,
255        /// Public key of the leader submitting the proposal
256        sender: TYPES::SignatureKey,
257    },
258
259    /// A message destined for external listeners was received
260    ExternalMessageReceived {
261        /// Public Key of the message sender
262        sender: TYPES::SignatureKey,
263        /// Serialized data of the message
264        data: Vec<u8>,
265    },
266
267    /// Emitted by the legacy consensus task whenever it signs and broadcasts a
268    /// `TimeoutVote2`. Used at the legacy → new-protocol upgrade boundary so
269    /// the espresso bridge can forward the same vote into the new-protocol
270    /// coordinator's vote collectors. The wire-level protocols differ but the
271    /// underlying `TimeoutVote2` type and its version-tagged signature
272    /// commitment are shared, so the same vote is valid in both systems.
273    LegacyTimeoutVoteEmitted {
274        /// The vote that was signed and broadcast on the legacy wire.
275        vote: TimeoutVote2<TYPES>,
276    },
277}
278
279impl<TYPES: NodeType> std::fmt::Display for EventType<TYPES> {
280    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281        match self {
282            Self::Error { error } => write!(f, "Error: {error}"),
283            Self::Decide { leaf_chain, .. } => {
284                let newest = leaf_chain.first().map(|l| l.leaf.view_number());
285                let oldest = leaf_chain.last().map(|l| l.leaf.view_number());
286                write!(
287                    f,
288                    "Decide: leaves={} oldest={:?} newest={:?}",
289                    leaf_chain.len(),
290                    oldest,
291                    newest,
292                )
293            },
294            Self::ReplicaViewTimeout { view_number } => {
295                write!(f, "ReplicaViewTimeout: view={view_number}")
296            },
297            Self::ViewFinished { view_number } => {
298                write!(f, "ViewFinished: view={view_number}")
299            },
300            Self::ViewTimeout { view_number } => {
301                write!(f, "ViewTimeout: view={view_number}")
302            },
303            Self::Transactions { transactions } => {
304                write!(f, "Transactions: count={}", transactions.len())
305            },
306            Self::DaProposal { proposal, .. } => {
307                write!(f, "DaProposal: view={}", proposal.data.view_number())
308            },
309            Self::QuorumProposal { proposal, .. } => {
310                write!(f, "QuorumProposal: view={}", proposal.data.view_number())
311            },
312            Self::UpgradeProposal { proposal, .. } => {
313                write!(
314                    f,
315                    "UpgradeProposal: view={} old_version={} new_version={}",
316                    proposal.data.view_number,
317                    proposal.data.upgrade_proposal.old_version,
318                    proposal.data.upgrade_proposal.new_version,
319                )
320            },
321            Self::ExternalMessageReceived { .. } => {
322                write!(f, "ExternalMessageReceived")
323            },
324            Self::LegacyTimeoutVoteEmitted { vote } => {
325                write!(f, "LegacyTimeoutVoteEmitted: view={}", vote.view_number())
326            },
327        }
328    }
329}
330
331impl<TYPES: NodeType> EventType<TYPES> {
332    pub fn to_legacy(self) -> anyhow::Result<LegacyEventType<TYPES>> {
333        Ok(match self {
334            EventType::Error { error } => LegacyEventType::Error { error },
335            EventType::Decide {
336                leaf_chain,
337                committing_qc: qc,
338                block_size,
339                ..
340            } => LegacyEventType::Decide {
341                leaf_chain: Arc::new(
342                    leaf_chain
343                        .iter()
344                        .cloned()
345                        .map(LeafInfo::to_legacy_unsafe)
346                        .collect::<anyhow::Result<_, _>>()?,
347                ),
348                qc: Arc::new(qc.qc().clone().to_qc()),
349                block_size,
350            },
351            EventType::ReplicaViewTimeout { view_number } => {
352                LegacyEventType::ReplicaViewTimeout { view_number }
353            },
354            EventType::ViewFinished { view_number } => {
355                LegacyEventType::ViewFinished { view_number }
356            },
357            EventType::ViewTimeout { view_number } => LegacyEventType::ViewTimeout { view_number },
358            EventType::Transactions { transactions } => {
359                LegacyEventType::Transactions { transactions }
360            },
361            EventType::DaProposal { proposal, sender } => LegacyEventType::DaProposal {
362                proposal: convert_proposal(proposal),
363                sender,
364            },
365            EventType::QuorumProposal { proposal, sender } => LegacyEventType::QuorumProposal {
366                proposal: convert_proposal(proposal),
367                sender,
368            },
369            EventType::UpgradeProposal { proposal, sender } => {
370                LegacyEventType::UpgradeProposal { proposal, sender }
371            },
372            EventType::ExternalMessageReceived { sender, data } => {
373                LegacyEventType::ExternalMessageReceived { sender, data }
374            },
375            // Upgrade-bridging event: doesn't exist in the pre-epoch event
376            // surface. Convert to a no-op equivalent (drop) since legacy
377            // consumers wouldn't know what to do with it.
378            EventType::LegacyTimeoutVoteEmitted { .. } => {
379                anyhow::bail!(
380                    "LegacyTimeoutVoteEmitted is upgrade-bridging only and has no legacy \
381                     equivalent"
382                )
383            },
384        })
385    }
386}
387
388/// Pre-epoch version of the `EventType` enum.
389#[non_exhaustive]
390#[derive(Clone, Debug, Serialize, Deserialize)]
391#[serde(bound(deserialize = "TYPES: NodeType"))]
392#[allow(clippy::large_enum_variant)]
393pub enum LegacyEventType<TYPES: NodeType> {
394    /// A view encountered an error and was interrupted
395    Error {
396        /// The underlying error
397        #[serde(with = "error_adaptor")]
398        error: Arc<HotShotError<TYPES>>,
399    },
400    /// A new decision event was issued
401    Decide {
402        /// The chain of Leaves that were committed by this decision
403        ///
404        /// This list is sorted in reverse view number order, with the newest (highest view number)
405        /// block first in the list.
406        ///
407        /// This list may be incomplete if the node is currently performing catchup.
408        /// Vid Info for a decided view may be missing if this node never saw it's share.
409        leaf_chain: Arc<LegacyLeafChain<TYPES>>,
410        /// The QC signing the most recent leaf in `leaf_chain`.
411        ///
412        /// Note that the QC for each additional leaf in the chain can be obtained from the leaf
413        /// before it using
414        qc: Arc<QuorumCertificate<TYPES>>,
415        /// Optional information of the number of transactions in the block, for logging purposes.
416        block_size: Option<u64>,
417    },
418    /// A replica task was canceled by a timeout interrupt
419    ReplicaViewTimeout {
420        /// The view that timed out
421        view_number: ViewNumber,
422    },
423    /// The view has finished.  If values were decided on, a `Decide` event will also be emitted.
424    ViewFinished {
425        /// The view number that has just finished
426        view_number: ViewNumber,
427    },
428    /// The view timed out
429    ViewTimeout {
430        /// The view that timed out
431        view_number: ViewNumber,
432    },
433    /// New transactions were received from the network
434    /// or submitted to the network by us
435    Transactions {
436        /// The list of transactions
437        transactions: Vec<TYPES::Transaction>,
438    },
439    /// DA proposal was received from the network
440    /// or submitted to the network by us
441    DaProposal {
442        /// Contents of the proposal
443        proposal: Proposal<TYPES, DaProposal<TYPES>>,
444        /// Public key of the leader submitting the proposal
445        sender: TYPES::SignatureKey,
446    },
447    /// Quorum proposal was received from the network
448    /// or submitted to the network by us
449    QuorumProposal {
450        /// Contents of the proposal
451        proposal: Proposal<TYPES, QuorumProposal<TYPES>>,
452        /// Public key of the leader submitting the proposal
453        sender: TYPES::SignatureKey,
454    },
455    /// Upgrade proposal was received from the network
456    /// or submitted to the network by us
457    UpgradeProposal {
458        /// Contents of the proposal
459        proposal: Proposal<TYPES, UpgradeProposal>,
460        /// Public key of the leader submitting the proposal
461        sender: TYPES::SignatureKey,
462    },
463
464    /// A message destined for external listeners was received
465    ExternalMessageReceived {
466        /// Public Key of the message sender
467        sender: TYPES::SignatureKey,
468        /// Serialized data of the message
469        data: Vec<u8>,
470    },
471}
472
473#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
474/// A list of actions that we track for nodes
475pub enum HotShotAction {
476    /// A quorum vote was sent
477    Vote,
478    /// A timeout vote was sent
479    TimeoutVote,
480    /// View Sync Vote
481    ViewSyncVote,
482    /// A quorum proposal was sent
483    Propose,
484    /// DA proposal was sent
485    DaPropose,
486    /// DA vote was sent
487    DaVote,
488    /// DA certificate was sent
489    DaCert,
490    /// VID shares were sent
491    VidDisperse,
492    /// An upgrade vote was sent
493    UpgradeVote,
494    /// An upgrade proposal was sent
495    UpgradePropose,
496}