zebra_state/service/finalized_state/disk_format/
scan.rs

1//! Serialization formats for the shielded scanner results database.
2//!
3//! Due to Rust's orphan rule, these serializations must be implemented in this crate.
4//!
5//! # Correctness
6//!
7//! `zebra_scan::Storage::database_format_version_in_code()` must be incremented
8//! each time the database format (column, serialization, etc) changes.
9
10use std::fmt;
11
12use hex::{FromHex, ToHex};
13use zebra_chain::{block::Height, transaction};
14
15use crate::{FromDisk, IntoDisk, TransactionLocation};
16
17use super::block::TRANSACTION_LOCATION_DISK_BYTES;
18
19#[cfg(any(test, feature = "proptest-impl"))]
20use proptest_derive::Arbitrary;
21
22#[cfg(test)]
23mod tests;
24
25/// The type used in Zebra to store Sapling scanning keys.
26/// It can represent a full viewing key or an individual viewing key.
27pub type SaplingScanningKey = String;
28
29/// Stores a scanning result.
30///
31/// Currently contains a TXID in "display order", which is big-endian byte order following the u256
32/// convention set by Bitcoin and zcashd.
33#[derive(Copy, Clone, Eq, PartialEq)]
34#[cfg_attr(
35    any(test, feature = "proptest-impl"),
36    derive(Arbitrary, Default, serde::Serialize, serde::Deserialize)
37)]
38pub struct SaplingScannedResult(
39    #[cfg_attr(any(test, feature = "proptest-impl"), serde(with = "hex"))] [u8; 32],
40);
41
42impl fmt::Display for SaplingScannedResult {
43    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
44        f.write_str(&self.encode_hex::<String>())
45    }
46}
47
48impl fmt::Debug for SaplingScannedResult {
49    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
50        f.debug_tuple("SaplingScannedResult")
51            .field(&self.encode_hex::<String>())
52            .finish()
53    }
54}
55
56impl ToHex for &SaplingScannedResult {
57    fn encode_hex<T: FromIterator<char>>(&self) -> T {
58        self.bytes_in_display_order().encode_hex()
59    }
60
61    fn encode_hex_upper<T: FromIterator<char>>(&self) -> T {
62        self.bytes_in_display_order().encode_hex_upper()
63    }
64}
65
66impl ToHex for SaplingScannedResult {
67    fn encode_hex<T: FromIterator<char>>(&self) -> T {
68        (&self).encode_hex()
69    }
70
71    fn encode_hex_upper<T: FromIterator<char>>(&self) -> T {
72        (&self).encode_hex_upper()
73    }
74}
75
76impl FromHex for SaplingScannedResult {
77    type Error = <[u8; 32] as FromHex>::Error;
78
79    fn from_hex<T: AsRef<[u8]>>(hex: T) -> Result<Self, Self::Error> {
80        let result = <[u8; 32]>::from_hex(hex)?;
81
82        Ok(Self::from_bytes_in_display_order(result))
83    }
84}
85
86impl From<SaplingScannedResult> for transaction::Hash {
87    fn from(scanned_result: SaplingScannedResult) -> Self {
88        transaction::Hash::from_bytes_in_display_order(&scanned_result.0)
89    }
90}
91
92impl From<transaction::Hash> for SaplingScannedResult {
93    fn from(hash: transaction::Hash) -> Self {
94        SaplingScannedResult(hash.bytes_in_display_order())
95    }
96}
97
98impl SaplingScannedResult {
99    /// Creates a `SaplingScannedResult` from bytes in display order.
100    pub fn from_bytes_in_display_order(bytes: [u8; 32]) -> Self {
101        Self(bytes)
102    }
103
104    /// Returns the inner bytes in display order.
105    pub fn bytes_in_display_order(&self) -> [u8; 32] {
106        self.0
107    }
108}
109
110/// A database column family entry for a block scanned with a Sapling vieweing key.
111#[derive(Clone, Debug, Eq, PartialEq)]
112#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary, Default))]
113pub struct SaplingScannedDatabaseEntry {
114    /// The database column family key. Must be unique for each scanning key and scanned block.
115    pub index: SaplingScannedDatabaseIndex,
116
117    /// The database column family value.
118    pub value: Option<SaplingScannedResult>,
119}
120
121/// A database column family key for a block scanned with a Sapling vieweing key.
122#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
123#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary, Default))]
124pub struct SaplingScannedDatabaseIndex {
125    /// The Sapling viewing key used to scan the block.
126    pub sapling_key: SaplingScanningKey,
127
128    /// The transaction location: block height and transaction index.
129    pub tx_loc: TransactionLocation,
130}
131
132impl SaplingScannedDatabaseIndex {
133    /// The minimum value of a sapling scanned database index.
134    ///
135    /// This value is guarateed to be the minimum, and not correspond to a valid key.
136    //
137    // Note: to calculate the maximum value, we need a key length.
138    pub const fn min() -> Self {
139        Self {
140            // The empty string is the minimum value in RocksDB lexicographic order.
141            sapling_key: String::new(),
142            tx_loc: TransactionLocation::MIN,
143        }
144    }
145
146    /// The minimum value of a sapling scanned database index for `sapling_key`.
147    ///
148    /// This value does not correspond to a valid entry.
149    /// (The genesis coinbase transaction does not have shielded transfers.)
150    pub fn min_for_key(sapling_key: &SaplingScanningKey) -> Self {
151        Self {
152            sapling_key: sapling_key.clone(),
153            tx_loc: TransactionLocation::MIN,
154        }
155    }
156
157    /// The maximum value of a sapling scanned database index for `sapling_key`.
158    ///
159    /// This value may correspond to a valid entry, but it won't be mined for many decades.
160    pub fn max_for_key(sapling_key: &SaplingScanningKey) -> Self {
161        Self {
162            sapling_key: sapling_key.clone(),
163            tx_loc: TransactionLocation::MAX,
164        }
165    }
166
167    /// The minimum value of a sapling scanned database index for `sapling_key` and `height`.
168    ///
169    /// This value can be a valid entry for shielded coinbase.
170    pub fn min_for_key_and_height(sapling_key: &SaplingScanningKey, height: Height) -> Self {
171        Self {
172            sapling_key: sapling_key.clone(),
173            tx_loc: TransactionLocation::min_for_height(height),
174        }
175    }
176
177    /// The maximum value of a sapling scanned database index for `sapling_key` and `height`.
178    ///
179    /// This value can be a valid entry, but it won't fit in a 2MB block.
180    pub fn max_for_key_and_height(sapling_key: &SaplingScanningKey, height: Height) -> Self {
181        Self {
182            sapling_key: sapling_key.clone(),
183            tx_loc: TransactionLocation::max_for_height(height),
184        }
185    }
186}
187
188impl IntoDisk for SaplingScanningKey {
189    type Bytes = Vec<u8>;
190
191    fn as_bytes(&self) -> Self::Bytes {
192        SaplingScanningKey::as_bytes(self).to_vec()
193    }
194}
195
196impl FromDisk for SaplingScanningKey {
197    fn from_bytes(bytes: impl AsRef<[u8]>) -> Self {
198        SaplingScanningKey::from_utf8(bytes.as_ref().to_vec())
199            .expect("only valid UTF-8 strings are written to the database")
200    }
201}
202
203impl IntoDisk for SaplingScannedDatabaseIndex {
204    type Bytes = Vec<u8>;
205
206    fn as_bytes(&self) -> Self::Bytes {
207        let mut bytes = Vec::new();
208
209        bytes.extend(self.sapling_key.as_bytes());
210        bytes.extend(self.tx_loc.as_bytes());
211
212        bytes
213    }
214}
215
216impl FromDisk for SaplingScannedDatabaseIndex {
217    fn from_bytes(bytes: impl AsRef<[u8]>) -> Self {
218        let bytes = bytes.as_ref();
219
220        let (sapling_key, tx_loc) = bytes.split_at(bytes.len() - TRANSACTION_LOCATION_DISK_BYTES);
221
222        Self {
223            sapling_key: SaplingScanningKey::from_bytes(sapling_key),
224            tx_loc: TransactionLocation::from_bytes(tx_loc),
225        }
226    }
227}
228
229// We can't implement IntoDisk or FromDisk for SaplingScannedResult,
230// because the format is actually Option<SaplingScannedResult>.
231
232impl IntoDisk for Option<SaplingScannedResult> {
233    type Bytes = Vec<u8>;
234
235    fn as_bytes(&self) -> Self::Bytes {
236        let mut bytes = Vec::new();
237
238        if let Some(result) = self.as_ref() {
239            bytes.extend(result.bytes_in_display_order());
240        }
241
242        bytes
243    }
244}
245
246impl FromDisk for Option<SaplingScannedResult> {
247    #[allow(clippy::unwrap_in_result)]
248    fn from_bytes(bytes: impl AsRef<[u8]>) -> Self {
249        let bytes = bytes.as_ref();
250
251        if bytes.is_empty() {
252            None
253        } else {
254            Some(SaplingScannedResult::from_bytes_in_display_order(
255                bytes
256                    .try_into()
257                    .expect("unexpected incorrect SaplingScannedResult data length"),
258            ))
259        }
260    }
261}