diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 215fa5de..6602bf84 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -211,6 +211,10 @@ jobs: /zebra-state/**/constants.rs /zebra-state/**/finalized_state.rs /zebra-state/**/disk_format.rs + /zebra-state/**/disk_format/block.rs + /zebra-state/**/disk_format/chain.rs + /zebra-state/**/disk_format/shielded.rs + /zebra-state/**/disk_format/transparent.rs /zebra-state/**/disk_db.rs /zebra-state/**/zebra_db.rs /zebra-state/**/zebra_db/block.rs diff --git a/zebra-state/src/service/finalized_state/disk_format.rs b/zebra-state/src/service/finalized_state/disk_format.rs index dccfb49c..3d05d55f 100644 --- a/zebra-state/src/service/finalized_state/disk_format.rs +++ b/zebra-state/src/service/finalized_state/disk_format.rs @@ -5,65 +5,44 @@ //! The [`crate::constants::DATABASE_FORMAT_VERSION`] constant must //! be incremented each time the database format (column, serialization, etc) changes. -use std::{collections::BTreeMap, convert::TryInto, fmt::Debug, sync::Arc}; +use std::sync::Arc; -use bincode::Options; -use serde::{Deserialize, Serialize}; - -use zebra_chain::{ - amount::NonNegative, - block, - block::{Block, Height}, - history_tree::NonEmptyHistoryTree, - orchard, - parameters::Network, - primitives::zcash_history, - sapling, - serialization::{ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize}, - sprout, transaction, transparent, - value_balance::ValueBalance, -}; +pub mod block; +pub mod chain; +pub mod shielded; +pub mod transparent; #[cfg(test)] mod tests; -/// A transaction's location in the chain, by block height and transaction index. -/// -/// This provides a chain-order list of transactions. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] -pub struct TransactionLocation { - /// The block height of the transaction. - pub height: block::Height, +pub use block::TransactionLocation; - /// The index of the transaction in its block. - pub index: u32, -} - -impl TransactionLocation { - /// Create a transaction location from a block height and index (as the native index integer type). - #[allow(dead_code)] - pub fn from_usize(height: Height, index: usize) -> TransactionLocation { - TransactionLocation { - height, - index: index - .try_into() - .expect("all valid indexes are much lower than u32::MAX"), - } - } -} - -// Helper trait for defining the exact format used to interact with disk per -// type. +/// Helper trait for defining the exact format used to interact with disk per +/// type. pub trait IntoDisk { - // The type used to compare a value as a key to other keys stored in a - // database + /// The type used to compare a value as a key to other keys stored in a + /// database. type Bytes: AsRef<[u8]>; - // function to convert the current type to its disk format in `zs_get()` - // without necessarily allocating a new IVec + /// Converts the current type to its disk format in `zs_get()`, + /// without necessarily allocating a new ivec. fn as_bytes(&self) -> Self::Bytes; } +/// Helper type for retrieving types from the disk with the correct format. +/// +/// The ivec should be correctly encoded by IntoDisk. +pub trait FromDisk: Sized { + /// Function to convert the disk bytes back into the deserialized type. + /// + /// # Panics + /// + /// - if the input data doesn't deserialize correctly + fn from_bytes(bytes: impl AsRef<[u8]>) -> Self; +} + +// Generic trait impls + impl<'a, T> IntoDisk for &'a T where T: IntoDisk, @@ -86,18 +65,6 @@ where } } -/// Helper type for retrieving types from the disk with the correct format. -/// -/// The ivec should be correctly encoded by IntoDisk. -pub trait FromDisk: Sized { - /// Function to convert the disk bytes back into the deserialized type. - /// - /// # Panics - /// - /// - if the input data doesn't deserialize correctly - fn from_bytes(bytes: impl AsRef<[u8]>) -> Self; -} - impl FromDisk for Arc where T: FromDisk, @@ -107,106 +74,6 @@ where } } -impl IntoDisk for Block { - type Bytes = Vec; - - fn as_bytes(&self) -> Self::Bytes { - self.zcash_serialize_to_vec() - .expect("serialization to vec doesn't fail") - } -} - -impl FromDisk for Block { - fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { - Block::zcash_deserialize(bytes.as_ref()) - .expect("deserialization format should match the serialization format used by IntoDisk") - } -} - -impl IntoDisk for TransactionLocation { - type Bytes = [u8; 8]; - - fn as_bytes(&self) -> Self::Bytes { - let height_bytes = self.height.0.to_be_bytes(); - let index_bytes = self.index.to_be_bytes(); - - let mut bytes = [0; 8]; - - bytes[0..4].copy_from_slice(&height_bytes); - bytes[4..8].copy_from_slice(&index_bytes); - - bytes - } -} - -impl FromDisk for TransactionLocation { - fn from_bytes(disk_bytes: impl AsRef<[u8]>) -> Self { - let disk_bytes = disk_bytes.as_ref(); - let height = { - let mut bytes = [0; 4]; - bytes.copy_from_slice(&disk_bytes[0..4]); - let height = u32::from_be_bytes(bytes); - block::Height(height) - }; - - let index = { - let mut bytes = [0; 4]; - bytes.copy_from_slice(&disk_bytes[4..8]); - u32::from_be_bytes(bytes) - }; - - TransactionLocation { height, index } - } -} - -impl IntoDisk for transaction::Hash { - type Bytes = [u8; 32]; - - fn as_bytes(&self) -> Self::Bytes { - self.0 - } -} - -impl IntoDisk for block::Hash { - type Bytes = [u8; 32]; - - fn as_bytes(&self) -> Self::Bytes { - self.0 - } -} - -impl FromDisk for block::Hash { - fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { - let array = bytes.as_ref().try_into().unwrap(); - Self(array) - } -} - -impl IntoDisk for sprout::Nullifier { - type Bytes = [u8; 32]; - - fn as_bytes(&self) -> Self::Bytes { - self.0 - } -} - -impl IntoDisk for sapling::Nullifier { - type Bytes = [u8; 32]; - - fn as_bytes(&self) -> Self::Bytes { - self.0 - } -} - -impl IntoDisk for orchard::Nullifier { - type Bytes = [u8; 32]; - - fn as_bytes(&self) -> Self::Bytes { - let nullifier: orchard::Nullifier = *self; - nullifier.into() - } -} - impl IntoDisk for () { type Bytes = [u8; 0]; @@ -214,197 +81,3 @@ impl IntoDisk for () { [] } } - -impl IntoDisk for block::Height { - type Bytes = [u8; 4]; - - fn as_bytes(&self) -> Self::Bytes { - self.0.to_be_bytes() - } -} - -impl FromDisk for block::Height { - fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { - let array = bytes.as_ref().try_into().unwrap(); - block::Height(u32::from_be_bytes(array)) - } -} - -impl IntoDisk for transparent::Utxo { - type Bytes = Vec; - - fn as_bytes(&self) -> Self::Bytes { - let mut bytes = vec![0; 5]; - bytes[0..4].copy_from_slice(&self.height.0.to_be_bytes()); - bytes[4] = self.from_coinbase as u8; - self.output - .zcash_serialize(&mut bytes) - .expect("serialization to vec doesn't fail"); - bytes - } -} - -impl FromDisk for transparent::Utxo { - fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { - let (meta_bytes, output_bytes) = bytes.as_ref().split_at(5); - let height = block::Height(u32::from_be_bytes(meta_bytes[0..4].try_into().unwrap())); - let from_coinbase = meta_bytes[4] == 1u8; - let output = output_bytes - .zcash_deserialize_into() - .expect("db has serialized data"); - Self { - output, - height, - from_coinbase, - } - } -} - -impl IntoDisk for transparent::OutPoint { - type Bytes = Vec; - - fn as_bytes(&self) -> Self::Bytes { - self.zcash_serialize_to_vec() - .expect("serialization to vec doesn't fail") - } -} - -impl IntoDisk for sprout::tree::Root { - type Bytes = [u8; 32]; - - fn as_bytes(&self) -> Self::Bytes { - self.into() - } -} - -impl IntoDisk for sapling::tree::Root { - type Bytes = [u8; 32]; - - fn as_bytes(&self) -> Self::Bytes { - self.into() - } -} - -impl IntoDisk for orchard::tree::Root { - type Bytes = [u8; 32]; - - fn as_bytes(&self) -> Self::Bytes { - self.into() - } -} - -impl IntoDisk for ValueBalance { - type Bytes = [u8; 32]; - - fn as_bytes(&self) -> Self::Bytes { - self.to_bytes() - } -} - -impl FromDisk for ValueBalance { - fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { - let array = bytes.as_ref().try_into().unwrap(); - ValueBalance::from_bytes(array).unwrap() - } -} - -// The following implementations for the note commitment trees use `serde` and -// `bincode` because currently the inner Merkle tree frontier (from -// `incrementalmerkletree`) only supports `serde` for serialization. `bincode` -// was chosen because it is small and fast. We explicitly use `DefaultOptions` -// in particular to disallow trailing bytes; see -// https://docs.rs/bincode/1.3.3/bincode/config/index.html#options-struct-vs-bincode-functions - -impl IntoDisk for sprout::tree::NoteCommitmentTree { - type Bytes = Vec; - - fn as_bytes(&self) -> Self::Bytes { - bincode::DefaultOptions::new() - .serialize(self) - .expect("serialization to vec doesn't fail") - } -} - -impl FromDisk for sprout::tree::NoteCommitmentTree { - fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { - bincode::DefaultOptions::new() - .deserialize(bytes.as_ref()) - .expect("deserialization format should match the serialization format used by IntoDisk") - } -} -impl IntoDisk for sapling::tree::NoteCommitmentTree { - type Bytes = Vec; - - fn as_bytes(&self) -> Self::Bytes { - bincode::DefaultOptions::new() - .serialize(self) - .expect("serialization to vec doesn't fail") - } -} - -impl FromDisk for sapling::tree::NoteCommitmentTree { - fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { - bincode::DefaultOptions::new() - .deserialize(bytes.as_ref()) - .expect("deserialization format should match the serialization format used by IntoDisk") - } -} - -impl IntoDisk for orchard::tree::NoteCommitmentTree { - type Bytes = Vec; - - fn as_bytes(&self) -> Self::Bytes { - bincode::DefaultOptions::new() - .serialize(self) - .expect("serialization to vec doesn't fail") - } -} - -impl FromDisk for orchard::tree::NoteCommitmentTree { - fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { - bincode::DefaultOptions::new() - .deserialize(bytes.as_ref()) - .expect("deserialization format should match the serialization format used by IntoDisk") - } -} - -#[derive(serde::Serialize, serde::Deserialize)] -struct HistoryTreeParts { - network: Network, - size: u32, - peaks: BTreeMap, - current_height: Height, -} - -impl IntoDisk for NonEmptyHistoryTree { - type Bytes = Vec; - - fn as_bytes(&self) -> Self::Bytes { - let data = HistoryTreeParts { - network: self.network(), - size: self.size(), - peaks: self.peaks().clone(), - current_height: self.current_height(), - }; - bincode::DefaultOptions::new() - .serialize(&data) - .expect("serialization to vec doesn't fail") - } -} - -impl FromDisk for NonEmptyHistoryTree { - fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { - let parts: HistoryTreeParts = bincode::DefaultOptions::new() - .deserialize(bytes.as_ref()) - .expect( - "deserialization format should match the serialization format used by IntoDisk", - ); - NonEmptyHistoryTree::from_cache( - parts.network, - parts.size, - parts.peaks, - parts.current_height, - ) - .expect("deserialization format should match the serialization format used by IntoDisk") - } -} diff --git a/zebra-state/src/service/finalized_state/disk_format/block.rs b/zebra-state/src/service/finalized_state/disk_format/block.rs new file mode 100644 index 00000000..f9dbdc76 --- /dev/null +++ b/zebra-state/src/service/finalized_state/disk_format/block.rs @@ -0,0 +1,137 @@ +//! Block and transaction serialization formats for finalized data. +//! +//! # Correctness +//! +//! The [`crate::constants::DATABASE_FORMAT_VERSION`] constant must +//! be incremented each time the database format (column, serialization, etc) changes. + +use std::fmt::Debug; + +use serde::{Deserialize, Serialize}; + +use zebra_chain::{ + block::{self, Block, Height}, + serialization::{ZcashDeserialize, ZcashSerialize}, + transaction, +}; + +use crate::service::finalized_state::disk_format::{FromDisk, IntoDisk}; + +/// A transaction's location in the chain, by block height and transaction index. +/// +/// This provides a chain-order list of transactions. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] +pub struct TransactionLocation { + /// The block height of the transaction. + pub height: Height, + + /// The index of the transaction in its block. + pub index: u32, +} + +impl TransactionLocation { + /// Create a transaction location from a block height and index (as the native index integer type). + #[allow(dead_code)] + pub fn from_usize(height: Height, index: usize) -> TransactionLocation { + TransactionLocation { + height, + index: index + .try_into() + .expect("all valid indexes are much lower than u32::MAX"), + } + } +} + +// Block trait impls + +impl IntoDisk for Block { + type Bytes = Vec; + + fn as_bytes(&self) -> Self::Bytes { + self.zcash_serialize_to_vec() + .expect("serialization to vec doesn't fail") + } +} + +impl FromDisk for Block { + fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { + Block::zcash_deserialize(bytes.as_ref()) + .expect("deserialization format should match the serialization format used by IntoDisk") + } +} + +impl IntoDisk for Height { + type Bytes = [u8; 4]; + + fn as_bytes(&self) -> Self::Bytes { + self.0.to_be_bytes() + } +} + +impl FromDisk for Height { + fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { + let array = bytes.as_ref().try_into().unwrap(); + Height(u32::from_be_bytes(array)) + } +} + +impl IntoDisk for block::Hash { + type Bytes = [u8; 32]; + + fn as_bytes(&self) -> Self::Bytes { + self.0 + } +} + +impl FromDisk for block::Hash { + fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { + let array = bytes.as_ref().try_into().unwrap(); + Self(array) + } +} + +// Transaction trait impls + +impl IntoDisk for TransactionLocation { + type Bytes = [u8; 8]; + + fn as_bytes(&self) -> Self::Bytes { + let height_bytes = self.height.as_bytes(); + let index_bytes = self.index.to_be_bytes(); + + let mut bytes = [0; 8]; + + bytes[0..4].copy_from_slice(&height_bytes); + bytes[4..8].copy_from_slice(&index_bytes); + + bytes + } +} + +impl FromDisk for TransactionLocation { + fn from_bytes(disk_bytes: impl AsRef<[u8]>) -> Self { + let disk_bytes = disk_bytes.as_ref(); + let height = { + let mut bytes = [0; 4]; + bytes.copy_from_slice(&disk_bytes[0..4]); + let height = u32::from_be_bytes(bytes); + Height(height) + }; + + let index = { + let mut bytes = [0; 4]; + bytes.copy_from_slice(&disk_bytes[4..8]); + u32::from_be_bytes(bytes) + }; + + TransactionLocation { height, index } + } +} + +impl IntoDisk for transaction::Hash { + type Bytes = [u8; 32]; + + fn as_bytes(&self) -> Self::Bytes { + self.0 + } +} diff --git a/zebra-state/src/service/finalized_state/disk_format/chain.rs b/zebra-state/src/service/finalized_state/disk_format/chain.rs new file mode 100644 index 00000000..fb3d4271 --- /dev/null +++ b/zebra-state/src/service/finalized_state/disk_format/chain.rs @@ -0,0 +1,73 @@ +//! Chain data serialization formats for finalized data. +//! +//! # Correctness +//! +//! The [`crate::constants::DATABASE_FORMAT_VERSION`] constant must +//! be incremented each time the database format (column, serialization, etc) changes. + +use std::collections::BTreeMap; + +use bincode::Options; + +use zebra_chain::{ + amount::NonNegative, block::Height, history_tree::NonEmptyHistoryTree, parameters::Network, + primitives::zcash_history, value_balance::ValueBalance, +}; + +use crate::service::finalized_state::disk_format::{FromDisk, IntoDisk}; + +impl IntoDisk for ValueBalance { + type Bytes = [u8; 32]; + + fn as_bytes(&self) -> Self::Bytes { + self.to_bytes() + } +} + +impl FromDisk for ValueBalance { + fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { + let array = bytes.as_ref().try_into().unwrap(); + ValueBalance::from_bytes(array).unwrap() + } +} + +#[derive(serde::Serialize, serde::Deserialize)] +struct HistoryTreeParts { + network: Network, + size: u32, + peaks: BTreeMap, + current_height: Height, +} + +impl IntoDisk for NonEmptyHistoryTree { + type Bytes = Vec; + + fn as_bytes(&self) -> Self::Bytes { + let data = HistoryTreeParts { + network: self.network(), + size: self.size(), + peaks: self.peaks().clone(), + current_height: self.current_height(), + }; + bincode::DefaultOptions::new() + .serialize(&data) + .expect("serialization to vec doesn't fail") + } +} + +impl FromDisk for NonEmptyHistoryTree { + fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { + let parts: HistoryTreeParts = bincode::DefaultOptions::new() + .deserialize(bytes.as_ref()) + .expect( + "deserialization format should match the serialization format used by IntoDisk", + ); + NonEmptyHistoryTree::from_cache( + parts.network, + parts.size, + parts.peaks, + parts.current_height, + ) + .expect("deserialization format should match the serialization format used by IntoDisk") + } +} diff --git a/zebra-state/src/service/finalized_state/disk_format/shielded.rs b/zebra-state/src/service/finalized_state/disk_format/shielded.rs new file mode 100644 index 00000000..4cd75898 --- /dev/null +++ b/zebra-state/src/service/finalized_state/disk_format/shielded.rs @@ -0,0 +1,121 @@ +//! Shielded transfer serialization formats for finalized data. +//! +//! # Correctness +//! +//! The [`crate::constants::DATABASE_FORMAT_VERSION`] constant must +//! be incremented each time the database format (column, serialization, etc) changes. + +use bincode::Options; + +use zebra_chain::{orchard, sapling, sprout}; + +use crate::service::finalized_state::disk_format::{FromDisk, IntoDisk}; + +impl IntoDisk for sprout::Nullifier { + type Bytes = [u8; 32]; + + fn as_bytes(&self) -> Self::Bytes { + self.0 + } +} + +impl IntoDisk for sapling::Nullifier { + type Bytes = [u8; 32]; + + fn as_bytes(&self) -> Self::Bytes { + self.0 + } +} + +impl IntoDisk for orchard::Nullifier { + type Bytes = [u8; 32]; + + fn as_bytes(&self) -> Self::Bytes { + let nullifier: orchard::Nullifier = *self; + nullifier.into() + } +} + +impl IntoDisk for sprout::tree::Root { + type Bytes = [u8; 32]; + + fn as_bytes(&self) -> Self::Bytes { + self.into() + } +} + +impl IntoDisk for sapling::tree::Root { + type Bytes = [u8; 32]; + + fn as_bytes(&self) -> Self::Bytes { + self.into() + } +} + +impl IntoDisk for orchard::tree::Root { + type Bytes = [u8; 32]; + + fn as_bytes(&self) -> Self::Bytes { + self.into() + } +} + +// The following implementations for the note commitment trees use `serde` and +// `bincode` because currently the inner Merkle tree frontier (from +// `incrementalmerkletree`) only supports `serde` for serialization. `bincode` +// was chosen because it is small and fast. We explicitly use `DefaultOptions` +// in particular to disallow trailing bytes; see +// https://docs.rs/bincode/1.3.3/bincode/config/index.html#options-struct-vs-bincode-functions + +impl IntoDisk for sprout::tree::NoteCommitmentTree { + type Bytes = Vec; + + fn as_bytes(&self) -> Self::Bytes { + bincode::DefaultOptions::new() + .serialize(self) + .expect("serialization to vec doesn't fail") + } +} + +impl FromDisk for sprout::tree::NoteCommitmentTree { + fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { + bincode::DefaultOptions::new() + .deserialize(bytes.as_ref()) + .expect("deserialization format should match the serialization format used by IntoDisk") + } +} +impl IntoDisk for sapling::tree::NoteCommitmentTree { + type Bytes = Vec; + + fn as_bytes(&self) -> Self::Bytes { + bincode::DefaultOptions::new() + .serialize(self) + .expect("serialization to vec doesn't fail") + } +} + +impl FromDisk for sapling::tree::NoteCommitmentTree { + fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { + bincode::DefaultOptions::new() + .deserialize(bytes.as_ref()) + .expect("deserialization format should match the serialization format used by IntoDisk") + } +} + +impl IntoDisk for orchard::tree::NoteCommitmentTree { + type Bytes = Vec; + + fn as_bytes(&self) -> Self::Bytes { + bincode::DefaultOptions::new() + .serialize(self) + .expect("serialization to vec doesn't fail") + } +} + +impl FromDisk for orchard::tree::NoteCommitmentTree { + fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { + bincode::DefaultOptions::new() + .deserialize(bytes.as_ref()) + .expect("deserialization format should match the serialization format used by IntoDisk") + } +} diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshot.rs b/zebra-state/src/service/finalized_state/disk_format/tests/snapshot.rs index 2223f0df..a8cb0241 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshot.rs +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshot.rs @@ -12,6 +12,14 @@ //! If this test fails, run `cargo insta review` to update the test snapshots, //! then commit the `test_*.snap` files using git. //! +//! # Snapshot Format +//! +//! These snapshots use [RON (Rusty Object Notation)](https://github.com/ron-rs/ron#readme), +//! a text format similar to Rust syntax. Raw byte data is encoded in hexadecimal. +//! +//! Due to `serde` limitations, some object types can't be represented exactly, +//! so RON uses the closest equivalent structure. +//! //! # TODO //! //! Test shielded data, and data activated in Overwinter and later network upgrades. diff --git a/zebra-state/src/service/finalized_state/disk_format/transparent.rs b/zebra-state/src/service/finalized_state/disk_format/transparent.rs new file mode 100644 index 00000000..4de232a8 --- /dev/null +++ b/zebra-state/src/service/finalized_state/disk_format/transparent.rs @@ -0,0 +1,53 @@ +//! Transparent transfer serialization formats for finalized data. +//! +//! # Correctness +//! +//! The [`crate::constants::DATABASE_FORMAT_VERSION`] constant must +//! be incremented each time the database format (column, serialization, etc) changes. + +use zebra_chain::{ + block::Height, + serialization::{ZcashDeserializeInto, ZcashSerialize}, + transparent, +}; + +use crate::service::finalized_state::disk_format::{FromDisk, IntoDisk}; + +impl IntoDisk for transparent::Utxo { + type Bytes = Vec; + + fn as_bytes(&self) -> Self::Bytes { + let mut bytes = vec![0; 5]; + bytes[0..4].copy_from_slice(&self.height.0.to_be_bytes()); + bytes[4] = self.from_coinbase as u8; + self.output + .zcash_serialize(&mut bytes) + .expect("serialization to vec doesn't fail"); + bytes + } +} + +impl FromDisk for transparent::Utxo { + fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { + let (meta_bytes, output_bytes) = bytes.as_ref().split_at(5); + let height = Height(u32::from_be_bytes(meta_bytes[0..4].try_into().unwrap())); + let from_coinbase = meta_bytes[4] == 1u8; + let output = output_bytes + .zcash_deserialize_into() + .expect("db has serialized data"); + Self { + output, + height, + from_coinbase, + } + } +} + +impl IntoDisk for transparent::OutPoint { + type Bytes = Vec; + + fn as_bytes(&self) -> Self::Bytes { + self.zcash_serialize_to_vec() + .expect("serialization to vec doesn't fail") + } +} diff --git a/zebra-state/src/service/finalized_state/zebra_db/block/tests/snapshot.rs b/zebra-state/src/service/finalized_state/zebra_db/block/tests/snapshot.rs index a8271656..2357141d 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/block/tests/snapshot.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/block/tests/snapshot.rs @@ -14,6 +14,14 @@ //! //! These tests use fixed test vectors, based on the results of other database queries. //! +//! # Snapshot Format +//! +//! These snapshots use [RON (Rusty Object Notation)](https://github.com/ron-rs/ron#readme), +//! a text format similar to Rust syntax. Raw byte data is encoded in hexadecimal. +//! +//! Due to `serde` limitations, some object types can't be represented exactly, +//! so RON uses the closest equivalent structure. +//! //! # Fixing Test Failures //! //! If this test fails, run `cargo insta review` to update the test snapshots,