Skip to main content

signet_cold/
traits.rs

1//! Core trait definition for cold storage backends.
2//!
3//! The [`ColdStorage`] trait defines the interface that all cold storage
4//! backends must implement. Backends are responsible for data organization,
5//! indexing, and keying - the trait is agnostic to these implementation details.
6
7use alloy::{consensus::Header, primitives::BlockNumber};
8use signet_storage_types::{
9    DbSignetEvent, DbZenithHeader, ExecutedBlock, Receipt, TransactionSigned,
10};
11use std::future::Future;
12
13use super::{
14    ColdResult, Confirmed, HeaderSpecifier, ReceiptSpecifier, SignetEventsSpecifier,
15    TransactionSpecifier, ZenithHeaderSpecifier,
16};
17
18/// Data for appending a complete block to cold storage.
19#[derive(Debug, Clone)]
20pub struct BlockData {
21    /// The block header.
22    pub header: Header,
23    /// The transactions in the block.
24    pub transactions: Vec<TransactionSigned>,
25    /// The receipts for the transactions.
26    pub receipts: Vec<Receipt>,
27    /// The signet events in the block.
28    pub signet_events: Vec<DbSignetEvent>,
29    /// The zenith header for the block, if present.
30    pub zenith_header: Option<DbZenithHeader>,
31}
32
33impl BlockData {
34    /// Create new block data.
35    pub const fn new(
36        header: Header,
37        transactions: Vec<TransactionSigned>,
38        receipts: Vec<Receipt>,
39        signet_events: Vec<DbSignetEvent>,
40        zenith_header: Option<DbZenithHeader>,
41    ) -> Self {
42        Self { header, transactions, receipts, signet_events, zenith_header }
43    }
44
45    /// Get the block number of the block.
46    pub const fn block_number(&self) -> BlockNumber {
47        self.header.number
48    }
49}
50
51/// All data needed to build a complete RPC receipt response.
52///
53/// Bundles a [`Confirmed`] receipt with its transaction, block header,
54/// and the prior cumulative gas (needed to compute per-tx `gas_used`).
55#[derive(Debug, Clone)]
56pub struct ReceiptContext {
57    /// The block header.
58    pub header: Header,
59    /// The transaction that produced this receipt.
60    pub transaction: TransactionSigned,
61    /// The receipt with block confirmation metadata.
62    pub receipt: Confirmed<Receipt>,
63    /// Cumulative gas used by all preceding transactions in the block.
64    /// Zero for the first transaction.
65    pub prior_cumulative_gas: u64,
66}
67
68impl ReceiptContext {
69    /// Create a new receipt context.
70    pub const fn new(
71        header: Header,
72        transaction: TransactionSigned,
73        receipt: Confirmed<Receipt>,
74        prior_cumulative_gas: u64,
75    ) -> Self {
76        Self { header, transaction, receipt, prior_cumulative_gas }
77    }
78}
79
80impl From<ExecutedBlock> for BlockData {
81    fn from(block: ExecutedBlock) -> Self {
82        Self::new(
83            block.header.into_inner(),
84            block.transactions,
85            block.receipts,
86            block.signet_events,
87            block.zenith_header,
88        )
89    }
90}
91
92/// Unified cold storage backend trait.
93///
94/// Backend is responsible for all data organization, indexing, and keying.
95/// The trait is agnostic to how the backend stores or indexes data.
96///
97/// All methods are async and return futures that are `Send`.
98///
99/// # Implementation Guide
100///
101/// Implementers must ensure:
102///
103/// - **Append-only ordering**: `append_block` must enforce monotonically
104///   increasing block numbers. Attempting to append a block with a number <=
105///   the current latest should return an error.
106///
107/// - **Atomic truncation**: `truncate_above` must remove all data for blocks
108///   N+1 and higher atomically. Partial truncation is not acceptable.
109///
110/// - **Index maintenance**: Hash-based lookups (e.g., header by hash,
111///   transaction by hash) require the implementation to maintain appropriate
112///   indexes. These indexes must be updated during `append_block` and cleaned
113///   during `truncate_above`.
114///
115/// - **Consistent reads**: Read operations should return consistent snapshots.
116///   A read started before a write completes should not see partial data from
117///   that write.
118///
119pub trait ColdStorage: Send + Sync + 'static {
120    // --- Headers ---
121
122    /// Get a header by specifier.
123    fn get_header(
124        &self,
125        spec: HeaderSpecifier,
126    ) -> impl Future<Output = ColdResult<Option<Header>>> + Send;
127
128    /// Get multiple headers by specifiers.
129    fn get_headers(
130        &self,
131        specs: Vec<HeaderSpecifier>,
132    ) -> impl Future<Output = ColdResult<Vec<Option<Header>>>> + Send;
133
134    // --- Transactions ---
135
136    /// Get a transaction by specifier, with block confirmation metadata.
137    fn get_transaction(
138        &self,
139        spec: TransactionSpecifier,
140    ) -> impl Future<Output = ColdResult<Option<Confirmed<TransactionSigned>>>> + Send;
141
142    /// Get all transactions in a block.
143    fn get_transactions_in_block(
144        &self,
145        block: BlockNumber,
146    ) -> impl Future<Output = ColdResult<Vec<TransactionSigned>>> + Send;
147
148    /// Get the number of transactions in a block.
149    fn get_transaction_count(
150        &self,
151        block: BlockNumber,
152    ) -> impl Future<Output = ColdResult<u64>> + Send;
153
154    // --- Receipts ---
155
156    /// Get a receipt by specifier, with block confirmation metadata.
157    fn get_receipt(
158        &self,
159        spec: ReceiptSpecifier,
160    ) -> impl Future<Output = ColdResult<Option<Confirmed<Receipt>>>> + Send;
161
162    /// Get all receipts in a block.
163    fn get_receipts_in_block(
164        &self,
165        block: BlockNumber,
166    ) -> impl Future<Output = ColdResult<Vec<Receipt>>> + Send;
167
168    // --- SignetEvents ---
169
170    /// Get signet events by specifier.
171    fn get_signet_events(
172        &self,
173        spec: SignetEventsSpecifier,
174    ) -> impl Future<Output = ColdResult<Vec<DbSignetEvent>>> + Send;
175
176    // --- ZenithHeaders ---
177
178    /// Get a zenith header by specifier.
179    fn get_zenith_header(
180        &self,
181        spec: ZenithHeaderSpecifier,
182    ) -> impl Future<Output = ColdResult<Option<DbZenithHeader>>> + Send;
183
184    /// Get multiple zenith headers by specifier.
185    fn get_zenith_headers(
186        &self,
187        spec: ZenithHeaderSpecifier,
188    ) -> impl Future<Output = ColdResult<Vec<DbZenithHeader>>> + Send;
189
190    // --- Metadata ---
191
192    /// Get the latest block number in storage.
193    fn get_latest_block(&self) -> impl Future<Output = ColdResult<Option<BlockNumber>>> + Send;
194
195    // --- Composite queries ---
196
197    /// Get a receipt with all context needed for RPC responses.
198    ///
199    /// Returns the receipt, its transaction, the block header, confirmation
200    /// metadata, and the cumulative gas used by preceding transactions.
201    /// Returns `None` if the receipt does not exist.
202    ///
203    /// The default implementation composes existing trait methods. Backends
204    /// that can serve this more efficiently (e.g., in a single transaction)
205    /// should override.
206    fn get_receipt_with_context(
207        &self,
208        spec: ReceiptSpecifier,
209    ) -> impl Future<Output = ColdResult<Option<ReceiptContext>>> + Send {
210        async move {
211            let Some(receipt) = self.get_receipt(spec).await? else {
212                return Ok(None);
213            };
214            let block = receipt.meta().block_number();
215            let index = receipt.meta().transaction_index();
216
217            let Some(header) = self.get_header(HeaderSpecifier::Number(block)).await? else {
218                return Ok(None);
219            };
220            let Some(tx) =
221                self.get_transaction(TransactionSpecifier::BlockAndIndex { block, index }).await?
222            else {
223                return Ok(None);
224            };
225
226            let prior_cumulative_gas = if index > 0 {
227                self.get_receipt(ReceiptSpecifier::BlockAndIndex { block, index: index - 1 })
228                    .await?
229                    .map(|r| r.into_inner().inner.cumulative_gas_used)
230                    .unwrap_or(0)
231            } else {
232                0
233            };
234
235            Ok(Some(ReceiptContext::new(header, tx.into_inner(), receipt, prior_cumulative_gas)))
236        }
237    }
238
239    // --- Write operations ---
240
241    /// Append a single block to cold storage.
242    fn append_block(&self, data: BlockData) -> impl Future<Output = ColdResult<()>> + Send;
243
244    /// Append multiple blocks to cold storage.
245    fn append_blocks(&self, data: Vec<BlockData>) -> impl Future<Output = ColdResult<()>> + Send;
246
247    /// Truncate all data above the given block number (exclusive).
248    ///
249    /// This removes block N+1 and higher from all tables. Used for reorg handling.
250    fn truncate_above(&self, block: BlockNumber) -> impl Future<Output = ColdResult<()>> + Send;
251}