Skip to main content

zebra_state/service/finalized_state/disk_format/
chain.rs

1//! Chain data 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 std::collections::BTreeMap;
9
10use bincode::Options;
11use serde_big_array::BigArray;
12
13use zebra_chain::{
14    amount::NonNegative,
15    block::Height,
16    block_info::BlockInfo,
17    history_tree::{HistoryTreeError, NonEmptyHistoryTree},
18    parameters::{Network, NetworkKind},
19    primitives::zcash_history,
20    value_balance::ValueBalance,
21};
22
23use crate::service::finalized_state::disk_format::{FromDisk, IntoDisk};
24
25impl IntoDisk for ValueBalance<NonNegative> {
26    type Bytes = [u8; 48];
27
28    fn as_bytes(&self) -> Self::Bytes {
29        self.to_bytes()
30    }
31}
32
33impl FromDisk for ValueBalance<NonNegative> {
34    fn from_bytes(bytes: impl AsRef<[u8]>) -> Self {
35        ValueBalance::from_bytes(bytes.as_ref()).expect("ValueBalance should be parsable")
36    }
37}
38
39// The following implementations for history trees use `serde` and
40// `bincode`. `serde` serializations depend on the inner structure of the type.
41// They should not be used in new code. (This is an issue for any derived serialization format.)
42//
43// We explicitly use `bincode::DefaultOptions`  to disallow trailing bytes; see
44// https://docs.rs/bincode/1.3.3/bincode/config/index.html#options-struct-vs-bincode-functions
45
46#[derive(serde::Serialize, serde::Deserialize)]
47pub struct HistoryTreeParts {
48    network_kind: NetworkKind,
49    size: u32,
50    peaks: BTreeMap<u32, zcash_history::Entry>,
51    current_height: Height,
52}
53
54impl HistoryTreeParts {
55    /// Converts [`HistoryTreeParts`] to a [`NonEmptyHistoryTree`].
56    pub(crate) fn with_network(
57        self,
58        network: &Network,
59    ) -> Result<NonEmptyHistoryTree, HistoryTreeError> {
60        assert_eq!(
61            self.network_kind,
62            network.kind(),
63            "history tree network kind should match current network"
64        );
65
66        NonEmptyHistoryTree::from_cache(network, self.size, self.peaks, self.current_height)
67    }
68}
69
70impl From<&NonEmptyHistoryTree> for HistoryTreeParts {
71    fn from(history_tree: &NonEmptyHistoryTree) -> Self {
72        HistoryTreeParts {
73            network_kind: history_tree.network().kind(),
74            size: history_tree.size(),
75            peaks: history_tree.peaks().clone(),
76            current_height: history_tree.current_height(),
77        }
78    }
79}
80
81impl IntoDisk for HistoryTreeParts {
82    type Bytes = Vec<u8>;
83
84    fn as_bytes(&self) -> Self::Bytes {
85        bincode::DefaultOptions::new()
86            .serialize(self)
87            .expect("serialization to vec doesn't fail")
88    }
89}
90
91/// The width of a history-tree [`zcash_history::Entry`] as serialized by database formats written
92/// before NU6.3 (Ironwood) widened `zcash_history::NodeData`. Equal to the pre-Ironwood
93/// `MAX_ENTRY_SIZE`: a `MAX_NODE_DATA_SIZE` of 244 bytes plus the 9-byte entry header.
94const LEGACY_MAX_ENTRY_SIZE: usize = 253;
95
96/// A mirror of [`HistoryTreeParts`] using the pre-NU6.3 [`zcash_history::Entry`] width.
97///
98/// Used to read history trees written by earlier database formats, whose entries are too narrow to
99/// parse at the current width. The field order must match [`HistoryTreeParts`] so the two share a
100/// bincode layout (bincode ignores field names and identifies fields by position).
101#[derive(serde::Serialize, serde::Deserialize)]
102struct LegacyHistoryTreeParts {
103    network_kind: NetworkKind,
104    size: u32,
105    peaks: BTreeMap<u32, LegacyEntry>,
106    current_height: Height,
107}
108
109/// A history-tree entry serialized at the pre-NU6.3 [`LEGACY_MAX_ENTRY_SIZE`] width.
110#[derive(serde::Serialize, serde::Deserialize)]
111struct LegacyEntry {
112    #[serde(with = "BigArray")]
113    inner: [u8; LEGACY_MAX_ENTRY_SIZE],
114}
115
116impl From<LegacyHistoryTreeParts> for HistoryTreeParts {
117    fn from(legacy: LegacyHistoryTreeParts) -> Self {
118        HistoryTreeParts {
119            network_kind: legacy.network_kind,
120            size: legacy.size,
121            peaks: legacy
122                .peaks
123                .into_iter()
124                .map(|(index, entry)| {
125                    (
126                        index,
127                        zcash_history::Entry::from_raw_bytes_padded(&entry.inner),
128                    )
129                })
130                .collect(),
131            current_height: legacy.current_height,
132        }
133    }
134}
135
136impl FromDisk for HistoryTreeParts {
137    fn from_bytes(bytes: impl AsRef<[u8]>) -> Self {
138        let bytes = bytes.as_ref();
139        let options = bincode::DefaultOptions::new();
140
141        // Try the current entry width first. Databases written before NU6.3 (Ironwood) widened
142        // `zcash_history::Entry` store narrower entries that fail to parse at the current width, so
143        // fall back to the legacy width and zero-pad each entry up to the current width. New data
144        // always parses at the current width, so it never reaches the fallback.
145        //
146        // The fallback must run on *any* current-width parse error, not just `UnexpectedEof`.
147        // Entries are fixed-size arrays with no length prefix, so parsing a multi-peak legacy record
148        // at the wider width reads later `BTreeMap` keys from the middle of an entry's bytes; bincode
149        // then fails with a varint/integer-range error (not `UnexpectedEof`) whenever a misread key
150        // byte is out of range. Gating on `UnexpectedEof` alone panicked on those records. A genuine
151        // current-format record only reaches the fallback if it is corrupt, in which case the legacy
152        // parse fails too and the error still surfaces.
153        options
154            .deserialize::<HistoryTreeParts>(bytes)
155            .or_else(|_| {
156                options
157                    .deserialize::<LegacyHistoryTreeParts>(bytes)
158                    .map(HistoryTreeParts::from)
159            })
160            .expect("deserialization format should match the serialization format used by IntoDisk")
161    }
162}
163
164impl IntoDisk for BlockInfo {
165    type Bytes = Vec<u8>;
166
167    fn as_bytes(&self) -> Self::Bytes {
168        self.value_pools()
169            .as_bytes()
170            .iter()
171            .copied()
172            .chain(self.size().to_le_bytes().iter().copied())
173            .collect()
174    }
175}
176
177impl FromDisk for BlockInfo {
178    fn from_bytes(bytes: impl AsRef<[u8]>) -> Self {
179        // Records are exactly 52 bytes from NU6.3 onward (48-byte value pool incl. the ironwood
180        // pool, plus the 4-byte block size) and exactly 44 bytes for records written by earlier
181        // Zebra versions (40-byte value pool plus 4-byte size). We discriminate the two layouts by
182        // length, and stay forward-compatible by reading the known prefix
183        // and ignoring any unexpected trailing bytes.
184        match bytes.as_ref().len() {
185            // NU6.3 onward (and any forward-compatible larger record): 48-byte pool + 4-byte size.
186            52.. => {
187                let value_pools = ValueBalance::<NonNegative>::from_bytes(&bytes.as_ref()[0..48])
188                    .expect("must work for 48 bytes");
189                let size =
190                    u32::from_le_bytes(bytes.as_ref()[48..52].try_into().expect("must be 4 bytes"));
191                BlockInfo::new(value_pools, size)
192            }
193            // Pre-NU6.3 records (exactly 44 bytes; the open range stays forward-compatible, and the
194            // 52.. arm above already took every NU6.3 record).
195            44.. => {
196                let value_pools = ValueBalance::<NonNegative>::from_bytes(&bytes.as_ref()[0..40])
197                    .expect("must work for 40 bytes");
198                let size =
199                    u32::from_le_bytes(bytes.as_ref()[40..44].try_into().expect("must be 4 bytes"));
200                BlockInfo::new(value_pools, size)
201            }
202            _ => panic!("invalid format"),
203        }
204    }
205}
206
207#[cfg(test)]
208mod tests;