zebra_state/service/finalized_state/disk_format/
block.rs

1//! Block and transaction serialization formats for finalized data.
2//!
3//! # Correctness
4//!
5//! [`crate::constants::state_database_format_version_in_code()`] must be incremented
6//! each time the database format (column, serialization, etc) changes.
7
8use zebra_chain::{
9    block::{self, Height},
10    serialization::{ZcashDeserializeInto, ZcashSerialize},
11    transaction::{self, Transaction},
12};
13
14use crate::service::finalized_state::disk_format::{
15    expand_zero_be_bytes, truncate_zero_be_bytes, FromDisk, IntoDisk,
16};
17
18#[cfg(any(test, feature = "proptest-impl"))]
19use proptest_derive::Arbitrary;
20#[cfg(any(test, feature = "proptest-impl"))]
21use serde::{Deserialize, Serialize};
22
23/// The maximum value of an on-disk serialized [`Height`].
24///
25/// This allows us to store [`OutputLocation`](crate::OutputLocation)s in
26/// 8 bytes, which makes database searches more efficient.
27///
28/// # Consensus
29///
30/// This maximum height supports on-disk storage of blocks until around 2050.
31///
32/// Since Zebra only stores fully verified blocks on disk, blocks with heights
33/// larger than this height are rejected before reaching the database.
34/// (It would take decades to generate a valid chain this high.)
35pub const MAX_ON_DISK_HEIGHT: Height = Height((1 << (HEIGHT_DISK_BYTES * 8)) - 1);
36
37/// [`Height`]s are stored as 3 bytes on disk.
38///
39/// This reduces database size and increases lookup performance.
40pub const HEIGHT_DISK_BYTES: usize = 3;
41
42/// [`TransactionIndex`]es are stored as 2 bytes on disk.
43///
44/// This reduces database size and increases lookup performance.
45pub const TX_INDEX_DISK_BYTES: usize = 2;
46
47/// [`TransactionLocation`]s are stored as a 3 byte height and a 2 byte transaction index.
48///
49/// This reduces database size and increases lookup performance.
50pub const TRANSACTION_LOCATION_DISK_BYTES: usize = HEIGHT_DISK_BYTES + TX_INDEX_DISK_BYTES;
51
52// Block and transaction types
53
54/// A transaction's index in its block.
55///
56/// # Consensus
57///
58/// A 2-byte index supports on-disk storage of transactions in blocks up to ~5 MB.
59/// (The current maximum block size is 2 MB.)
60///
61/// Since Zebra only stores fully verified blocks on disk,
62/// blocks larger than this size are rejected before reaching the database.
63///
64/// (The maximum transaction count is tested by the large generated block serialization tests.)
65#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
66#[cfg_attr(
67    any(test, feature = "proptest-impl"),
68    derive(Arbitrary, Default, Serialize, Deserialize)
69)]
70pub struct TransactionIndex(pub(super) u16);
71
72impl TransactionIndex {
73    /// Creates a transaction index from the inner type.
74    pub fn from_index(transaction_index: u16) -> TransactionIndex {
75        TransactionIndex(transaction_index)
76    }
77
78    /// Returns this index as the inner type.
79    pub fn index(&self) -> u16 {
80        self.0
81    }
82
83    /// Creates a transaction index from a `usize`.
84    pub fn from_usize(transaction_index: usize) -> TransactionIndex {
85        TransactionIndex(
86            transaction_index
87                .try_into()
88                .expect("the maximum valid index fits in the inner type"),
89        )
90    }
91
92    /// Returns this index as a `usize`.
93    pub fn as_usize(&self) -> usize {
94        self.0.into()
95    }
96
97    /// Creates a transaction index from a `u64`.
98    pub fn from_u64(transaction_index: u64) -> TransactionIndex {
99        TransactionIndex(
100            transaction_index
101                .try_into()
102                .expect("the maximum valid index fits in the inner type"),
103        )
104    }
105
106    /// Returns this index as a `u64`.
107    #[allow(dead_code)]
108    pub fn as_u64(&self) -> u64 {
109        self.0.into()
110    }
111
112    /// The minimum value of a transaction index.
113    ///
114    /// This value corresponds to the coinbase transaction.
115    pub const MIN: Self = Self(u16::MIN);
116
117    /// The maximum value of a transaction index.
118    ///
119    /// This value corresponds to the highest possible transaction index.
120    pub const MAX: Self = Self(u16::MAX);
121}
122
123/// A transaction's location in the chain, by block height and transaction index.
124///
125/// This provides a chain-order list of transactions.
126#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
127#[cfg_attr(
128    any(test, feature = "proptest-impl"),
129    derive(Arbitrary, Default, Serialize, Deserialize)
130)]
131pub struct TransactionLocation {
132    /// The block height of the transaction.
133    pub height: Height,
134
135    /// The index of the transaction in its block.
136    pub index: TransactionIndex,
137}
138
139impl TransactionLocation {
140    /// Creates a transaction location from a block height and transaction index.
141    pub fn from_parts(height: Height, index: TransactionIndex) -> TransactionLocation {
142        TransactionLocation { height, index }
143    }
144
145    /// Creates a transaction location from a block height and transaction index.
146    pub fn from_index(height: Height, transaction_index: u16) -> TransactionLocation {
147        TransactionLocation {
148            height,
149            index: TransactionIndex::from_index(transaction_index),
150        }
151    }
152
153    /// Creates a transaction location from a block height and `usize` transaction index.
154    pub fn from_usize(height: Height, transaction_index: usize) -> TransactionLocation {
155        TransactionLocation {
156            height,
157            index: TransactionIndex::from_usize(transaction_index),
158        }
159    }
160
161    /// Creates a transaction location from a block height and `u64` transaction index.
162    pub fn from_u64(height: Height, transaction_index: u64) -> TransactionLocation {
163        TransactionLocation {
164            height,
165            index: TransactionIndex::from_u64(transaction_index),
166        }
167    }
168
169    /// The minimum value of a transaction location.
170    ///
171    /// This value corresponds to the genesis coinbase transaction.
172    pub const MIN: Self = Self {
173        height: Height::MIN,
174        index: TransactionIndex::MIN,
175    };
176
177    /// The maximum value of a transaction location.
178    ///
179    /// This value corresponds to the last transaction in the highest possible block.
180    pub const MAX: Self = Self {
181        height: Height::MAX,
182        index: TransactionIndex::MAX,
183    };
184
185    /// The minimum value of a transaction location for `height`.
186    ///
187    /// This value is the coinbase transaction.
188    pub const fn min_for_height(height: Height) -> Self {
189        Self {
190            height,
191            index: TransactionIndex::MIN,
192        }
193    }
194
195    /// The maximum value of a transaction location for `height`.
196    ///
197    /// This value can be a valid entry, but it won't fit in a 2MB block.
198    pub const fn max_for_height(height: Height) -> Self {
199        Self {
200            height,
201            index: TransactionIndex::MAX,
202        }
203    }
204}
205
206// Block and transaction trait impls
207
208impl IntoDisk for block::Header {
209    type Bytes = Vec<u8>;
210
211    fn as_bytes(&self) -> Self::Bytes {
212        self.zcash_serialize_to_vec()
213            .expect("serialization to vec doesn't fail")
214    }
215}
216
217impl FromDisk for block::Header {
218    fn from_bytes(bytes: impl AsRef<[u8]>) -> Self {
219        bytes
220            .as_ref()
221            .zcash_deserialize_into()
222            .expect("deserialization format should match the serialization format used by IntoDisk")
223    }
224}
225
226impl IntoDisk for Height {
227    /// Consensus: see the note at [`MAX_ON_DISK_HEIGHT`].
228    type Bytes = [u8; HEIGHT_DISK_BYTES];
229
230    fn as_bytes(&self) -> Self::Bytes {
231        let mem_bytes = self.0.to_be_bytes();
232
233        let disk_bytes = truncate_zero_be_bytes(&mem_bytes, HEIGHT_DISK_BYTES);
234
235        match disk_bytes {
236            Some(b) => b.try_into().unwrap(),
237
238            // # Security
239            //
240            // The RPC method or state query was given a block height that is ridiculously high.
241            // But to save space in database indexes, we don't support heights 2^24 and above.
242            //
243            // Instead we return the biggest valid database Height to the lookup code.
244            // So RPC methods and queued block checks will return an error or None.
245            //
246            // At the current block production rate, these heights can't be inserted into the
247            // database until at least 2050. (Blocks are verified in strict height order.)
248            None => truncate_zero_be_bytes(&MAX_ON_DISK_HEIGHT.0.to_be_bytes(), HEIGHT_DISK_BYTES)
249                .expect("max on disk height is valid")
250                .try_into()
251                .unwrap(),
252        }
253    }
254}
255
256impl FromDisk for Height {
257    fn from_bytes(disk_bytes: impl AsRef<[u8]>) -> Self {
258        const MEM_LEN: usize = size_of::<u32>();
259
260        let mem_bytes = expand_zero_be_bytes::<MEM_LEN>(disk_bytes.as_ref());
261        Height(u32::from_be_bytes(mem_bytes))
262    }
263}
264
265impl IntoDisk for block::Hash {
266    type Bytes = [u8; 32];
267
268    fn as_bytes(&self) -> Self::Bytes {
269        self.0
270    }
271}
272
273impl FromDisk for block::Hash {
274    fn from_bytes(bytes: impl AsRef<[u8]>) -> Self {
275        let array = bytes.as_ref().try_into().unwrap();
276        Self(array)
277    }
278}
279
280// Transaction trait impls
281
282impl IntoDisk for Transaction {
283    type Bytes = Vec<u8>;
284
285    fn as_bytes(&self) -> Self::Bytes {
286        self.zcash_serialize_to_vec()
287            .expect("serialization to vec doesn't fail")
288    }
289}
290
291impl FromDisk for Transaction {
292    fn from_bytes(bytes: impl AsRef<[u8]>) -> Self {
293        let bytes = bytes.as_ref();
294
295        // TODO: skip cryptography verification during transaction deserialization from storage,
296        //       or do it in a rayon thread (ideally in parallel with other transactions)
297        bytes
298            .as_ref()
299            .zcash_deserialize_into()
300            .expect("deserialization format should match the serialization format used by IntoDisk")
301    }
302}
303
304/// TransactionIndex is only serialized as part of TransactionLocation
305impl IntoDisk for TransactionIndex {
306    type Bytes = [u8; TX_INDEX_DISK_BYTES];
307
308    fn as_bytes(&self) -> Self::Bytes {
309        self.index().to_be_bytes()
310    }
311}
312
313impl FromDisk for TransactionIndex {
314    fn from_bytes(disk_bytes: impl AsRef<[u8]>) -> Self {
315        let disk_bytes = disk_bytes.as_ref().try_into().unwrap();
316
317        TransactionIndex::from_index(u16::from_be_bytes(disk_bytes))
318    }
319}
320
321impl IntoDisk for TransactionLocation {
322    type Bytes = [u8; TRANSACTION_LOCATION_DISK_BYTES];
323
324    fn as_bytes(&self) -> Self::Bytes {
325        let height_bytes = self.height.as_bytes().to_vec();
326        let index_bytes = self.index.as_bytes().to_vec();
327
328        [height_bytes, index_bytes].concat().try_into().unwrap()
329    }
330}
331
332impl FromDisk for Option<TransactionLocation> {
333    fn from_bytes(disk_bytes: impl AsRef<[u8]>) -> Self {
334        if disk_bytes.as_ref().len() == TRANSACTION_LOCATION_DISK_BYTES {
335            Some(TransactionLocation::from_bytes(disk_bytes))
336        } else {
337            None
338        }
339    }
340}
341
342impl FromDisk for TransactionLocation {
343    fn from_bytes(disk_bytes: impl AsRef<[u8]>) -> Self {
344        let (height_bytes, index_bytes) = disk_bytes.as_ref().split_at(HEIGHT_DISK_BYTES);
345
346        let height = Height::from_bytes(height_bytes);
347        let index = TransactionIndex::from_bytes(index_bytes);
348
349        TransactionLocation { height, index }
350    }
351}
352
353impl IntoDisk for transaction::Hash {
354    type Bytes = [u8; 32];
355
356    fn as_bytes(&self) -> Self::Bytes {
357        self.0
358    }
359}
360
361impl FromDisk for transaction::Hash {
362    fn from_bytes(disk_bytes: impl AsRef<[u8]>) -> Self {
363        transaction::Hash(disk_bytes.as_ref().try_into().unwrap())
364    }
365}