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.
910use std::fmt;
1112use hex::{FromHex, ToHex};
13use zebra_chain::{block::Height, transaction};
1415use crate::{FromDisk, IntoDisk, TransactionLocation};
1617use super::block::TRANSACTION_LOCATION_DISK_BYTES;
1819#[cfg(any(test, feature = "proptest-impl"))]
20use proptest_derive::Arbitrary;
2122#[cfg(test)]
23mod tests;
2425/// 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;
2829/// 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);
4142impl fmt::Display for SaplingScannedResult {
43fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
44 f.write_str(&self.encode_hex::<String>())
45 }
46}
4748impl fmt::Debug for SaplingScannedResult {
49fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
50 f.debug_tuple("SaplingScannedResult")
51 .field(&self.encode_hex::<String>())
52 .finish()
53 }
54}
5556impl ToHex for &SaplingScannedResult {
57fn encode_hex<T: FromIterator<char>>(&self) -> T {
58self.bytes_in_display_order().encode_hex()
59 }
6061fn encode_hex_upper<T: FromIterator<char>>(&self) -> T {
62self.bytes_in_display_order().encode_hex_upper()
63 }
64}
6566impl ToHex for SaplingScannedResult {
67fn encode_hex<T: FromIterator<char>>(&self) -> T {
68 (&self).encode_hex()
69 }
7071fn encode_hex_upper<T: FromIterator<char>>(&self) -> T {
72 (&self).encode_hex_upper()
73 }
74}
7576impl FromHex for SaplingScannedResult {
77type Error = <[u8; 32] as FromHex>::Error;
7879fn from_hex<T: AsRef<[u8]>>(hex: T) -> Result<Self, Self::Error> {
80let result = <[u8; 32]>::from_hex(hex)?;
8182Ok(Self::from_bytes_in_display_order(result))
83 }
84}
8586impl From<SaplingScannedResult> for transaction::Hash {
87fn from(scanned_result: SaplingScannedResult) -> Self {
88 transaction::Hash::from_bytes_in_display_order(&scanned_result.0)
89 }
90}
9192impl From<transaction::Hash> for SaplingScannedResult {
93fn from(hash: transaction::Hash) -> Self {
94 SaplingScannedResult(hash.bytes_in_display_order())
95 }
96}
9798impl SaplingScannedResult {
99/// Creates a `SaplingScannedResult` from bytes in display order.
100pub fn from_bytes_in_display_order(bytes: [u8; 32]) -> Self {
101Self(bytes)
102 }
103104/// Returns the inner bytes in display order.
105pub fn bytes_in_display_order(&self) -> [u8; 32] {
106self.0
107}
108}
109110/// 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.
115pub index: SaplingScannedDatabaseIndex,
116117/// The database column family value.
118pub value: Option<SaplingScannedResult>,
119}
120121/// 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.
126pub sapling_key: SaplingScanningKey,
127128/// The transaction location: block height and transaction index.
129pub tx_loc: TransactionLocation,
130}
131132impl 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.
138pub const fn min() -> Self {
139Self {
140// The empty string is the minimum value in RocksDB lexicographic order.
141sapling_key: String::new(),
142 tx_loc: TransactionLocation::MIN,
143 }
144 }
145146/// 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.)
150pub fn min_for_key(sapling_key: &SaplingScanningKey) -> Self {
151Self {
152 sapling_key: sapling_key.clone(),
153 tx_loc: TransactionLocation::MIN,
154 }
155 }
156157/// 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.
160pub fn max_for_key(sapling_key: &SaplingScanningKey) -> Self {
161Self {
162 sapling_key: sapling_key.clone(),
163 tx_loc: TransactionLocation::MAX,
164 }
165 }
166167/// 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.
170pub fn min_for_key_and_height(sapling_key: &SaplingScanningKey, height: Height) -> Self {
171Self {
172 sapling_key: sapling_key.clone(),
173 tx_loc: TransactionLocation::min_for_height(height),
174 }
175 }
176177/// 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.
180pub fn max_for_key_and_height(sapling_key: &SaplingScanningKey, height: Height) -> Self {
181Self {
182 sapling_key: sapling_key.clone(),
183 tx_loc: TransactionLocation::max_for_height(height),
184 }
185 }
186}
187188impl IntoDisk for SaplingScanningKey {
189type Bytes = Vec<u8>;
190191fn as_bytes(&self) -> Self::Bytes {
192 SaplingScanningKey::as_bytes(self).to_vec()
193 }
194}
195196impl FromDisk for SaplingScanningKey {
197fn 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}
202203impl IntoDisk for SaplingScannedDatabaseIndex {
204type Bytes = Vec<u8>;
205206fn as_bytes(&self) -> Self::Bytes {
207let mut bytes = Vec::new();
208209 bytes.extend(self.sapling_key.as_bytes());
210 bytes.extend(self.tx_loc.as_bytes());
211212 bytes
213 }
214}
215216impl FromDisk for SaplingScannedDatabaseIndex {
217fn from_bytes(bytes: impl AsRef<[u8]>) -> Self {
218let bytes = bytes.as_ref();
219220let (sapling_key, tx_loc) = bytes.split_at(bytes.len() - TRANSACTION_LOCATION_DISK_BYTES);
221222Self {
223 sapling_key: SaplingScanningKey::from_bytes(sapling_key),
224 tx_loc: TransactionLocation::from_bytes(tx_loc),
225 }
226 }
227}
228229// We can't implement IntoDisk or FromDisk for SaplingScannedResult,
230// because the format is actually Option<SaplingScannedResult>.
231232impl IntoDisk for Option<SaplingScannedResult> {
233type Bytes = Vec<u8>;
234235fn as_bytes(&self) -> Self::Bytes {
236let mut bytes = Vec::new();
237238if let Some(result) = self.as_ref() {
239 bytes.extend(result.bytes_in_display_order());
240 }
241242 bytes
243 }
244}
245246impl FromDisk for Option<SaplingScannedResult> {
247#[allow(clippy::unwrap_in_result)]
248fn from_bytes(bytes: impl AsRef<[u8]>) -> Self {
249let bytes = bytes.as_ref();
250251if bytes.is_empty() {
252None
253} else {
254Some(SaplingScannedResult::from_bytes_in_display_order(
255 bytes
256 .try_into()
257 .expect("unexpected incorrect SaplingScannedResult data length"),
258 ))
259 }
260 }
261}