2. refactor(db): split the raw disk serialzation format into modules (#3717)

* doc(db): fix some comments

* refactor(db): split disk serialization types into their own module

* refactor(db): split the disk format into modules

* doc(db/test): explain the RON serialization format
This commit is contained in:
teor 2022-03-08 17:59:41 +10:00 committed by GitHub
parent 4a5f0c25ce
commit f1123e0386
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 430 additions and 353 deletions

View File

@ -211,6 +211,10 @@ jobs:
/zebra-state/**/constants.rs /zebra-state/**/constants.rs
/zebra-state/**/finalized_state.rs /zebra-state/**/finalized_state.rs
/zebra-state/**/disk_format.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/**/disk_db.rs
/zebra-state/**/zebra_db.rs /zebra-state/**/zebra_db.rs
/zebra-state/**/zebra_db/block.rs /zebra-state/**/zebra_db/block.rs

View File

@ -5,65 +5,44 @@
//! The [`crate::constants::DATABASE_FORMAT_VERSION`] constant must //! The [`crate::constants::DATABASE_FORMAT_VERSION`] constant must
//! be incremented each time the database format (column, serialization, etc) changes. //! 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; pub mod block;
use serde::{Deserialize, Serialize}; pub mod chain;
pub mod shielded;
use zebra_chain::{ pub mod transparent;
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,
};
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
/// A transaction's location in the chain, by block height and transaction index. pub use block::TransactionLocation;
///
/// 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,
/// The index of the transaction in its block. /// Helper trait for defining the exact format used to interact with disk per
pub index: u32, /// type.
}
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.
pub trait IntoDisk { pub trait IntoDisk {
// The type used to compare a value as a key to other keys stored in a /// The type used to compare a value as a key to other keys stored in a
// database /// database.
type Bytes: AsRef<[u8]>; type Bytes: AsRef<[u8]>;
// function to convert the current type to its disk format in `zs_get()` /// Converts the current type to its disk format in `zs_get()`,
// without necessarily allocating a new IVec /// without necessarily allocating a new ivec.
fn as_bytes(&self) -> Self::Bytes; 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 impl<'a, T> IntoDisk for &'a T
where where
T: IntoDisk, 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<T> FromDisk for Arc<T> impl<T> FromDisk for Arc<T>
where where
T: FromDisk, T: FromDisk,
@ -107,106 +74,6 @@ where
} }
} }
impl IntoDisk for Block {
type Bytes = Vec<u8>;
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 () { impl IntoDisk for () {
type Bytes = [u8; 0]; 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<u8>;
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<u8>;
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<NonNegative> {
type Bytes = [u8; 32];
fn as_bytes(&self) -> Self::Bytes {
self.to_bytes()
}
}
impl FromDisk for ValueBalance<NonNegative> {
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<u8>;
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<u8>;
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<u8>;
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<u32, zcash_history::Entry>,
current_height: Height,
}
impl IntoDisk for NonEmptyHistoryTree {
type Bytes = Vec<u8>;
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")
}
}

View File

@ -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<u8>;
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
}
}

View File

@ -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<NonNegative> {
type Bytes = [u8; 32];
fn as_bytes(&self) -> Self::Bytes {
self.to_bytes()
}
}
impl FromDisk for ValueBalance<NonNegative> {
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<u32, zcash_history::Entry>,
current_height: Height,
}
impl IntoDisk for NonEmptyHistoryTree {
type Bytes = Vec<u8>;
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")
}
}

View File

@ -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<u8>;
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<u8>;
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<u8>;
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")
}
}

View File

@ -12,6 +12,14 @@
//! If this test fails, run `cargo insta review` to update the test snapshots, //! If this test fails, run `cargo insta review` to update the test snapshots,
//! then commit the `test_*.snap` files using git. //! 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 //! # TODO
//! //!
//! Test shielded data, and data activated in Overwinter and later network upgrades. //! Test shielded data, and data activated in Overwinter and later network upgrades.

View File

@ -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<u8>;
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<u8>;
fn as_bytes(&self) -> Self::Bytes {
self.zcash_serialize_to_vec()
.expect("serialization to vec doesn't fail")
}
}

View File

@ -14,6 +14,14 @@
//! //!
//! These tests use fixed test vectors, based on the results of other database queries. //! 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 //! # Fixing Test Failures
//! //!
//! If this test fails, run `cargo insta review` to update the test snapshots, //! If this test fails, run `cargo insta review` to update the test snapshots,