Skip to main content

hotshot_query_service/data_source/
update.rs

1// Copyright (c) 2022 Espresso Systems (espressosys.com)
2// This file is part of the HotShot Query Service library.
3//
4// This program is free software: you can redistribute it and/or modify it under the terms of the GNU
5// General Public License as published by the Free Software Foundation, either version 3 of the
6// License, or (at your option) any later version.
7// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
8// even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9// General Public License for more details.
10// You should have received a copy of the GNU General Public License along with this program. If not,
11// see <https://www.gnu.org/licenses/>.
12
13//! A generic algorithm for updating a HotShot Query Service data source with new data.
14use std::iter::once;
15
16use anyhow::{Context, ensure};
17use async_trait::async_trait;
18use committable::Committable;
19use futures::future::Future;
20use hotshot::types::EventType;
21use hotshot_types::{
22    data::{
23        Leaf2, VidCommitment, VidCommon, VidDisperseShare, VidShare, ViewNumber,
24        ns_table::parse_ns_table,
25    },
26    event::LeafInfo,
27    new_protocol::CoordinatorEvent,
28    traits::{
29        block_contents::{BlockHeader, BlockPayload, EncodeBytes, GENESIS_VID_NUM_STORAGE_NODES},
30        node_implementation::NodeType,
31    },
32    vid::{
33        advz::advz_scheme,
34        avidm::{AvidMScheme, init_avidm_param},
35        avidm_gf2::{AvidmGf2Scheme, init_avidm_gf2_param},
36    },
37    vote::HasViewNumber,
38};
39use jf_advz::VidScheme;
40
41use crate::{
42    Header, Payload,
43    availability::{
44        BlockInfo, BlockQueryData, LeafQueryData, QueryableHeader, QueryablePayload,
45        UpdateAvailabilityData, VidCommonQueryData,
46    },
47    types::HeightIndexed,
48};
49
50/// An extension trait for types which implement the update trait for each API module.
51///
52/// If a type implements [UpdateAvailabilityData] and
53/// [UpdateStatusData](crate::status::UpdateStatusData), then it can be fully kept up to date
54/// through two interfaces:
55/// * [populate_metrics](crate::status::UpdateStatusData::populate_metrics), to get a handle for
56///   populating the status metrics, which should be used when initializing a
57///   [SystemContextHandle](hotshot::types::SystemContextHandle)
58/// * [update](Self::update), provided by this extension trait, to update the query state when a new
59///   HotShot event is emitted
60#[async_trait]
61pub trait UpdateDataSource<Types: NodeType>: UpdateAvailabilityData<Types> {
62    /// Update query state based on consensus event.
63    ///
64    /// The caller is responsible for authenticating `event`. This function does not perform any
65    /// authentication, and if given an invalid `event` (one which does not follow from the latest
66    /// known state of the ledger) it may panic or silently accept the invalid `event`. This allows
67    /// the best possible performance in the case where the query service and the HotShot instance
68    /// are running in the same process (and thus the event stream, directly from HotShot) is
69    /// trusted.
70    ///
71    /// If you want to update the data source with an untrusted event, for example one received from
72    /// a peer over the network, you must authenticate it first.
73    ///
74    ///
75    /// For each decided leaf the query service stores a `BlockInfo` containing the leaf paired
76    /// with a QC that certifies it (`LeafQueryData`), the block payload and VID data when
77    /// available, and only on the **newest** leaf in the batch, it stores the proof that finalizes it:
78    /// a `qc_chain` for legacy protocol set via `BlockInfo::with_qc_chain`, or a `cert2` for new protocol set
79    /// via `BlockInfo::with_cert2`. The two protocols differ only in where those pieces come
80    /// from:
81    ///
82    /// In both events leaves arrive in **newest → oldest** order (`leaves[0]` is the leaf being
83    /// finalized; each subsequent leaf is its ancestor reached via `justify_qc`). For the new
84    /// protocol, `vid_shares` is parallel to `leaves` (same ordering, one share per leaf). The
85    /// handler iterates in reverse so heights are appended ascending.
86    ///
87    /// - **Legacy (`CoordinatorEvent::LegacyEvent` → `EventType::Decide`).** The newest leaf is
88    ///   certified by `committing_qc`; each older leaf is certified by the *next-newer* leaf's
89    ///   `justify_qc`. The newest leaf's `qc_chain` is set to `[committing_qc, deciding_qc]` —
90    ///   the two consecutive QCs that decide it under the legacy 3-chain rule.
91    ///
92    /// - **New protocol (`CoordinatorEvent::NewDecide`).** The newest leaf is certified by
93    ///   `cert1`; older leaves are again certified by the next leaf's `justify_qc`. When a
94    ///   `cert2` is present, it is attached to the newest leaf. Under the new protocol a single
95    ///   `cert2` finalizes that leaf directly, replacing the legacy QC chain.
96    ///
97    /// # Returns
98    ///
99    /// If all provided data is successfully inserted into the database, returns `Ok(())`. If any
100    /// error occurred, the error is logged, and the return value is the height of the first leaf
101    /// which failed to be inserted.
102    async fn update(&self, event: &CoordinatorEvent<Types>) -> Result<(), u64>;
103}
104
105#[async_trait]
106impl<Types: NodeType, T> UpdateDataSource<Types> for T
107where
108    T: UpdateAvailabilityData<Types> + Send + Sync,
109    Header<Types>: QueryableHeader<Types>,
110    Payload<Types>: QueryablePayload<Types>,
111{
112    async fn update(&self, event: &CoordinatorEvent<Types>) -> Result<(), u64> {
113        match event {
114            CoordinatorEvent::LegacyEvent(event) => {
115                let EventType::Decide {
116                    leaf_chain,
117                    committing_qc,
118                    deciding_qc,
119                    ..
120                } = &event.event
121                else {
122                    return Ok(());
123                };
124
125                // `qc` justifies the first (most recent) leaf...
126                let qcs = once(committing_qc.qc().clone())
127                    // ...and each leaf in the chain justifies the subsequent leaf (its parent)
128                    // through `leaf.justify_qc`.
129                    .chain(leaf_chain.iter().map(|leaf| leaf.leaf.justify_qc()))
130                    // Put the QCs in chronological order.
131                    .rev()
132                    // The oldest QC is the `justify_qc` of the oldest leaf, which does not justify
133                    // any leaf in the new chain, so we don't need it.
134                    .skip(1);
135                for (
136                    qc2,
137                    LeafInfo {
138                        leaf: leaf2,
139                        vid_share,
140                        ..
141                    },
142                ) in qcs.zip(leaf_chain.iter().rev())
143                {
144                    let height = leaf2.block_header().block_number();
145
146                    let leaf_data = match LeafQueryData::new(leaf2.clone(), qc2.clone()) {
147                        Ok(leaf) => leaf,
148                        Err(err) => {
149                            tracing::error!(
150                                height,
151                                ?leaf2,
152                                ?committing_qc,
153                                "inconsistent leaf; cannot append leaf information: {err:#}"
154                            );
155                            return Err(leaf2.block_header().block_number());
156                        },
157                    };
158                    let block_data = leaf2
159                        .block_payload()
160                        .map(|payload| BlockQueryData::new(leaf2.block_header().clone(), payload));
161                    if block_data.is_none() {
162                        tracing::warn!(height, "block payload missing at decide");
163                    }
164
165                    let (vid_common, vid_share) = match vid_share {
166                        Some(VidDisperseShare::V0(share)) => (
167                            Some(VidCommonQueryData::new(
168                                leaf2.block_header().clone(),
169                                VidCommon::V0(share.common.clone()),
170                            )),
171                            Some(VidShare::V0(share.share.clone())),
172                        ),
173                        Some(VidDisperseShare::V1(share)) => (
174                            Some(VidCommonQueryData::new(
175                                leaf2.block_header().clone(),
176                                VidCommon::V1(share.common.clone()),
177                            )),
178                            Some(VidShare::V1(share.share.clone())),
179                        ),
180                        Some(VidDisperseShare::V2(share)) => (
181                            Some(VidCommonQueryData::new(
182                                leaf2.block_header().clone(),
183                                VidCommon::V2(share.common.clone()),
184                            )),
185                            Some(VidShare::V2(share.share.clone())),
186                        ),
187                        None => {
188                            if leaf2.view_number() == ViewNumber::genesis() {
189                                // HotShot does not run VID in consensus for the genesis block. In
190                                // this case, the block payload is guaranteed to always be empty, so
191                                // VID isn't really necessary. But for consistency, we will still
192                                // store the VID dispersal data, computing it ourselves based on the
193                                // well-known genesis VID commitment.
194                                match genesis_vid(leaf2) {
195                                    Ok((common, share)) => (Some(common), Some(share)),
196                                    Err(err) => {
197                                        tracing::warn!("failed to compute genesis VID: {err:#}");
198                                        (None, None)
199                                    },
200                                }
201                            } else {
202                                (None, None)
203                            }
204                        },
205                    };
206
207                    if vid_common.is_none() {
208                        tracing::info!(height, "VID not available at decide");
209                    }
210
211                    let mut info = BlockInfo::new(leaf_data, block_data, vid_common, vid_share);
212                    if let Some(deciding_qc) = deciding_qc
213                        && committing_qc.view_number() == info.leaf.leaf().view_number()
214                    {
215                        let qc_chain =
216                            [committing_qc.as_ref().clone(), deciding_qc.as_ref().clone()];
217                        info = info.with_qc_chain(qc_chain);
218                    }
219                    if let Err(err) = self.append(info).await {
220                        tracing::error!(height, "failed to append leaf information: {err:#}");
221                        return Err(leaf2.block_header().block_number());
222                    }
223                }
224            },
225            CoordinatorEvent::NewDecide {
226                leaf_infos,
227                cert1,
228                cert2,
229            } => {
230                let Some(first) = leaf_infos.first() else {
231                    tracing::error!("new decide event contained no leaves");
232                    return Ok(());
233                };
234                let first_leaf = &first.leaf;
235
236                if let Some(cert2) = cert2
237                    && cert2.data.leaf_commit != Committable::commit(first_leaf)
238                {
239                    tracing::error!(
240                        height = first_leaf.height(),
241                        cert2_leaf = %cert2.data.leaf_commit,
242                        newest_leaf = %Committable::commit(first_leaf),
243                        "new decide event cert2 does not certify the newest leaf"
244                    );
245                    return Err(first_leaf.height());
246                }
247
248                // `cert1` certifies the newest leaf; each newer leaf's justify_qc
249                // certifies the next older leaf.
250                let certifying_qcs = once(cert1.clone())
251                    .chain(leaf_infos.iter().map(|info| info.leaf.justify_qc()))
252                    .take(leaf_infos.len())
253                    .collect::<Vec<_>>();
254
255                for (index, (info, qc)) in leaf_infos.iter().zip(certifying_qcs).enumerate().rev() {
256                    let leaf = &info.leaf;
257                    let height = leaf.block_header().block_number();
258
259                    let leaf_data = match LeafQueryData::new(leaf.clone(), qc) {
260                        Ok(leaf) => leaf,
261                        Err(err) => {
262                            tracing::error!(
263                                height,
264                                ?leaf,
265                                "inconsistent leaf; cannot append leaf information: {err:#}"
266                            );
267                            return Err(height);
268                        },
269                    };
270
271                    let block_data = leaf
272                        .block_payload()
273                        .map(|payload| BlockQueryData::new(leaf.block_header().clone(), payload));
274                    if block_data.is_none() {
275                        tracing::warn!(height, "block payload missing at decide");
276                    }
277
278                    // Extract VID common data from the new protocol's VidDisperseShare2.
279                    let (vid_common, vid_share) = match &info.vid_share {
280                        Some(VidDisperseShare::V2(share)) => (
281                            Some(VidCommonQueryData::new(
282                                leaf.block_header().clone(),
283                                VidCommon::V2(share.common.clone()),
284                            )),
285                            Some(VidShare::V2(share.share.clone())),
286                        ),
287                        Some(_) => (None, None),
288                        None => {
289                            if leaf.view_number() == ViewNumber::genesis() {
290                                // HotShot does not run VID in consensus for the genesis block. In
291                                // this case, the block payload is guaranteed to always be empty, so
292                                // VID isn't really necessary. But for consistency, we will still
293                                // store the VID dispersal data, computing it ourselves based on the
294                                // well-known genesis VID commitment.
295                                match genesis_vid(leaf) {
296                                    Ok((common, share)) => (Some(common), Some(share)),
297                                    Err(err) => {
298                                        tracing::warn!("failed to compute genesis VID: {err:#}");
299                                        (None, None)
300                                    },
301                                }
302                            } else {
303                                (None, None)
304                            }
305                        },
306                    };
307
308                    if vid_common.is_none() {
309                        tracing::info!(height, "VID not available at decide");
310                    }
311
312                    let mut info = BlockInfo::new(leaf_data, block_data, vid_common, vid_share);
313
314                    // Attach `cert2` only to the newest leaf in the batch (`leaves[0]`, which is
315                    // `index == 0` since we iterate in reverse). Under the new protocol a single
316                    // `cert2` finalizes that leaf directly
317                    // older leaves in the batch are finalized using indirect rule
318                    if index == 0
319                        && let Some(cert2) = &cert2
320                    {
321                        info = info.with_cert2(cert2.clone());
322                    }
323
324                    if let Err(err) = self.append(info).await {
325                        tracing::error!(height, "failed to append leaf information: {err:#}");
326                        return Err(height);
327                    }
328                }
329            },
330            CoordinatorEvent::BlockPayloadReconstructed {
331                header, payload, ..
332            } => {
333                let block = BlockQueryData::new(header.clone(), payload.clone());
334                let height = block.height();
335                if let Err(err) = self.append_payload(block).await {
336                    tracing::error!(height, "failed to store reconstructed payload: {err:#}");
337                    return Err(height);
338                }
339            },
340            _ => {},
341        }
342        Ok(())
343    }
344}
345
346fn genesis_vid<Types: NodeType>(
347    leaf: &Leaf2<Types>,
348) -> anyhow::Result<(VidCommonQueryData<Types>, VidShare)> {
349    let payload = Payload::<Types>::empty().0;
350    let bytes = payload.encode();
351
352    match leaf.block_header().payload_commitment() {
353        VidCommitment::V0(commit) => {
354            let mut disperse = advz_scheme(GENESIS_VID_NUM_STORAGE_NODES)
355                .disperse(bytes)
356                .context("unable to compute VID dispersal for genesis block")?;
357
358            ensure!(
359                disperse.commit == commit,
360                "computed VID commit {} for genesis block does not match header commit {}",
361                disperse.commit,
362                commit
363            );
364            Ok((
365                VidCommonQueryData::new(
366                    leaf.block_header().clone(),
367                    VidCommon::V0(disperse.common),
368                ),
369                VidShare::V0(disperse.shares.remove(0)),
370            ))
371        },
372        VidCommitment::V1(commit) => {
373            let avidm_param = init_avidm_param(GENESIS_VID_NUM_STORAGE_NODES)?;
374            let weights = vec![1; GENESIS_VID_NUM_STORAGE_NODES];
375            let ns_table = parse_ns_table(bytes.len(), &leaf.block_header().metadata().encode());
376
377            let (calculated_commit, mut shares) =
378                AvidMScheme::ns_disperse(&avidm_param, &weights, &bytes, ns_table).unwrap();
379
380            ensure!(
381                calculated_commit == commit,
382                "computed VID commit {} for genesis block does not match header commit {}",
383                calculated_commit,
384                commit
385            );
386
387            Ok((
388                VidCommonQueryData::new(leaf.block_header().clone(), VidCommon::V1(avidm_param)),
389                VidShare::V1(shares.remove(0)),
390            ))
391        },
392        VidCommitment::V2(commit) => {
393            let avidm_gf2_param = init_avidm_gf2_param(GENESIS_VID_NUM_STORAGE_NODES)?;
394            let weights = vec![1; GENESIS_VID_NUM_STORAGE_NODES];
395            let ns_table = parse_ns_table(bytes.len(), &leaf.block_header().metadata().encode());
396
397            let (calculated_commit, common, mut shares) =
398                AvidmGf2Scheme::ns_disperse(&avidm_gf2_param, &weights, &bytes, ns_table).unwrap();
399
400            ensure!(
401                calculated_commit == commit,
402                "computed VID commit {} for genesis block does not match header commit {}",
403                calculated_commit,
404                commit
405            );
406
407            Ok((
408                VidCommonQueryData::new(leaf.block_header().clone(), VidCommon::V2(common)),
409                VidShare::V2(shares.remove(0)),
410            ))
411        },
412    }
413}
414
415/// A data source with an atomic transaction-based synchronization interface.
416///
417/// Changes are made to a versioned data source through a [`Transaction`]. Any changes made in a
418/// [`Transaction`] are initially visible only when queried through that same [`Transaction`]. They
419/// are not immediately written back to storage, which means that a new data source object opened
420/// against the same persistent storage will not reflect the changes. In particular, this means that
421/// if the process restarts and reopens its storage, uncommitted changes will be lost.
422///
423/// Only when a [`Transaction`] is committed are changes written back to storage, synchronized with
424/// any concurrent changes, and made visible to other connections to the same data source.
425pub trait VersionedDataSource: Send + Sync {
426    /// A transaction which can read and modify the data source.
427    type Transaction<'a>: Transaction
428    where
429        Self: 'a;
430
431    type ReadOnly<'a>: Transaction
432    where
433        Self: 'a;
434
435    /// Start an atomic transaction on the data source.
436    fn write(&self) -> impl Future<Output = anyhow::Result<Self::Transaction<'_>>> + Send;
437
438    /// Start a read-only transaction on the data source.
439    ///
440    /// A read-only transaction allows the owner to string together multiple queries of the data
441    /// source, which otherwise would not be atomic with respect to concurrent writes, in an atomic
442    /// fashion. Upon returning, [`read`](Self::read) locks in a fully consistent snapshot of the
443    /// data source, and any read operations performed upon the transaction thereafter read from the
444    /// same consistent snapshot. Concurrent modifications to the data source may occur (for
445    /// example, from concurrent [`write`](Self::write) transactions being committed), but their
446    /// results will not be reflected in a successful read-only transaction which was opened before
447    /// the write was committed.
448    ///
449    /// Read-only transactions do not need to be committed, and reverting has no effect.
450    fn read(&self) -> impl Future<Output = anyhow::Result<Self::ReadOnly<'_>>> + Send;
451}
452
453/// A unit of atomicity for updating a shared data source.
454///
455/// The methods provided by this trait can be used to write such pending changes back to persistent
456/// storage ([commit](Self::commit)) so that they become visible to other clients of the same
457/// underlying storage, and are saved if the process restarts. It also allows pending changes to be
458/// rolled back ([revert](Self::revert)) so that they are never written back to storage and are no
459/// longer reflected even through the data source object which was used to make the changes.
460pub trait Transaction: Send + Sync + Sized {
461    fn commit(self) -> impl Future<Output = anyhow::Result<()>> + Send;
462    fn revert(self) -> impl Future + Send;
463}