diff --git a/zebra-chain/src/amount.rs b/zebra-chain/src/amount.rs index f4a81c14893..b4820907f43 100644 --- a/zebra-chain/src/amount.rs +++ b/zebra-chain/src/amount.rs @@ -47,6 +47,13 @@ pub struct Amount( PhantomData, ); +impl Amount { + /// TODO: Use a u64 for burn amounts instead of an Amount and remove this method + pub fn as_i128(&self) -> i128 { + self.0.into() + } +} + impl fmt::Display for Amount { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let zats = self.zatoshis(); diff --git a/zebra-chain/src/orchard.rs b/zebra-chain/src/orchard.rs index a44aa4ae2ec..6062421cab1 100644 --- a/zebra-chain/src/orchard.rs +++ b/zebra-chain/src/orchard.rs @@ -30,3 +30,5 @@ pub(crate) use shielded_data::ActionCommon; #[cfg(feature = "tx-v6")] pub use orchard_flavor_ext::OrchardZSA; + +pub use orchard::note::AssetBase; diff --git a/zebra-chain/src/orchard/orchard_flavor_ext.rs b/zebra-chain/src/orchard/orchard_flavor_ext.rs index 6ad05abd889..32d887472f4 100644 --- a/zebra-chain/src/orchard/orchard_flavor_ext.rs +++ b/zebra-chain/src/orchard/orchard_flavor_ext.rs @@ -9,7 +9,10 @@ use proptest_derive::Arbitrary; use orchard::{note_encryption::OrchardDomainCommon, orchard_flavor}; -use crate::serialization::{ZcashDeserialize, ZcashSerialize}; +use crate::{ + orchard_zsa, + serialization::{ZcashDeserialize, ZcashSerialize}, +}; #[cfg(feature = "tx-v6")] use crate::orchard_zsa::{Burn, NoBurn}; @@ -50,7 +53,13 @@ pub trait OrchardFlavorExt: Clone + Debug { /// A type representing a burn field for this protocol version. #[cfg(feature = "tx-v6")] - type BurnType: Clone + Debug + Default + ZcashDeserialize + ZcashSerialize + TestArbitrary; + type BurnType: Clone + + Debug + + Default + + ZcashDeserialize + + ZcashSerialize + + TestArbitrary + + AsRef<[orchard_zsa::BurnItem]>; } /// A structure representing a tag for Orchard protocol variant used for the transaction version `V5`. diff --git a/zebra-chain/src/orchard_zsa.rs b/zebra-chain/src/orchard_zsa.rs index be3e29ec0e4..73a96a030da 100644 --- a/zebra-chain/src/orchard_zsa.rs +++ b/zebra-chain/src/orchard_zsa.rs @@ -9,5 +9,5 @@ mod common; mod burn; mod issuance; -pub(crate) use burn::{Burn, NoBurn}; -pub(crate) use issuance::IssueData; +pub use burn::{Burn, BurnItem, NoBurn}; +pub use issuance::{IssueData, Note}; diff --git a/zebra-chain/src/orchard_zsa/burn.rs b/zebra-chain/src/orchard_zsa/burn.rs index 812728b9380..7133129728d 100644 --- a/zebra-chain/src/orchard_zsa/burn.rs +++ b/zebra-chain/src/orchard_zsa/burn.rs @@ -19,9 +19,21 @@ const AMOUNT_SIZE: u64 = 8; const BURN_ITEM_SIZE: u64 = ASSET_BASE_SIZE + AMOUNT_SIZE; /// Orchard ZSA burn item. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct BurnItem(AssetBase, Amount); +impl BurnItem { + /// Returns [`AssetBase`] being burned. + pub fn asset(&self) -> AssetBase { + self.0 + } + + /// Returns [`Amount`] being burned. + pub fn amount(&self) -> Amount { + self.1 + } +} + // Convert from burn item type used in `orchard` crate impl TryFrom<(AssetBase, NoteValue)> for BurnItem { type Error = crate::amount::Error; @@ -105,10 +117,22 @@ impl ZcashDeserialize for NoBurn { } } +impl AsRef<[BurnItem]> for NoBurn { + fn as_ref(&self) -> &[BurnItem] { + &[] + } +} + /// Orchard ZSA burn items (assets intended for burning) #[derive(Default, Clone, Debug, PartialEq, Eq, Serialize)] pub struct Burn(Vec); +impl AsRef<[BurnItem]> for Burn { + fn as_ref(&self) -> &[BurnItem] { + &self.0 + } +} + impl From> for Burn { fn from(inner: Vec) -> Self { Self(inner) diff --git a/zebra-chain/src/orchard_zsa/issuance.rs b/zebra-chain/src/orchard_zsa/issuance.rs index 9f7b4e9faaf..0b521c81acb 100644 --- a/zebra-chain/src/orchard_zsa/issuance.rs +++ b/zebra-chain/src/orchard_zsa/issuance.rs @@ -20,7 +20,7 @@ use orchard::{ note::{ExtractedNoteCommitment, RandomSeed, Rho}, primitives::redpallas::{SigType, Signature, SpendAuth}, value::NoteValue, - Address, Note, + Address, }; use crate::{ @@ -33,6 +33,8 @@ use crate::{ use super::common::ASSET_BASE_SIZE; +pub use orchard::Note; + /// Wrapper for `IssueBundle` used in the context of Transaction V6. This allows the implementation of /// a Serde serializer for unit tests within this crate. #[derive(Clone, Debug, PartialEq, Eq)] @@ -57,6 +59,11 @@ impl IssueData { }) }) } + + /// Returns issue actions + pub fn actions(&self) -> &NonEmpty { + self.0.actions() + } } // Sizes of the serialized values for types in bytes (used for TrustedPreallocate impls) diff --git a/zebra-chain/src/transaction.rs b/zebra-chain/src/transaction.rs index 737253d6eab..624a9e793f5 100644 --- a/zebra-chain/src/transaction.rs +++ b/zebra-chain/src/transaction.rs @@ -1071,6 +1071,56 @@ impl Transaction { } } + /// Access the issuance actions in this transaction, if there are any, + /// regardless of version. + pub fn issue_actions(&self) -> impl Iterator { + let orchard_zsa_issue_data = match self { + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { .. } + | Transaction::V5 { .. } => &None, + + #[cfg(feature = "tx-v6")] + Transaction::V6 { + orchard_zsa_issue_data, + .. + } => orchard_zsa_issue_data, + }; + + orchard_zsa_issue_data + .iter() + .flat_map(orchard_zsa::IssueData::actions) + } + + /// Access the asset burns in this transaction, if there are any, + /// regardless of version. + #[cfg(feature = "tx-v6")] + pub fn burns(&self) -> Vec { + match self { + #[cfg(feature = "tx-v6")] + Transaction::V5 { + orchard_shielded_data: Some(orchard_shielded_data), + .. + } => orchard_shielded_data.burn.as_ref().to_vec(), + #[cfg(feature = "tx-v6")] + Transaction::V6 { + orchard_shielded_data, + .. + } => orchard_shielded_data + .iter() + .flat_map(|data| data.burn.as_ref()) + .copied() + .collect(), + + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { .. } + | Transaction::V5 { .. } => Vec::new(), + } + } + /// Access the [`orchard::Flags`] in this transaction, if there is any, /// regardless of version. pub fn orchard_flags(&self) -> Option { diff --git a/zebra-state/src/service/finalized_state.rs b/zebra-state/src/service/finalized_state.rs index f8c9bade5c1..f604f803835 100644 --- a/zebra-state/src/service/finalized_state.rs +++ b/zebra-state/src/service/finalized_state.rs @@ -46,7 +46,7 @@ pub use column_family::{TypedColumnFamily, WriteTypedBatch}; pub use disk_db::{DiskDb, DiskWriteBatch, ReadDisk, WriteDisk}; #[allow(unused_imports)] pub use disk_format::{ - FromDisk, IntoDisk, OutputIndex, OutputLocation, RawBytes, TransactionIndex, + AssetState, FromDisk, IntoDisk, OutputIndex, OutputLocation, RawBytes, TransactionIndex, TransactionLocation, MAX_ON_DISK_HEIGHT, }; pub use zebra_db::ZebraDb; @@ -91,6 +91,7 @@ pub const STATE_COLUMN_FAMILIES_IN_CODE: &[&str] = &[ "orchard_anchors", "orchard_note_commitment_tree", "orchard_note_commitment_subtree", + "orchard_issued_assets", // Chain "history_tree", "tip_chain_value_pool", diff --git a/zebra-state/src/service/finalized_state/disk_format.rs b/zebra-state/src/service/finalized_state/disk_format.rs index 0ce04431e54..b6090bd64b9 100644 --- a/zebra-state/src/service/finalized_state/disk_format.rs +++ b/zebra-state/src/service/finalized_state/disk_format.rs @@ -20,6 +20,7 @@ pub mod scan; mod tests; pub use block::{TransactionIndex, TransactionLocation, MAX_ON_DISK_HEIGHT}; +pub use shielded::AssetState; pub use transparent::{OutputIndex, OutputLocation}; #[cfg(feature = "shielded-scan")] diff --git a/zebra-state/src/service/finalized_state/disk_format/shielded.rs b/zebra-state/src/service/finalized_state/disk_format/shielded.rs index bcd24d5c604..074509da3a4 100644 --- a/zebra-state/src/service/finalized_state/disk_format/shielded.rs +++ b/zebra-state/src/service/finalized_state/disk_format/shielded.rs @@ -9,7 +9,9 @@ use bincode::Options; use zebra_chain::{ block::Height, - orchard, sapling, sprout, + orchard::{self, AssetBase}, + orchard_zsa::{self, BurnItem}, + sapling, sprout, subtree::{NoteCommitmentSubtreeData, NoteCommitmentSubtreeIndex}, }; @@ -17,6 +19,102 @@ use crate::service::finalized_state::disk_format::{FromDisk, IntoDisk}; use super::block::HEIGHT_DISK_BYTES; +/// The circulating supply and whether that supply has been finalized. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct AssetState { + /// Indicates whether the asset is finalized such that no more of it can be issued. + pub is_finalized: bool, + /// The circulating supply that has been issued for an asset. + pub total_supply: i128, +} + +impl AssetState { + /// Adds partial asset states + pub fn with_change(&self, change: Self) -> Self { + Self { + is_finalized: self.is_finalized || change.is_finalized, + total_supply: self + .total_supply + .checked_add(change.total_supply) + .expect("asset supply sum should not exceed u64 size"), + } + } + + pub fn from_note(is_finalized: bool, note: orchard_zsa::Note) -> (AssetBase, Self) { + ( + note.asset(), + Self { + is_finalized, + total_supply: note.value().inner().into(), + }, + ) + } + + pub fn from_notes( + is_finalized: bool, + notes: &[orchard_zsa::Note], + ) -> impl Iterator + '_ { + notes + .iter() + .map(move |note| Self::from_note(is_finalized, *note)) + } + + pub fn from_burn(burn: BurnItem) -> (AssetBase, Self) { + ( + burn.asset(), + Self { + is_finalized: false, + total_supply: -burn.amount().as_i128(), + }, + ) + } + + pub fn from_burns(burns: Vec) -> impl Iterator { + burns.into_iter().map(Self::from_burn) + } +} + +impl IntoDisk for AssetState { + type Bytes = [u8; 9]; + + fn as_bytes(&self) -> Self::Bytes { + [ + vec![self.is_finalized as u8], + self.total_supply.to_be_bytes().to_vec(), + ] + .concat() + .try_into() + .unwrap() + } +} + +impl FromDisk for AssetState { + fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { + let (&is_finalized_byte, bytes) = bytes.as_ref().split_first().unwrap(); + let (&total_supply_bytes, _bytes) = bytes.split_first_chunk().unwrap(); + + Self { + is_finalized: is_finalized_byte != 0, + total_supply: u64::from_be_bytes(total_supply_bytes).into(), + } + } +} + +impl IntoDisk for orchard::AssetBase { + type Bytes = [u8; 32]; + + fn as_bytes(&self) -> Self::Bytes { + self.to_bytes() + } +} + +impl FromDisk for orchard::AssetBase { + fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { + let (asset_base_bytes, _) = bytes.as_ref().split_first_chunk().unwrap(); + Self::from_bytes(asset_base_bytes).unwrap() + } +} + impl IntoDisk for sprout::Nullifier { type Bytes = [u8; 32]; diff --git a/zebra-state/src/service/finalized_state/zebra_db/block.rs b/zebra-state/src/service/finalized_state/zebra_db/block.rs index 4dc3a801ef3..6f0d2340b91 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/block.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/block.rs @@ -463,7 +463,7 @@ impl DiskWriteBatch { // which is already present from height 1 to the first shielded transaction. // // In Zebra we include the nullifiers and note commitments in the genesis block because it simplifies our code. - self.prepare_shielded_transaction_batch(db, finalized)?; + self.prepare_shielded_transaction_batch(zebra_db, finalized)?; self.prepare_trees_batch(zebra_db, finalized, prev_note_commitment_trees)?; // # Consensus diff --git a/zebra-state/src/service/finalized_state/zebra_db/shielded.rs b/zebra-state/src/service/finalized_state/zebra_db/shielded.rs index 4bba75b1891..c652b13bd93 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/shielded.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/shielded.rs @@ -30,17 +30,34 @@ use crate::{ request::{FinalizedBlock, Treestate}, service::finalized_state::{ disk_db::{DiskDb, DiskWriteBatch, ReadDisk, WriteDisk}, - disk_format::RawBytes, + disk_format::{shielded::AssetState, RawBytes}, zebra_db::ZebraDb, }, - BoxError, + BoxError, TypedColumnFamily, }; // Doc-only items #[allow(unused_imports)] use zebra_chain::subtree::NoteCommitmentSubtree; +/// The name of the chain value pools column family. +/// +/// This constant should be used so the compiler can detect typos. +pub const ISSUED_ASSETS: &str = "orchard_issued_assets"; + +/// The type for reading value pools from the database. +/// +/// This constant should be used so the compiler can detect incorrectly typed accesses to the +/// column family. +pub type IssuedAssetsCf<'cf> = TypedColumnFamily<'cf, orchard::AssetBase, AssetState>; + impl ZebraDb { + /// Returns a typed handle to the `history_tree` column family. + pub(crate) fn issued_assets_cf(&self) -> IssuedAssetsCf { + IssuedAssetsCf::new(&self.db, ISSUED_ASSETS) + .expect("column family was created when database was created") + } + // Read shielded methods /// Returns `true` if the finalized state contains `sprout_nullifier`. @@ -437,16 +454,19 @@ impl DiskWriteBatch { /// - Propagates any errors from updating note commitment trees pub fn prepare_shielded_transaction_batch( &mut self, - db: &DiskDb, + zebra_db: &ZebraDb, finalized: &FinalizedBlock, ) -> Result<(), BoxError> { let FinalizedBlock { block, .. } = finalized; // Index each transaction's shielded data for transaction in &block.transactions { - self.prepare_nullifier_batch(db, transaction)?; + self.prepare_nullifier_batch(&zebra_db.db, transaction)?; } + #[cfg(feature = "tx-v6")] + self.prepare_issued_assets_batch(zebra_db, &block.transactions)?; + Ok(()) } @@ -480,6 +500,57 @@ impl DiskWriteBatch { Ok(()) } + /// Prepare a database batch containing `finalized.block`'s asset issuance + /// and return it (without actually writing anything). + /// + /// # Errors + /// + /// - This method doesn't currently return any errors, but it might in future + #[allow(clippy::unwrap_in_result)] + #[cfg(feature = "tx-v6")] + pub fn prepare_issued_assets_batch( + &mut self, + db: &ZebraDb, + transaction: &[Arc], + ) -> Result<(), BoxError> { + let issued_assets_cf = db.issued_assets_cf(); + let mut batch = db.issued_assets_cf().with_batch_for_writing(self); + + let updated_issued_assets = transaction + .iter() + .map(|tx| (tx.issue_actions(), tx.burns())) + .fold( + HashMap::new(), + |mut issued_assets: HashMap, (actions, burns)| { + for (asset_base, asset_state_change) in actions + .flat_map(|action| { + AssetState::from_notes(action.is_finalized(), action.notes()) + }) + .chain(AssetState::from_burns(burns)) + { + if let Some(asset_state) = issued_assets.get_mut(&asset_base) { + assert!(!asset_state.is_finalized); + *asset_state = asset_state.with_change(asset_state_change); + } else { + let prev_state = issued_assets_cf.zs_get(&asset_base); + let new_state = prev_state.map_or(asset_state_change, |prev| { + prev.with_change(asset_state_change) + }); + issued_assets.insert(asset_base, new_state); + }; + } + + issued_assets + }, + ); + + for (asset_base, updated_issued_asset_state) in updated_issued_assets { + batch = batch.zs_insert(&asset_base, &updated_issued_asset_state); + } + + Ok(()) + } + /// Prepare a database batch containing the note commitment and history tree updates /// from `finalized.block`, and return it (without actually writing anything). ///