zebra_state/service/finalized_state/disk_format/
transparent.rs

1//! Transparent transfer 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::{cmp::max, fmt::Debug};
9
10use serde::{Deserialize, Serialize};
11
12use zebra_chain::{
13    amount::{self, Amount, Constraint, NegativeAllowed, NonNegative},
14    block::Height,
15    parameters::NetworkKind,
16    serialization::{ZcashDeserializeInto, ZcashSerialize},
17    transparent::{self, Address::*},
18};
19
20use crate::service::finalized_state::disk_format::{
21    block::{TransactionIndex, TransactionLocation, TRANSACTION_LOCATION_DISK_BYTES},
22    expand_zero_be_bytes, truncate_zero_be_bytes, FromDisk, IntoDisk,
23};
24
25#[cfg(any(test, feature = "proptest-impl"))]
26use proptest_derive::Arbitrary;
27
28#[cfg(any(test, feature = "proptest-impl"))]
29mod arbitrary;
30
31/// Transparent balances are stored as an 8 byte integer on disk.
32pub const BALANCE_DISK_BYTES: usize = 8;
33
34/// [`OutputIndex`]es are stored as 3 bytes on disk.
35///
36/// This reduces database size and increases lookup performance.
37pub const OUTPUT_INDEX_DISK_BYTES: usize = 3;
38
39/// The maximum value of an on-disk serialized [`OutputIndex`].
40///
41/// This allows us to store [`OutputLocation`]s in
42/// 8 bytes, which makes database searches more efficient.
43///
44/// # Consensus
45///
46/// This output index is impossible with the current 2 MB block size limit.
47///
48/// Since Zebra only stores fully verified blocks on disk, blocks with larger indexes
49/// are rejected before reaching the database.
50pub const MAX_ON_DISK_OUTPUT_INDEX: OutputIndex =
51    OutputIndex((1 << (OUTPUT_INDEX_DISK_BYTES * 8)) - 1);
52
53/// [`OutputLocation`]s are stored as a 3 byte height, 2 byte transaction index,
54/// and 3 byte output index on disk.
55///
56/// This reduces database size and increases lookup performance.
57pub const OUTPUT_LOCATION_DISK_BYTES: usize =
58    TRANSACTION_LOCATION_DISK_BYTES + OUTPUT_INDEX_DISK_BYTES;
59
60// Transparent types
61
62/// A transparent output's index in its transaction.
63#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
64pub struct OutputIndex(u32);
65
66impl OutputIndex {
67    /// Create a transparent output index from the Zcash consensus integer type.
68    ///
69    /// `u32` is also the inner type.
70    pub fn from_index(output_index: u32) -> OutputIndex {
71        OutputIndex(output_index)
72    }
73
74    /// Returns this index as the inner type.
75    pub fn index(&self) -> u32 {
76        self.0
77    }
78
79    /// Create a transparent output index from `usize`.
80    #[allow(dead_code)]
81    pub fn from_usize(output_index: usize) -> OutputIndex {
82        OutputIndex(
83            output_index
84                .try_into()
85                .expect("the maximum valid index fits in the inner type"),
86        )
87    }
88
89    /// Return this index as `usize`.
90    #[allow(dead_code)]
91    pub fn as_usize(&self) -> usize {
92        self.0
93            .try_into()
94            .expect("the maximum valid index fits in usize")
95    }
96
97    /// Create a transparent output index from `u64`.
98    #[allow(dead_code)]
99    pub fn from_u64(output_index: u64) -> OutputIndex {
100        OutputIndex(
101            output_index
102                .try_into()
103                .expect("the maximum u64 index fits in the inner type"),
104        )
105    }
106
107    /// Return this index as `u64`.
108    #[allow(dead_code)]
109    pub fn as_u64(&self) -> u64 {
110        self.0.into()
111    }
112}
113
114/// A transparent output's location in the chain, by block height and transaction index.
115///
116/// [`OutputLocation`]s are sorted in increasing chain order, by height, transaction index,
117/// and output index.
118#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
119#[cfg_attr(
120    any(test, feature = "proptest-impl"),
121    derive(Arbitrary, Serialize, Deserialize)
122)]
123pub struct OutputLocation {
124    /// The location of the transparent input's transaction.
125    transaction_location: TransactionLocation,
126
127    /// The index of the transparent output in its transaction.
128    output_index: OutputIndex,
129}
130
131impl OutputLocation {
132    /// Creates an output location from a block height, and `usize` transaction and output indexes.
133    #[allow(dead_code)]
134    pub fn from_usize(
135        height: Height,
136        transaction_index: usize,
137        output_index: usize,
138    ) -> OutputLocation {
139        OutputLocation {
140            transaction_location: TransactionLocation::from_usize(height, transaction_index),
141            output_index: OutputIndex::from_usize(output_index),
142        }
143    }
144
145    /// Creates an output location from an [`transparent::OutPoint`],
146    /// and the [`TransactionLocation`] of its transaction.
147    ///
148    /// The [`TransactionLocation`] is provided separately,
149    /// because the lookup is a database operation.
150    pub fn from_outpoint(
151        transaction_location: TransactionLocation,
152        outpoint: &transparent::OutPoint,
153    ) -> OutputLocation {
154        OutputLocation::from_output_index(transaction_location, outpoint.index)
155    }
156
157    /// Creates an output location from a [`TransactionLocation`] and a `u32` output index.
158    ///
159    /// Output indexes are serialized to `u32` in the Zcash consensus-critical transaction format.
160    pub fn from_output_index(
161        transaction_location: TransactionLocation,
162        output_index: u32,
163    ) -> OutputLocation {
164        OutputLocation {
165            transaction_location,
166            output_index: OutputIndex::from_index(output_index),
167        }
168    }
169
170    /// Returns the height of this [`transparent::Output`].
171    pub fn height(&self) -> Height {
172        self.transaction_location.height
173    }
174
175    /// Returns the transaction index of this [`transparent::Output`].
176    pub fn transaction_index(&self) -> TransactionIndex {
177        self.transaction_location.index
178    }
179
180    /// Returns the output index of this [`transparent::Output`].
181    pub fn output_index(&self) -> OutputIndex {
182        self.output_index
183    }
184
185    /// Returns the location of the transaction for this [`transparent::Output`].
186    pub fn transaction_location(&self) -> TransactionLocation {
187        self.transaction_location
188    }
189
190    /// Allows tests to set the height of this output location.
191    #[cfg(any(test, feature = "proptest-impl"))]
192    #[allow(dead_code)]
193    pub fn height_mut(&mut self) -> &mut Height {
194        &mut self.transaction_location.height
195    }
196}
197
198/// The location of the first [`transparent::Output`] sent to an address.
199///
200/// The address location stays the same, even if the corresponding output
201/// has been spent.
202///
203/// The first output location is used to represent the address in the database,
204/// because output locations are significantly smaller than addresses.
205///
206/// TODO: make this a different type to OutputLocation?
207///       derive IntoDisk and FromDisk?
208pub type AddressLocation = OutputLocation;
209
210/// The inner type of [`AddressBalanceLocation`] and [`AddressBalanceLocationChange`].
211#[derive(Copy, Clone, Debug, Eq, PartialEq)]
212#[cfg_attr(
213    any(test, feature = "proptest-impl"),
214    derive(Arbitrary, Serialize, Deserialize),
215    serde(bound = "C: Constraint + Clone")
216)]
217pub struct AddressBalanceLocationInner<C: Constraint + Copy + std::fmt::Debug> {
218    /// The total balance of all UTXOs sent to an address.
219    balance: Amount<C>,
220
221    /// The total balance of all spent and unspent outputs sent to an address.
222    received: u64,
223
224    /// The location of the first [`transparent::Output`] sent to an address.
225    location: AddressLocation,
226}
227
228impl<C: Constraint + Copy + std::fmt::Debug> AddressBalanceLocationInner<C> {
229    /// Creates a new [`AddressBalanceLocationInner`] from the location of
230    /// the first [`transparent::Output`] sent to an address.
231    ///
232    /// The returned value has a zero initial balance and received balance.
233    fn new(first_output: OutputLocation) -> Self {
234        Self {
235            balance: Amount::zero(),
236            received: 0,
237            location: first_output,
238        }
239    }
240
241    /// Returns the current balance for the address.
242    pub fn balance(&self) -> Amount<C> {
243        self.balance
244    }
245
246    /// Returns the current received balance for the address.
247    pub fn received(&self) -> u64 {
248        self.received
249    }
250
251    /// Returns a mutable reference to the current balance for the address.
252    pub fn balance_mut(&mut self) -> &mut Amount<C> {
253        &mut self.balance
254    }
255
256    /// Returns a mutable reference to the current received balance for the address.
257    pub fn received_mut(&mut self) -> &mut u64 {
258        &mut self.received
259    }
260
261    /// Returns the location of the first [`transparent::Output`] sent to an address.
262    pub fn address_location(&self) -> AddressLocation {
263        self.location
264    }
265
266    /// Allows tests to set the height of the address location.
267    #[cfg(any(test, feature = "proptest-impl"))]
268    #[allow(dead_code)]
269    pub fn height_mut(&mut self) -> &mut Height {
270        &mut self.location.transaction_location.height
271    }
272}
273
274impl<C: Constraint + Copy + std::fmt::Debug> std::ops::Add for AddressBalanceLocationInner<C> {
275    type Output = Result<Self, amount::Error>;
276
277    fn add(self, rhs: Self) -> Self::Output {
278        Ok(AddressBalanceLocationInner {
279            balance: (self.balance + rhs.balance)?,
280            received: self.received.saturating_add(rhs.received),
281            location: self.location.min(rhs.location),
282        })
283    }
284}
285
286/// Represents a change in the [`AddressBalanceLocation`] of a transparent address
287/// in the finalized state.
288pub struct AddressBalanceLocationChange(AddressBalanceLocationInner<NegativeAllowed>);
289
290impl AddressBalanceLocationChange {
291    /// Creates a new [`AddressBalanceLocationChange`].
292    ///
293    /// See [`AddressBalanceLocationInner::new`] for more details.
294    pub fn new(location: AddressLocation) -> Self {
295        Self(AddressBalanceLocationInner::new(location))
296    }
297
298    /// Updates the current balance by adding the supplied output's value.
299    #[allow(clippy::unwrap_in_result)]
300    pub fn receive_output(
301        &mut self,
302        unspent_output: &transparent::Output,
303    ) -> Result<(), amount::Error> {
304        self.balance = (self
305            .balance
306            .zatoshis()
307            .checked_add(unspent_output.value().zatoshis()))
308        .expect("adding two Amounts is always within an i64")
309        .try_into()?;
310        self.received = self.received.saturating_add(unspent_output.value().into());
311        Ok(())
312    }
313
314    /// Updates the current balance by subtracting the supplied output's value.
315    #[allow(clippy::unwrap_in_result)]
316    pub fn spend_output(
317        &mut self,
318        spent_output: &transparent::Output,
319    ) -> Result<(), amount::Error> {
320        self.balance = (self
321            .balance
322            .zatoshis()
323            .checked_sub(spent_output.value().zatoshis()))
324        .expect("subtracting two Amounts is always within an i64")
325        .try_into()?;
326
327        Ok(())
328    }
329}
330
331impl std::ops::Deref for AddressBalanceLocationChange {
332    type Target = AddressBalanceLocationInner<NegativeAllowed>;
333
334    fn deref(&self) -> &Self::Target {
335        &self.0
336    }
337}
338
339impl std::ops::DerefMut for AddressBalanceLocationChange {
340    fn deref_mut(&mut self) -> &mut Self::Target {
341        &mut self.0
342    }
343}
344
345impl std::ops::Add for AddressBalanceLocationChange {
346    type Output = Result<Self, amount::Error>;
347
348    fn add(self, rhs: Self) -> Self::Output {
349        (self.0 + rhs.0).map(Self)
350    }
351}
352
353/// Data which Zebra indexes for each [`transparent::Address`].
354///
355/// Currently, Zebra tracks this data 1:1 for each address:
356/// - the balance [`Amount`] for a transparent address, and
357/// - the [`AddressLocation`] for the first [`transparent::Output`] sent to that address
358///   (regardless of whether that output is spent or unspent).
359///
360/// All other address data is tracked multiple times for each address
361/// (UTXOs and transactions).
362#[derive(Copy, Clone, Debug, Eq, PartialEq)]
363#[cfg_attr(
364    any(test, feature = "proptest-impl"),
365    derive(Arbitrary, Serialize, Deserialize)
366)]
367pub struct AddressBalanceLocation(AddressBalanceLocationInner<NonNegative>);
368
369impl AddressBalanceLocation {
370    /// Creates a new [`AddressBalanceLocation`].
371    ///
372    /// See [`AddressBalanceLocationInner::new`] for more details.
373    pub fn new(first_output: OutputLocation) -> Self {
374        Self(AddressBalanceLocationInner::new(first_output))
375    }
376
377    /// Consumes self and returns a new [`AddressBalanceLocationChange`] with
378    /// a zero balance, zero received balance, and the `location` of `self`.
379    pub fn into_new_change(self) -> AddressBalanceLocationChange {
380        AddressBalanceLocationChange::new(self.location)
381    }
382}
383
384impl std::ops::Deref for AddressBalanceLocation {
385    type Target = AddressBalanceLocationInner<NonNegative>;
386
387    fn deref(&self) -> &Self::Target {
388        &self.0
389    }
390}
391
392impl std::ops::DerefMut for AddressBalanceLocation {
393    fn deref_mut(&mut self) -> &mut Self::Target {
394        &mut self.0
395    }
396}
397
398impl std::ops::Add for AddressBalanceLocation {
399    type Output = Result<Self, amount::Error>;
400
401    fn add(self, rhs: Self) -> Self::Output {
402        (self.0 + rhs.0).map(Self)
403    }
404}
405
406/// A single unspent output for a [`transparent::Address`].
407///
408/// We store both the address location key and unspend output location value
409/// in the RocksDB column family key. This improves insert and delete performance.
410///
411/// This requires 8 extra bytes for each unspent output,
412/// because we repeat the key for each value.
413/// But RocksDB compression reduces the duplicate data size on disk.
414#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
415#[cfg_attr(
416    any(test, feature = "proptest-impl"),
417    derive(Arbitrary, Serialize, Deserialize)
418)]
419pub struct AddressUnspentOutput {
420    /// The location of the first [`transparent::Output`] sent to the address in `output`.
421    address_location: AddressLocation,
422
423    /// The location of this unspent output.
424    unspent_output_location: OutputLocation,
425}
426
427impl AddressUnspentOutput {
428    /// Create a new [`AddressUnspentOutput`] from an address location,
429    /// and an unspent output location.
430    pub fn new(
431        address_location: AddressLocation,
432        unspent_output_location: OutputLocation,
433    ) -> AddressUnspentOutput {
434        AddressUnspentOutput {
435            address_location,
436            unspent_output_location,
437        }
438    }
439
440    /// Create an [`AddressUnspentOutput`] which starts iteration for the
441    /// supplied address. Used to look up the first output with
442    /// [`ReadDisk::zs_next_key_value_from`][1].
443    ///
444    /// The unspent output location is before all unspent output locations in
445    /// the index. It is always invalid, due to the genesis consensus rules. But
446    /// this is not an issue since [`ReadDisk::zs_next_key_value_from`][1] will
447    /// fetch the next existing (valid) value.
448    ///
449    /// [1]: super::super::disk_db::ReadDisk::zs_next_key_value_from
450    pub fn address_iterator_start(address_location: AddressLocation) -> AddressUnspentOutput {
451        // Iterating from the lowest possible output location gets us the first output.
452        let zero_output_location = OutputLocation::from_usize(Height(0), 0, 0);
453
454        AddressUnspentOutput {
455            address_location,
456            unspent_output_location: zero_output_location,
457        }
458    }
459
460    /// Update the unspent output location to the next possible output for the
461    /// supplied address. Used to look up the next output with
462    /// [`ReadDisk::zs_next_key_value_from`][1].
463    ///
464    /// The updated unspent output location may be invalid, which is not an
465    /// issue since [`ReadDisk::zs_next_key_value_from`][1] will fetch the next
466    /// existing (valid) value.
467    ///
468    /// [1]: super::super::disk_db::ReadDisk::zs_next_key_value_from
469    pub fn address_iterator_next(&mut self) {
470        // Iterating from the next possible output location gets us the next output,
471        // even if it is in a later block or transaction.
472        //
473        // Consensus: the block size limit is 2MB, which is much lower than the index range.
474        self.unspent_output_location.output_index.0 += 1;
475    }
476
477    /// The location of the first [`transparent::Output`] sent to the address of this output.
478    ///
479    /// This can be used to look up the address.
480    pub fn address_location(&self) -> AddressLocation {
481        self.address_location
482    }
483
484    /// The location of this unspent output.
485    pub fn unspent_output_location(&self) -> OutputLocation {
486        self.unspent_output_location
487    }
488
489    /// Allows tests to modify the address location.
490    #[cfg(any(test, feature = "proptest-impl"))]
491    #[allow(dead_code)]
492    pub fn address_location_mut(&mut self) -> &mut AddressLocation {
493        &mut self.address_location
494    }
495
496    /// Allows tests to modify the unspent output location.
497    #[cfg(any(test, feature = "proptest-impl"))]
498    #[allow(dead_code)]
499    pub fn unspent_output_location_mut(&mut self) -> &mut OutputLocation {
500        &mut self.unspent_output_location
501    }
502}
503
504/// A single transaction sent to a [`transparent::Address`].
505///
506/// We store both the address location key and transaction location value
507/// in the RocksDB column family key. This improves insert and delete performance.
508///
509/// This requires 8 extra bytes for each transaction location,
510/// because we repeat the key for each value.
511/// But RocksDB compression reduces the duplicate data size on disk.
512#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
513#[cfg_attr(
514    any(test, feature = "proptest-impl"),
515    derive(Arbitrary, Serialize, Deserialize)
516)]
517pub struct AddressTransaction {
518    /// The location of the first [`transparent::Output`] sent to the address in `output`.
519    address_location: AddressLocation,
520
521    /// The location of the transaction sent to the address.
522    transaction_location: TransactionLocation,
523}
524
525impl AddressTransaction {
526    /// Create a new [`AddressTransaction`] from an address location,
527    /// and a transaction location.
528    pub fn new(
529        address_location: AddressLocation,
530        transaction_location: TransactionLocation,
531    ) -> AddressTransaction {
532        AddressTransaction {
533            address_location,
534            transaction_location,
535        }
536    }
537
538    /// Create a range of [`AddressTransaction`]s which starts iteration for the supplied
539    /// address. Starts at the first UTXO, or at the `query` start height, whichever is greater.
540    /// Ends at the maximum possible transaction index for the end height.
541    ///
542    /// Used to look up transactions with [`DiskDb::zs_forward_range_iter`][1].
543    ///
544    /// The transaction locations in the:
545    /// - start bound might be invalid, if it is based on the `query` start height.
546    /// - end bound will always be invalid.
547    ///
548    /// But this is not an issue, since [`DiskDb::zs_forward_range_iter`][1] will fetch all existing
549    /// (valid) values in the range.
550    ///
551    /// [1]: super::super::disk_db::DiskDb
552    pub fn address_iterator_range(
553        address_location: AddressLocation,
554        query: std::ops::RangeInclusive<Height>,
555    ) -> std::ops::RangeInclusive<AddressTransaction> {
556        // Iterating from the lowest possible transaction location gets us the first transaction.
557        //
558        // The address location is the output location of the first UTXO sent to the address,
559        // and addresses can not spend funds until they receive their first UTXO.
560        let first_utxo_location = address_location.transaction_location();
561
562        // Iterating from the start height to the end height filters out transactions that aren't needed.
563        let query_start_location = TransactionLocation::from_index(*query.start(), 0);
564        let query_end_location = TransactionLocation::from_index(*query.end(), u16::MAX);
565
566        let addr_tx = |tx_loc| AddressTransaction::new(address_location, tx_loc);
567
568        addr_tx(max(first_utxo_location, query_start_location))..=addr_tx(query_end_location)
569    }
570
571    /// Update the transaction location to the next possible transaction for the
572    /// supplied address. Used to look up the next output with
573    /// [`ReadDisk::zs_next_key_value_from`][1].
574    ///
575    /// The updated transaction location may be invalid, which is not an issue
576    /// since [`ReadDisk::zs_next_key_value_from`][1] will fetch the next
577    /// existing (valid) value.
578    ///
579    /// [1]: super::super::disk_db::ReadDisk::zs_next_key_value_from
580    #[allow(dead_code)]
581    pub fn address_iterator_next(&mut self) {
582        // Iterating from the next possible output location gets us the next output,
583        // even if it is in a later block or transaction.
584        //
585        // Consensus: the block size limit is 2MB, which is much lower than the index range.
586        self.transaction_location.index.0 += 1;
587    }
588
589    /// The location of the first [`transparent::Output`] sent to the address of this output.
590    ///
591    /// This can be used to look up the address.
592    pub fn address_location(&self) -> AddressLocation {
593        self.address_location
594    }
595
596    /// The location of this transaction.
597    pub fn transaction_location(&self) -> TransactionLocation {
598        self.transaction_location
599    }
600
601    /// Allows tests to modify the address location.
602    #[cfg(any(test, feature = "proptest-impl"))]
603    #[allow(dead_code)]
604    pub fn address_location_mut(&mut self) -> &mut AddressLocation {
605        &mut self.address_location
606    }
607
608    /// Allows tests to modify the unspent output location.
609    #[cfg(any(test, feature = "proptest-impl"))]
610    #[allow(dead_code)]
611    pub fn transaction_location_mut(&mut self) -> &mut TransactionLocation {
612        &mut self.transaction_location
613    }
614}
615
616// Transparent trait impls
617
618/// Returns a byte representing the [`transparent::Address`] variant.
619fn address_variant(address: &transparent::Address) -> u8 {
620    use NetworkKind::*;
621    // Return smaller values for more common variants.
622    //
623    // (This probably doesn't matter, but it might help slightly with data compression.)
624    match (address.network_kind(), address) {
625        (Mainnet, PayToPublicKeyHash { .. }) => 0,
626        (Mainnet, PayToScriptHash { .. }) => 1,
627        // There's no way to distinguish between Regtest and Testnet for encoded transparent addresses,
628        // we can consider `Regtest` to use `Testnet` transparent addresses, so it's okay to use the `Testnet`
629        // address variant for `Regtest` transparent addresses in the db format
630        (Testnet | Regtest, PayToPublicKeyHash { .. }) => 2,
631        (Testnet | Regtest, PayToScriptHash { .. }) => 3,
632        // TEX address variants
633        (Mainnet, Tex { .. }) => 4,
634        (Testnet | Regtest, Tex { .. }) => 5,
635    }
636}
637
638impl IntoDisk for transparent::Address {
639    type Bytes = [u8; 21];
640
641    fn as_bytes(&self) -> Self::Bytes {
642        let variant_bytes = vec![address_variant(self)];
643        let hash_bytes = self.hash_bytes().to_vec();
644
645        [variant_bytes, hash_bytes].concat().try_into().unwrap()
646    }
647}
648
649#[cfg(any(test, feature = "proptest-impl"))]
650impl FromDisk for transparent::Address {
651    fn from_bytes(disk_bytes: impl AsRef<[u8]>) -> Self {
652        let (address_variant, hash_bytes) = disk_bytes.as_ref().split_at(1);
653
654        let address_variant = address_variant[0];
655        let hash_bytes = hash_bytes.try_into().unwrap();
656
657        let network = if address_variant < 2 {
658            NetworkKind::Mainnet
659        } else {
660            NetworkKind::Testnet
661        };
662
663        if address_variant % 2 == 0 {
664            transparent::Address::from_pub_key_hash(network, hash_bytes)
665        } else {
666            transparent::Address::from_script_hash(network, hash_bytes)
667        }
668    }
669}
670
671impl<C: Constraint> IntoDisk for Amount<C> {
672    type Bytes = [u8; BALANCE_DISK_BYTES];
673
674    fn as_bytes(&self) -> Self::Bytes {
675        self.to_bytes()
676    }
677}
678
679impl FromDisk for Amount<NonNegative> {
680    fn from_bytes(bytes: impl AsRef<[u8]>) -> Self {
681        let array = bytes.as_ref().try_into().unwrap();
682        Amount::from_bytes(array).unwrap()
683    }
684}
685
686impl IntoDisk for OutputIndex {
687    type Bytes = [u8; OUTPUT_INDEX_DISK_BYTES];
688
689    fn as_bytes(&self) -> Self::Bytes {
690        let mem_bytes = self.index().to_be_bytes();
691
692        let disk_bytes = truncate_zero_be_bytes(&mem_bytes, OUTPUT_INDEX_DISK_BYTES);
693
694        match disk_bytes {
695            Some(b) => b.try_into().unwrap(),
696            // # Security
697            //
698            // The RPC method or state query was given a transparent output index that is
699            // impossible with the current block size limit of 2 MB. To save space in database
700            // indexes, we don't support output indexes 2^24 and above.
701            //
702            // Instead,  we return an invalid database output index to the lookup code,
703            // which can never be inserted into the database as part of a valid block.
704            // So RPC methods will return an error or None.
705            None => {
706                #[cfg(test)]
707                {
708                    use zebra_chain::serialization::TrustedPreallocate;
709                    assert!(
710                        u64::from(MAX_ON_DISK_OUTPUT_INDEX.0)
711                            > zebra_chain::transparent::Output::max_allocation(),
712                        "increased block size requires database output index format change",
713                    );
714                }
715
716                truncate_zero_be_bytes(
717                    &MAX_ON_DISK_OUTPUT_INDEX.0.to_be_bytes(),
718                    OUTPUT_INDEX_DISK_BYTES,
719                )
720                .expect("max on disk output index is valid")
721                .try_into()
722                .unwrap()
723            }
724        }
725    }
726}
727
728impl FromDisk for OutputIndex {
729    fn from_bytes(disk_bytes: impl AsRef<[u8]>) -> Self {
730        let mem_len = u32::BITS / 8;
731        let mem_len = mem_len.try_into().unwrap();
732
733        let mem_bytes = expand_zero_be_bytes(disk_bytes.as_ref(), mem_len);
734        let mem_bytes = mem_bytes.try_into().unwrap();
735        OutputIndex::from_index(u32::from_be_bytes(mem_bytes))
736    }
737}
738
739impl IntoDisk for OutputLocation {
740    type Bytes = [u8; OUTPUT_LOCATION_DISK_BYTES];
741
742    fn as_bytes(&self) -> Self::Bytes {
743        let transaction_location_bytes = self.transaction_location().as_bytes().to_vec();
744        let output_index_bytes = self.output_index().as_bytes().to_vec();
745
746        [transaction_location_bytes, output_index_bytes]
747            .concat()
748            .try_into()
749            .unwrap()
750    }
751}
752
753impl FromDisk for OutputLocation {
754    fn from_bytes(disk_bytes: impl AsRef<[u8]>) -> Self {
755        let (transaction_location_bytes, output_index_bytes) = disk_bytes
756            .as_ref()
757            .split_at(TRANSACTION_LOCATION_DISK_BYTES);
758
759        let transaction_location = TransactionLocation::from_bytes(transaction_location_bytes);
760        let output_index = OutputIndex::from_bytes(output_index_bytes);
761
762        OutputLocation {
763            transaction_location,
764            output_index,
765        }
766    }
767}
768
769impl<C: Constraint + Copy + std::fmt::Debug> IntoDisk for AddressBalanceLocationInner<C> {
770    type Bytes = [u8; BALANCE_DISK_BYTES + OUTPUT_LOCATION_DISK_BYTES + size_of::<u64>()];
771
772    fn as_bytes(&self) -> Self::Bytes {
773        let balance_bytes = self.balance().as_bytes().to_vec();
774        let address_location_bytes = self.address_location().as_bytes().to_vec();
775        let received_bytes = self.received().to_le_bytes().to_vec();
776
777        [balance_bytes, address_location_bytes, received_bytes]
778            .concat()
779            .try_into()
780            .unwrap()
781    }
782}
783
784impl IntoDisk for AddressBalanceLocation {
785    type Bytes = [u8; BALANCE_DISK_BYTES + OUTPUT_LOCATION_DISK_BYTES + size_of::<u64>()];
786
787    fn as_bytes(&self) -> Self::Bytes {
788        self.0.as_bytes()
789    }
790}
791
792impl IntoDisk for AddressBalanceLocationChange {
793    type Bytes = [u8; BALANCE_DISK_BYTES + OUTPUT_LOCATION_DISK_BYTES + size_of::<u64>()];
794
795    fn as_bytes(&self) -> Self::Bytes {
796        self.0.as_bytes()
797    }
798}
799
800impl<C: Constraint + Copy + std::fmt::Debug> FromDisk for AddressBalanceLocationInner<C> {
801    fn from_bytes(disk_bytes: impl AsRef<[u8]>) -> Self {
802        let (balance_bytes, rest) = disk_bytes.as_ref().split_at(BALANCE_DISK_BYTES);
803        let (address_location_bytes, rest) = rest.split_at(BALANCE_DISK_BYTES);
804        let (received_bytes, _) = rest.split_at_checked(size_of::<u64>()).unwrap_or_default();
805
806        let balance = Amount::from_bytes(balance_bytes.try_into().unwrap()).unwrap();
807        let address_location = AddressLocation::from_bytes(address_location_bytes);
808        // # Backwards Compatibility
809        //
810        // If the value is missing a `received` field, default to 0.
811        let received = u64::from_le_bytes(received_bytes.try_into().unwrap_or_default());
812
813        let mut address_balance_location = Self::new(address_location);
814        *address_balance_location.balance_mut() = balance;
815        *address_balance_location.received_mut() = received;
816
817        address_balance_location
818    }
819}
820
821impl FromDisk for AddressBalanceLocation {
822    fn from_bytes(disk_bytes: impl AsRef<[u8]>) -> Self {
823        Self(AddressBalanceLocationInner::from_bytes(disk_bytes))
824    }
825}
826
827impl FromDisk for AddressBalanceLocationChange {
828    fn from_bytes(disk_bytes: impl AsRef<[u8]>) -> Self {
829        Self(AddressBalanceLocationInner::from_bytes(disk_bytes))
830    }
831}
832
833impl IntoDisk for transparent::Output {
834    type Bytes = Vec<u8>;
835
836    fn as_bytes(&self) -> Self::Bytes {
837        self.zcash_serialize_to_vec().unwrap()
838    }
839}
840
841impl FromDisk for transparent::Output {
842    fn from_bytes(bytes: impl AsRef<[u8]>) -> Self {
843        bytes.as_ref().zcash_deserialize_into().unwrap()
844    }
845}
846
847impl IntoDisk for AddressUnspentOutput {
848    type Bytes = [u8; OUTPUT_LOCATION_DISK_BYTES + OUTPUT_LOCATION_DISK_BYTES];
849
850    fn as_bytes(&self) -> Self::Bytes {
851        let address_location_bytes = self.address_location().as_bytes();
852        let unspent_output_location_bytes = self.unspent_output_location().as_bytes();
853
854        [address_location_bytes, unspent_output_location_bytes]
855            .concat()
856            .try_into()
857            .unwrap()
858    }
859}
860
861impl FromDisk for AddressUnspentOutput {
862    fn from_bytes(disk_bytes: impl AsRef<[u8]>) -> Self {
863        let (address_location_bytes, unspent_output_location_bytes) =
864            disk_bytes.as_ref().split_at(OUTPUT_LOCATION_DISK_BYTES);
865
866        let address_location = AddressLocation::from_bytes(address_location_bytes);
867        let unspent_output_location = AddressLocation::from_bytes(unspent_output_location_bytes);
868
869        AddressUnspentOutput::new(address_location, unspent_output_location)
870    }
871}
872
873impl IntoDisk for AddressTransaction {
874    type Bytes = [u8; OUTPUT_LOCATION_DISK_BYTES + TRANSACTION_LOCATION_DISK_BYTES];
875
876    fn as_bytes(&self) -> Self::Bytes {
877        let address_location_bytes: [u8; OUTPUT_LOCATION_DISK_BYTES] =
878            self.address_location().as_bytes();
879        let transaction_location_bytes: [u8; TRANSACTION_LOCATION_DISK_BYTES] =
880            self.transaction_location().as_bytes();
881
882        address_location_bytes
883            .iter()
884            .copied()
885            .chain(transaction_location_bytes.iter().copied())
886            .collect::<Vec<u8>>()
887            .try_into()
888            .expect("concatenation of fixed-sized arrays should have the correct size")
889    }
890}
891
892impl FromDisk for AddressTransaction {
893    fn from_bytes(disk_bytes: impl AsRef<[u8]>) -> Self {
894        let (address_location_bytes, transaction_location_bytes) =
895            disk_bytes.as_ref().split_at(OUTPUT_LOCATION_DISK_BYTES);
896
897        let address_location = AddressLocation::from_bytes(address_location_bytes);
898        let transaction_location = TransactionLocation::from_bytes(transaction_location_bytes);
899
900        AddressTransaction::new(address_location, transaction_location)
901    }
902}