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