From 1af120ea98b63d227fe6915c9bc4491a6896419a Mon Sep 17 00:00:00 2001 From: Arya Date: Tue, 12 Nov 2024 01:07:00 -0500 Subject: [PATCH 01/13] Defines and implements the issued asset state types --- zebra-chain/src/orchard/orchard_flavor_ext.rs | 13 +- zebra-chain/src/orchard_zsa.rs | 5 +- zebra-chain/src/orchard_zsa/asset_state.rs | 112 ++++++++++++++++++ zebra-chain/src/orchard_zsa/burn.rs | 39 ++++-- zebra-chain/src/orchard_zsa/issuance.rs | 5 + zebra-chain/src/transaction.rs | 64 ++++++++++ zebra-consensus/src/block.rs | 5 + zebra-state/src/arbitrary.rs | 7 ++ zebra-state/src/request.rs | 27 +++++ zebra-state/src/service/chain_tip.rs | 2 + .../zebra_db/block/tests/vectors.rs | 5 + 11 files changed, 274 insertions(+), 10 deletions(-) create mode 100644 zebra-chain/src/orchard_zsa/asset_state.rs 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..cbe93771e67 100644 --- a/zebra-chain/src/orchard_zsa.rs +++ b/zebra-chain/src/orchard_zsa.rs @@ -6,8 +6,11 @@ pub(crate) mod arbitrary; mod common; +mod asset_state; mod burn; mod issuance; -pub(crate) use burn::{Burn, NoBurn}; +pub(crate) use burn::{Burn, BurnItem, NoBurn}; pub(crate) use issuance::IssueData; + +pub use asset_state::{AssetBase, AssetState, AssetStateChange, IssuedAssetsChange}; diff --git a/zebra-chain/src/orchard_zsa/asset_state.rs b/zebra-chain/src/orchard_zsa/asset_state.rs new file mode 100644 index 00000000000..bdcba2528fe --- /dev/null +++ b/zebra-chain/src/orchard_zsa/asset_state.rs @@ -0,0 +1,112 @@ +//! Defines and implements the issued asset state types + +use std::{collections::HashMap, sync::Arc}; + +use orchard::issuance::IssueAction; +pub use orchard::note::AssetBase; + +use crate::block::Block; + +use super::BurnItem; + +/// 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: u128, +} + +/// A change to apply to the issued assets map. +// TODO: Reference ZIP +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct AssetStateChange { + /// Whether the asset should be finalized such that no more of it can be issued. + pub is_finalized: bool, + /// The change in supply from newly issued assets or burned assets. + pub supply_change: i128, +} + +impl AssetStateChange { + fn from_note(is_finalized: bool, note: orchard::Note) -> (AssetBase, Self) { + ( + note.asset(), + Self { + is_finalized, + supply_change: note.value().inner().into(), + }, + ) + } + + fn from_notes( + is_finalized: bool, + notes: &[orchard::Note], + ) -> impl Iterator + '_ { + notes + .iter() + .map(move |note| Self::from_note(is_finalized, *note)) + } + + fn from_issue_actions<'a>( + actions: impl Iterator + 'a, + ) -> impl Iterator + 'a { + actions.flat_map(|action| Self::from_notes(action.is_finalized(), action.notes())) + } + + fn from_burn(burn: &BurnItem) -> (AssetBase, Self) { + ( + burn.asset(), + Self { + is_finalized: false, + supply_change: -i128::from(burn.amount()), + }, + ) + } + + fn from_burns(burns: &[BurnItem]) -> impl Iterator + '_ { + burns.iter().map(Self::from_burn) + } +} + +impl std::ops::AddAssign for AssetStateChange { + fn add_assign(&mut self, rhs: Self) { + self.is_finalized |= rhs.is_finalized; + self.supply_change += rhs.supply_change; + } +} + +/// A map of changes to apply to the issued assets map. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct IssuedAssetsChange(HashMap); + +impl IssuedAssetsChange { + fn new() -> Self { + Self(HashMap::new()) + } + + fn update<'a>(&mut self, changes: impl Iterator + 'a) { + for (asset_base, change) in changes { + *self.0.entry(asset_base).or_default() += change; + } + } + + /// Accepts a reference to an [`Arc`]. + /// + /// Returns a tuple, ([`IssuedAssetsChange`], [`IssuedAssetsChange`]), where + /// the first item is from burns and the second one is for issuance. + pub fn from_block(block: &Arc) -> (Self, Self) { + let mut burn_change = Self::new(); + let mut issuance_change = Self::new(); + + for transaction in &block.transactions { + burn_change.update(AssetStateChange::from_burns(transaction.orchard_burns())); + issuance_change.update(AssetStateChange::from_issue_actions( + transaction.orchard_issue_actions(), + )); + } + + (burn_change, issuance_change) + } +} diff --git a/zebra-chain/src/orchard_zsa/burn.rs b/zebra-chain/src/orchard_zsa/burn.rs index 812728b9380..aea88f619ef 100644 --- a/zebra-chain/src/orchard_zsa/burn.rs +++ b/zebra-chain/src/orchard_zsa/burn.rs @@ -3,7 +3,6 @@ use std::io; use crate::{ - amount::Amount, block::MAX_BLOCK_BYTES, serialization::{SerializationError, TrustedPreallocate, ZcashDeserialize, ZcashSerialize}, }; @@ -19,15 +18,27 @@ const AMOUNT_SIZE: u64 = 8; const BURN_ITEM_SIZE: u64 = ASSET_BASE_SIZE + AMOUNT_SIZE; /// Orchard ZSA burn item. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct BurnItem(AssetBase, Amount); +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct BurnItem(AssetBase, u64); + +impl BurnItem { + /// Returns [`AssetBase`] being burned. + pub fn asset(&self) -> AssetBase { + self.0 + } + + /// Returns [`u64`] representing amount being burned. + pub fn amount(&self) -> u64 { + self.1 + } +} // Convert from burn item type used in `orchard` crate impl TryFrom<(AssetBase, NoteValue)> for BurnItem { type Error = crate::amount::Error; fn try_from(item: (AssetBase, NoteValue)) -> Result { - Ok(Self(item.0, item.1.inner().try_into()?)) + Ok(Self(item.0, item.1.inner())) } } @@ -36,7 +47,7 @@ impl ZcashSerialize for BurnItem { let BurnItem(asset_base, amount) = self; asset_base.zcash_serialize(&mut writer)?; - amount.zcash_serialize(&mut writer)?; + writer.write_all(&amount.to_be_bytes())?; Ok(()) } @@ -44,9 +55,11 @@ impl ZcashSerialize for BurnItem { impl ZcashDeserialize for BurnItem { fn zcash_deserialize(mut reader: R) -> Result { + let mut amount_bytes = [0; 8]; + reader.read_exact(&mut amount_bytes)?; Ok(Self( AssetBase::zcash_deserialize(&mut reader)?, - Amount::zcash_deserialize(&mut reader)?, + u64::from_be_bytes(amount_bytes), )) } } @@ -76,7 +89,7 @@ impl<'de> serde::Deserialize<'de> for BurnItem { D: serde::Deserializer<'de>, { // FIXME: consider another implementation (explicit specifying of [u8; 32] may not look perfect) - let (asset_base_bytes, amount) = <([u8; 32], Amount)>::deserialize(deserializer)?; + let (asset_base_bytes, amount) = <([u8; 32], u64)>::deserialize(deserializer)?; // FIXME: return custom error with a meaningful description? Ok(BurnItem( // FIXME: duplicates the body of AssetBase::zcash_deserialize? @@ -93,6 +106,12 @@ impl<'de> serde::Deserialize<'de> for BurnItem { #[derive(Default, Clone, Debug, PartialEq, Eq, Serialize)] pub struct NoBurn; +impl AsRef<[BurnItem]> for NoBurn { + fn as_ref(&self) -> &[BurnItem] { + &[] + } +} + impl ZcashSerialize for NoBurn { fn zcash_serialize(&self, mut _writer: W) -> Result<(), io::Error> { Ok(()) @@ -115,6 +134,12 @@ impl From> for Burn { } } +impl AsRef<[BurnItem]> for Burn { + fn as_ref(&self) -> &[BurnItem] { + &self.0 + } +} + impl ZcashSerialize for Burn { fn zcash_serialize(&self, writer: W) -> Result<(), io::Error> { self.0.zcash_serialize(writer) diff --git a/zebra-chain/src/orchard_zsa/issuance.rs b/zebra-chain/src/orchard_zsa/issuance.rs index 9f7b4e9faaf..0670419b745 100644 --- a/zebra-chain/src/orchard_zsa/issuance.rs +++ b/zebra-chain/src/orchard_zsa/issuance.rs @@ -57,6 +57,11 @@ impl IssueData { }) }) } + + /// Returns issuance 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..c72002c4b76 100644 --- a/zebra-chain/src/transaction.rs +++ b/zebra-chain/src/transaction.rs @@ -78,6 +78,31 @@ macro_rules! orchard_shielded_data_iter { }; } +macro_rules! orchard_shielded_data_map { + ($self:expr, $mapper:expr, $mapper2:expr) => { + match $self { + Transaction::V5 { + orchard_shielded_data: Some(shielded_data), + .. + } => $mapper(shielded_data), + + #[cfg(feature = "tx-v6")] + Transaction::V6 { + orchard_shielded_data: Some(shielded_data), + .. + } => $mapper2(shielded_data), + + // No Orchard shielded data + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { .. } + | Transaction::V5 { .. } + | Transaction::V6 { .. } => &[], + } + }; +} + // FIXME: doc this // Move down macro_rules! orchard_shielded_data_field { @@ -1071,6 +1096,45 @@ impl Transaction { } } + /// Access the Orchard issue data in this transaction, if any, + /// regardless of version. + #[cfg(feature = "tx-v6")] + fn orchard_issue_data(&self) -> &Option { + match self { + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { .. } + | Transaction::V5 { .. } => &None, + + Transaction::V6 { + orchard_zsa_issue_data, + .. + } => orchard_zsa_issue_data, + } + } + + /// Access the Orchard issuance actions in this transaction, if there are any, + /// regardless of version. + #[cfg(feature = "tx-v6")] + pub fn orchard_issue_actions(&self) -> impl Iterator { + self.orchard_issue_data() + .iter() + .flat_map(orchard_zsa::IssueData::actions) + } + + /// Access the Orchard asset burns in this transaction, if there are any, + /// regardless of version. + #[cfg(feature = "tx-v6")] + pub fn orchard_burns<'a>(&'a self) -> &[orchard_zsa::BurnItem] { + use crate::orchard::{OrchardVanilla, OrchardZSA}; + orchard_shielded_data_map!( + self, + |data: &'a orchard::ShieldedData| data.burn.as_ref(), + |data: &'a orchard::ShieldedData| data.burn.as_ref() + ) + } + /// Access the [`orchard::Flags`] in this transaction, if there is any, /// regardless of version. pub fn orchard_flags(&self) -> Option { diff --git a/zebra-consensus/src/block.rs b/zebra-consensus/src/block.rs index 611aea2ceba..42a1fbddbd6 100644 --- a/zebra-consensus/src/block.rs +++ b/zebra-consensus/src/block.rs @@ -24,6 +24,7 @@ use tracing::Instrument; use zebra_chain::{ amount::Amount, block, + orchard_zsa::IssuedAssetsChange, parameters::{subsidy::FundingStreamReceiver, Network}, transparent, work::equihash, @@ -314,6 +315,8 @@ where let new_outputs = Arc::into_inner(known_utxos) .expect("all verification tasks using known_utxos are complete"); + let (issued_assets_burns_change, issued_assets_issuance_change) = + IssuedAssetsChange::from_block(&block); let prepared_block = zs::SemanticallyVerifiedBlock { block, hash, @@ -321,6 +324,8 @@ where new_outputs, transaction_hashes, deferred_balance: Some(expected_deferred_amount), + issued_assets_burns_change, + issued_assets_issuance_change, }; // Return early for proposal requests when getblocktemplate-rpcs feature is enabled diff --git a/zebra-state/src/arbitrary.rs b/zebra-state/src/arbitrary.rs index 5c0b837566a..37337029391 100644 --- a/zebra-state/src/arbitrary.rs +++ b/zebra-state/src/arbitrary.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use zebra_chain::{ amount::Amount, block::{self, Block}, + orchard_zsa::IssuedAssetsChange, transaction::Transaction, transparent, value_balance::ValueBalance, @@ -30,6 +31,8 @@ impl Prepare for Arc { let transaction_hashes: Arc<[_]> = block.transactions.iter().map(|tx| tx.hash()).collect(); let new_outputs = transparent::new_ordered_outputs_with_height(&block, height, &transaction_hashes); + let (issued_assets_burns_change, issued_assets_issuance_change) = + IssuedAssetsChange::from_block(&block); SemanticallyVerifiedBlock { block, @@ -38,6 +41,8 @@ impl Prepare for Arc { new_outputs, transaction_hashes, deferred_balance: None, + issued_assets_burns_change, + issued_assets_issuance_change, } } } @@ -112,6 +117,8 @@ impl ContextuallyVerifiedBlock { new_outputs, transaction_hashes, deferred_balance: _, + issued_assets_burns_change: _, + issued_assets_issuance_change: _, } = block.into(); Self { diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 56be011d48e..14c9f73d66c 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -11,6 +11,7 @@ use zebra_chain::{ block::{self, Block}, history_tree::HistoryTree, orchard, + orchard_zsa::IssuedAssetsChange, parallel::tree::NoteCommitmentTrees, sapling, serialization::SerializationError, @@ -163,6 +164,12 @@ pub struct SemanticallyVerifiedBlock { pub transaction_hashes: Arc<[transaction::Hash]>, /// This block's contribution to the deferred pool. pub deferred_balance: Option>, + /// A map of burns to be applied to the issued assets map. + // TODO: Reference ZIP. + pub issued_assets_burns_change: IssuedAssetsChange, + /// A map of issuance to be applied to the issued assets map. + // TODO: Reference ZIP. + pub issued_assets_issuance_change: IssuedAssetsChange, } /// A block ready to be committed directly to the finalized state with @@ -392,6 +399,8 @@ impl ContextuallyVerifiedBlock { new_outputs, transaction_hashes, deferred_balance, + issued_assets_burns_change: _, + issued_assets_issuance_change: _, } = semantically_verified; // This is redundant for the non-finalized state, @@ -445,6 +454,8 @@ impl SemanticallyVerifiedBlock { .expect("semantically verified block should have a coinbase height"); let transaction_hashes: Arc<[_]> = block.transactions.iter().map(|tx| tx.hash()).collect(); let new_outputs = transparent::new_ordered_outputs(&block, &transaction_hashes); + let (issued_assets_burns_change, issued_assets_issuance_change) = + IssuedAssetsChange::from_block(&block); Self { block, @@ -453,6 +464,8 @@ impl SemanticallyVerifiedBlock { new_outputs, transaction_hashes, deferred_balance: None, + issued_assets_burns_change, + issued_assets_issuance_change, } } @@ -477,6 +490,8 @@ impl From> for SemanticallyVerifiedBlock { .expect("semantically verified block should have a coinbase height"); let transaction_hashes: Arc<[_]> = block.transactions.iter().map(|tx| tx.hash()).collect(); let new_outputs = transparent::new_ordered_outputs(&block, &transaction_hashes); + let (issued_assets_burns_change, issued_assets_issuance_change) = + IssuedAssetsChange::from_block(&block); Self { block, @@ -485,12 +500,17 @@ impl From> for SemanticallyVerifiedBlock { new_outputs, transaction_hashes, deferred_balance: None, + issued_assets_burns_change, + issued_assets_issuance_change, } } } impl From for SemanticallyVerifiedBlock { fn from(valid: ContextuallyVerifiedBlock) -> Self { + let (issued_assets_burns_change, issued_assets_issuance_change) = + IssuedAssetsChange::from_block(&valid.block); + Self { block: valid.block, hash: valid.hash, @@ -504,12 +524,17 @@ impl From for SemanticallyVerifiedBlock { .constrain::() .expect("deferred balance in a block must me non-negative"), ), + issued_assets_burns_change, + issued_assets_issuance_change, } } } impl From for SemanticallyVerifiedBlock { fn from(finalized: FinalizedBlock) -> Self { + let (issued_assets_burns_change, issued_assets_issuance_change) = + IssuedAssetsChange::from_block(&finalized.block); + Self { block: finalized.block, hash: finalized.hash, @@ -517,6 +542,8 @@ impl From for SemanticallyVerifiedBlock { new_outputs: finalized.new_outputs, transaction_hashes: finalized.transaction_hashes, deferred_balance: finalized.deferred_balance, + issued_assets_burns_change, + issued_assets_issuance_change, } } } diff --git a/zebra-state/src/service/chain_tip.rs b/zebra-state/src/service/chain_tip.rs index 04ea61d6982..af1ec34dc56 100644 --- a/zebra-state/src/service/chain_tip.rs +++ b/zebra-state/src/service/chain_tip.rs @@ -116,6 +116,8 @@ impl From for ChainTipBlock { new_outputs: _, transaction_hashes, deferred_balance: _, + issued_assets_burns_change: _, + issued_assets_issuance_change: _, } = prepared; Self { diff --git a/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs b/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs index 194f2202a87..323e41fd7ed 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs @@ -20,6 +20,7 @@ use zebra_chain::{ }, Block, Height, }, + orchard_zsa::IssuedAssetsChange, parameters::Network::{self, *}, serialization::{ZcashDeserializeInto, ZcashSerialize}, transparent::new_ordered_outputs_with_height, @@ -129,6 +130,8 @@ fn test_block_db_round_trip_with( .collect(); let new_outputs = new_ordered_outputs_with_height(&original_block, Height(0), &transaction_hashes); + let (issued_assets_burns_change, issued_assets_issuance_change) = + IssuedAssetsChange::from_block(&original_block); CheckpointVerifiedBlock(SemanticallyVerifiedBlock { block: original_block.clone(), @@ -137,6 +140,8 @@ fn test_block_db_round_trip_with( new_outputs, transaction_hashes, deferred_balance: None, + issued_assets_burns_change, + issued_assets_issuance_change, }) }; From cc8bc0da97d12cd34bc6987311a05c9a26e23c4c Mon Sep 17 00:00:00 2001 From: Arya Date: Tue, 12 Nov 2024 01:07:20 -0500 Subject: [PATCH 02/13] Adds issued assets to the finalized state --- zebra-chain/src/orchard_zsa.rs | 2 +- zebra-chain/src/orchard_zsa/asset_state.rs | 71 ++++++++++++++- zebra-consensus/src/block.rs | 11 +-- zebra-state/src/arbitrary.rs | 15 ++-- zebra-state/src/lib.rs | 3 +- zebra-state/src/request.rs | 90 ++++++++++++++----- zebra-state/src/service/chain_tip.rs | 3 +- .../finalized_state/disk_format/shielded.rs | 47 +++++++++- .../service/finalized_state/zebra_db/block.rs | 2 +- .../zebra_db/block/tests/vectors.rs | 11 +-- .../finalized_state/zebra_db/shielded.rs | 63 ++++++++++++- 11 files changed, 267 insertions(+), 51 deletions(-) diff --git a/zebra-chain/src/orchard_zsa.rs b/zebra-chain/src/orchard_zsa.rs index cbe93771e67..d012e8de4ca 100644 --- a/zebra-chain/src/orchard_zsa.rs +++ b/zebra-chain/src/orchard_zsa.rs @@ -13,4 +13,4 @@ mod issuance; pub(crate) use burn::{Burn, BurnItem, NoBurn}; pub(crate) use issuance::IssueData; -pub use asset_state::{AssetBase, AssetState, AssetStateChange, IssuedAssetsChange}; +pub use asset_state::{AssetBase, AssetState, AssetStateChange, IssuedAssets, IssuedAssetsChange}; diff --git a/zebra-chain/src/orchard_zsa/asset_state.rs b/zebra-chain/src/orchard_zsa/asset_state.rs index bdcba2528fe..5327f531107 100644 --- a/zebra-chain/src/orchard_zsa/asset_state.rs +++ b/zebra-chain/src/orchard_zsa/asset_state.rs @@ -10,7 +10,7 @@ use crate::block::Block; use super::BurnItem; /// The circulating supply and whether that supply has been finalized. -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Copy, Clone, Debug, Default, 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, @@ -29,6 +29,17 @@ pub struct AssetStateChange { pub supply_change: i128, } +impl AssetState { + fn with_change(mut self, change: AssetStateChange) -> Self { + self.is_finalized |= change.is_finalized; + self.total_supply = self + .total_supply + .checked_add_signed(change.supply_change) + .expect("burn amounts must not be greater than initial supply"); + self + } +} + impl AssetStateChange { fn from_note(is_finalized: bool, note: orchard::Note) -> (AssetBase, Self) { ( @@ -77,6 +88,33 @@ impl std::ops::AddAssign for AssetStateChange { } } +/// An `issued_asset` map +// TODO: Reference ZIP +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct IssuedAssets(HashMap); + +impl IssuedAssets { + fn new() -> Self { + Self(HashMap::new()) + } + + fn update<'a>(&mut self, issued_assets: impl Iterator + 'a) { + for (asset_base, asset_state) in issued_assets { + self.0.insert(asset_base, asset_state); + } + } +} + +impl IntoIterator for IssuedAssets { + type Item = (AssetBase, AssetState); + + type IntoIter = std::collections::hash_map::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + /// A map of changes to apply to the issued assets map. #[derive(Clone, Debug, PartialEq, Eq)] pub struct IssuedAssetsChange(HashMap); @@ -109,4 +147,35 @@ impl IssuedAssetsChange { (burn_change, issuance_change) } + + /// Consumes self and accepts a closure for looking up previous asset states. + /// + /// Applies changes in self to the previous asset state. + /// + /// Returns an [`IssuedAssets`] with the updated asset states. + pub fn apply_with(self, f: impl Fn(AssetBase) -> AssetState) -> IssuedAssets { + let mut issued_assets = IssuedAssets::new(); + + issued_assets.update( + self.0 + .into_iter() + .map(|(asset_base, change)| (asset_base, f(asset_base).with_change(change))), + ); + + issued_assets + } +} + +impl std::ops::Add for IssuedAssetsChange { + type Output = Self; + + fn add(mut self, mut rhs: Self) -> Self { + if self.0.len() > rhs.0.len() { + self.update(rhs.0.into_iter()); + self + } else { + rhs.update(self.0.into_iter()); + rhs + } + } } diff --git a/zebra-consensus/src/block.rs b/zebra-consensus/src/block.rs index 42a1fbddbd6..46ca02e0a08 100644 --- a/zebra-consensus/src/block.rs +++ b/zebra-consensus/src/block.rs @@ -29,7 +29,7 @@ use zebra_chain::{ transparent, work::equihash, }; -use zebra_state as zs; +use zebra_state::{self as zs, IssuedAssetsOrChanges}; use crate::{error::*, transaction as tx, BoxError}; @@ -315,8 +315,7 @@ where let new_outputs = Arc::into_inner(known_utxos) .expect("all verification tasks using known_utxos are complete"); - let (issued_assets_burns_change, issued_assets_issuance_change) = - IssuedAssetsChange::from_block(&block); + let (burns, issuance) = IssuedAssetsChange::from_block(&block); let prepared_block = zs::SemanticallyVerifiedBlock { block, hash, @@ -324,8 +323,10 @@ where new_outputs, transaction_hashes, deferred_balance: Some(expected_deferred_amount), - issued_assets_burns_change, - issued_assets_issuance_change, + issued_assets_changes: IssuedAssetsOrChanges::BurnAndIssuanceChanges { + burns, + issuance, + }, }; // Return early for proposal requests when getblocktemplate-rpcs feature is enabled diff --git a/zebra-state/src/arbitrary.rs b/zebra-state/src/arbitrary.rs index 37337029391..2c7ff4dd166 100644 --- a/zebra-state/src/arbitrary.rs +++ b/zebra-state/src/arbitrary.rs @@ -12,7 +12,8 @@ use zebra_chain::{ }; use crate::{ - request::ContextuallyVerifiedBlock, service::chain_tip::ChainTipBlock, + request::{ContextuallyVerifiedBlock, IssuedAssetsOrChanges}, + service::chain_tip::ChainTipBlock, SemanticallyVerifiedBlock, }; @@ -31,8 +32,7 @@ impl Prepare for Arc { let transaction_hashes: Arc<[_]> = block.transactions.iter().map(|tx| tx.hash()).collect(); let new_outputs = transparent::new_ordered_outputs_with_height(&block, height, &transaction_hashes); - let (issued_assets_burns_change, issued_assets_issuance_change) = - IssuedAssetsChange::from_block(&block); + let (burns, issuance) = IssuedAssetsChange::from_block(&block); SemanticallyVerifiedBlock { block, @@ -41,8 +41,10 @@ impl Prepare for Arc { new_outputs, transaction_hashes, deferred_balance: None, - issued_assets_burns_change, - issued_assets_issuance_change, + issued_assets_changes: IssuedAssetsOrChanges::BurnAndIssuanceChanges { + burns, + issuance, + }, } } } @@ -117,8 +119,7 @@ impl ContextuallyVerifiedBlock { new_outputs, transaction_hashes, deferred_balance: _, - issued_assets_burns_change: _, - issued_assets_issuance_change: _, + issued_assets_changes: _, } = block.into(); Self { diff --git a/zebra-state/src/lib.rs b/zebra-state/src/lib.rs index e93a3b8f905..58010fb648e 100644 --- a/zebra-state/src/lib.rs +++ b/zebra-state/src/lib.rs @@ -42,7 +42,8 @@ pub use error::{ ValidateContextError, }; pub use request::{ - CheckpointVerifiedBlock, HashOrHeight, ReadRequest, Request, SemanticallyVerifiedBlock, + CheckpointVerifiedBlock, HashOrHeight, IssuedAssetsOrChanges, ReadRequest, Request, + SemanticallyVerifiedBlock, }; pub use response::{KnownBlock, MinedTx, ReadResponse, Response}; pub use service::{ diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 14c9f73d66c..7d852dba638 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -11,7 +11,7 @@ use zebra_chain::{ block::{self, Block}, history_tree::HistoryTree, orchard, - orchard_zsa::IssuedAssetsChange, + orchard_zsa::{IssuedAssets, IssuedAssetsChange}, parallel::tree::NoteCommitmentTrees, sapling, serialization::SerializationError, @@ -166,10 +166,7 @@ pub struct SemanticallyVerifiedBlock { pub deferred_balance: Option>, /// A map of burns to be applied to the issued assets map. // TODO: Reference ZIP. - pub issued_assets_burns_change: IssuedAssetsChange, - /// A map of issuance to be applied to the issued assets map. - // TODO: Reference ZIP. - pub issued_assets_issuance_change: IssuedAssetsChange, + pub issued_assets_changes: IssuedAssetsOrChanges, } /// A block ready to be committed directly to the finalized state with @@ -300,6 +297,48 @@ pub struct FinalizedBlock { pub(super) treestate: Treestate, /// This block's contribution to the deferred pool. pub(super) deferred_balance: Option>, + /// Either changes to be applied to the previous `issued_assets` map for the finalized tip, or + /// updates asset states to be inserted into the finalized state, replacing the previous + /// asset states for those asset bases. + pub issued_assets: IssuedAssetsOrChanges, +} + +/// Either changes to be applied to the previous `issued_assets` map for the finalized tip, or +/// updates asset states to be inserted into the finalized state, replacing the previous +/// asset states for those asset bases. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum IssuedAssetsOrChanges { + /// A map of updated issued assets. + State(IssuedAssets), + + /// A map of changes to apply to the issued assets map. + Change(IssuedAssetsChange), + + /// A map of changes from burns and issuance to apply to the issued assets map. + BurnAndIssuanceChanges { + /// A map of changes from burns to apply to the issued assets map. + burns: IssuedAssetsChange, + /// A map of changes from issuance to apply to the issued assets map. + issuance: IssuedAssetsChange, + }, +} + +impl IssuedAssetsOrChanges { + /// Combines fields in the `BurnAndIssuanceChanges` variant then returns a `Change` variant, or + /// returns self unmodified. + pub fn combine(self) -> Self { + let Self::BurnAndIssuanceChanges { burns, issuance } = self else { + return self; + }; + + Self::Change(burns + issuance) + } +} + +impl From for IssuedAssetsOrChanges { + fn from(change: IssuedAssetsChange) -> Self { + Self::Change(change) + } } impl FinalizedBlock { @@ -326,6 +365,7 @@ impl FinalizedBlock { transaction_hashes: block.transaction_hashes, treestate, deferred_balance: block.deferred_balance, + issued_assets: block.issued_assets_changes.combine(), } } } @@ -399,8 +439,7 @@ impl ContextuallyVerifiedBlock { new_outputs, transaction_hashes, deferred_balance, - issued_assets_burns_change: _, - issued_assets_issuance_change: _, + issued_assets_changes: _, } = semantically_verified; // This is redundant for the non-finalized state, @@ -454,8 +493,7 @@ impl SemanticallyVerifiedBlock { .expect("semantically verified block should have a coinbase height"); let transaction_hashes: Arc<[_]> = block.transactions.iter().map(|tx| tx.hash()).collect(); let new_outputs = transparent::new_ordered_outputs(&block, &transaction_hashes); - let (issued_assets_burns_change, issued_assets_issuance_change) = - IssuedAssetsChange::from_block(&block); + let (burns, issuance) = IssuedAssetsChange::from_block(&block); Self { block, @@ -464,8 +502,11 @@ impl SemanticallyVerifiedBlock { new_outputs, transaction_hashes, deferred_balance: None, - issued_assets_burns_change, - issued_assets_issuance_change, + issued_assets_changes: IssuedAssetsOrChanges::BurnAndIssuanceChanges { + burns, + issuance, + } + .combine(), } } @@ -490,8 +531,7 @@ impl From> for SemanticallyVerifiedBlock { .expect("semantically verified block should have a coinbase height"); let transaction_hashes: Arc<[_]> = block.transactions.iter().map(|tx| tx.hash()).collect(); let new_outputs = transparent::new_ordered_outputs(&block, &transaction_hashes); - let (issued_assets_burns_change, issued_assets_issuance_change) = - IssuedAssetsChange::from_block(&block); + let (burns, issuance) = IssuedAssetsChange::from_block(&block); Self { block, @@ -500,16 +540,17 @@ impl From> for SemanticallyVerifiedBlock { new_outputs, transaction_hashes, deferred_balance: None, - issued_assets_burns_change, - issued_assets_issuance_change, + issued_assets_changes: IssuedAssetsOrChanges::BurnAndIssuanceChanges { + burns, + issuance, + }, } } } impl From for SemanticallyVerifiedBlock { fn from(valid: ContextuallyVerifiedBlock) -> Self { - let (issued_assets_burns_change, issued_assets_issuance_change) = - IssuedAssetsChange::from_block(&valid.block); + let (burns, issuance) = IssuedAssetsChange::from_block(&valid.block); Self { block: valid.block, @@ -524,16 +565,17 @@ impl From for SemanticallyVerifiedBlock { .constrain::() .expect("deferred balance in a block must me non-negative"), ), - issued_assets_burns_change, - issued_assets_issuance_change, + issued_assets_changes: IssuedAssetsOrChanges::BurnAndIssuanceChanges { + burns, + issuance, + }, } } } impl From for SemanticallyVerifiedBlock { fn from(finalized: FinalizedBlock) -> Self { - let (issued_assets_burns_change, issued_assets_issuance_change) = - IssuedAssetsChange::from_block(&finalized.block); + let (burns, issuance) = IssuedAssetsChange::from_block(&finalized.block); Self { block: finalized.block, @@ -542,8 +584,10 @@ impl From for SemanticallyVerifiedBlock { new_outputs: finalized.new_outputs, transaction_hashes: finalized.transaction_hashes, deferred_balance: finalized.deferred_balance, - issued_assets_burns_change, - issued_assets_issuance_change, + issued_assets_changes: IssuedAssetsOrChanges::BurnAndIssuanceChanges { + burns, + issuance, + }, } } } diff --git a/zebra-state/src/service/chain_tip.rs b/zebra-state/src/service/chain_tip.rs index af1ec34dc56..8a0ed517766 100644 --- a/zebra-state/src/service/chain_tip.rs +++ b/zebra-state/src/service/chain_tip.rs @@ -116,8 +116,7 @@ impl From for ChainTipBlock { new_outputs: _, transaction_hashes, deferred_balance: _, - issued_assets_burns_change: _, - issued_assets_issuance_change: _, + issued_assets_changes: _, } = prepared; Self { 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..953815cae4c 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, + orchard_zsa::{AssetBase, AssetState}, + sapling, sprout, subtree::{NoteCommitmentSubtreeData, NoteCommitmentSubtreeIndex}, }; @@ -207,3 +209,46 @@ impl FromDisk for NoteCommitmentSubtreeData { ) } } + +// TODO: Replace `.unwrap()`s with `.expect()`s + +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 AssetBase { + type Bytes = [u8; 32]; + + fn as_bytes(&self) -> Self::Bytes { + self.to_bytes() + } +} + +impl FromDisk for 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() + } +} 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/block/tests/vectors.rs b/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs index 323e41fd7ed..45555b969bf 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs @@ -29,7 +29,7 @@ use zebra_test::vectors::{MAINNET_BLOCKS, TESTNET_BLOCKS}; use crate::{ constants::{state_database_format_version_in_code, STATE_DATABASE_KIND}, - request::{FinalizedBlock, Treestate}, + request::{FinalizedBlock, IssuedAssetsOrChanges, Treestate}, service::finalized_state::{disk_db::DiskWriteBatch, ZebraDb, STATE_COLUMN_FAMILIES_IN_CODE}, CheckpointVerifiedBlock, Config, SemanticallyVerifiedBlock, }; @@ -130,8 +130,7 @@ fn test_block_db_round_trip_with( .collect(); let new_outputs = new_ordered_outputs_with_height(&original_block, Height(0), &transaction_hashes); - let (issued_assets_burns_change, issued_assets_issuance_change) = - IssuedAssetsChange::from_block(&original_block); + let (burns, issuance) = IssuedAssetsChange::from_block(&original_block); CheckpointVerifiedBlock(SemanticallyVerifiedBlock { block: original_block.clone(), @@ -140,8 +139,10 @@ fn test_block_db_round_trip_with( new_outputs, transaction_hashes, deferred_balance: None, - issued_assets_burns_change, - issued_assets_issuance_change, + issued_assets_changes: IssuedAssetsOrChanges::BurnAndIssuanceChanges { + burns, + issuance, + }, }) }; 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..02756265999 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/shielded.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/shielded.rs @@ -19,7 +19,8 @@ use std::{ use zebra_chain::{ block::Height, - orchard, + orchard::{self}, + orchard_zsa::{AssetBase, AssetState}, parallel::tree::NoteCommitmentTrees, sapling, sprout, subtree::{NoteCommitmentSubtreeData, NoteCommitmentSubtreeIndex}, @@ -33,14 +34,31 @@ use crate::{ disk_format::RawBytes, zebra_db::ZebraDb, }, - BoxError, + BoxError, IssuedAssetsOrChanges, 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, 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`. @@ -410,6 +428,11 @@ impl ZebraDb { Some(subtree_data.with_index(index)) } + /// Get the orchard issued asset state for the finalized tip. + pub fn issued_asset(&self, asset_base: &AssetBase) -> Option { + self.issued_assets_cf().zs_get(asset_base) + } + /// Returns the shielded note commitment trees of the finalized tip /// or the empty trees if the state is empty. /// Additionally, returns the sapling and orchard subtrees for the finalized tip if @@ -437,16 +460,18 @@ 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)?; } + self.prepare_issued_assets_batch(zebra_db, &finalized.issued_assets)?; + Ok(()) } @@ -480,6 +505,36 @@ 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)] + pub fn prepare_issued_assets_batch( + &mut self, + zebra_db: &ZebraDb, + issued_assets_or_changes: &IssuedAssetsOrChanges, + ) -> Result<(), BoxError> { + let mut batch = zebra_db.issued_assets_cf().with_batch_for_writing(self); + + let updated_issued_assets = match issued_assets_or_changes.clone().combine() { + IssuedAssetsOrChanges::State(issued_assets) => issued_assets, + IssuedAssetsOrChanges::Change(issued_assets_change) => issued_assets_change + .apply_with(|asset_base| zebra_db.issued_asset(&asset_base).unwrap_or_default()), + IssuedAssetsOrChanges::BurnAndIssuanceChanges { .. } => { + panic!("unexpected variant returned from `combine()`") + } + }; + + 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). /// From c7116f33b13db13dd9092be75d334e70f48faca2 Mon Sep 17 00:00:00 2001 From: Arya Date: Tue, 12 Nov 2024 01:56:15 -0500 Subject: [PATCH 03/13] Validates issuance actions and burns before committing blocks to a non-finalized chain. --- zebra-chain/src/orchard_zsa/asset_state.rs | 47 +++++++++++---- zebra-state/src/error.rs | 6 ++ zebra-state/src/request.rs | 2 +- zebra-state/src/service/check.rs | 1 + zebra-state/src/service/check/issuance.rs | 58 +++++++++++++++++++ .../src/service/non_finalized_state.rs | 3 + .../src/service/non_finalized_state/chain.rs | 14 ++++- 7 files changed, 116 insertions(+), 15 deletions(-) create mode 100644 zebra-state/src/service/check/issuance.rs diff --git a/zebra-chain/src/orchard_zsa/asset_state.rs b/zebra-chain/src/orchard_zsa/asset_state.rs index 5327f531107..8255da9cce7 100644 --- a/zebra-chain/src/orchard_zsa/asset_state.rs +++ b/zebra-chain/src/orchard_zsa/asset_state.rs @@ -30,13 +30,22 @@ pub struct AssetStateChange { } impl AssetState { - fn with_change(mut self, change: AssetStateChange) -> Self { + /// Updates and returns self with the provided [`AssetStateChange`] if the change is valid, or + /// returns None otherwise. + pub fn with_change(mut self, change: AssetStateChange) -> Option { + if self.is_finalized { + return None; + } + self.is_finalized |= change.is_finalized; - self.total_supply = self - .total_supply - .checked_add_signed(change.supply_change) - .expect("burn amounts must not be greater than initial supply"); - self + self.total_supply = self.total_supply.checked_add_signed(change.supply_change)?; + Some(self) + } +} + +impl From> for IssuedAssets { + fn from(issued_assets: HashMap) -> Self { + Self(issued_assets) } } @@ -94,7 +103,8 @@ impl std::ops::AddAssign for AssetStateChange { pub struct IssuedAssets(HashMap); impl IssuedAssets { - fn new() -> Self { + /// Creates a new [`IssuedAssets`]. + pub fn new() -> Self { Self(HashMap::new()) } @@ -156,11 +166,14 @@ impl IssuedAssetsChange { pub fn apply_with(self, f: impl Fn(AssetBase) -> AssetState) -> IssuedAssets { let mut issued_assets = IssuedAssets::new(); - issued_assets.update( - self.0 - .into_iter() - .map(|(asset_base, change)| (asset_base, f(asset_base).with_change(change))), - ); + issued_assets.update(self.0.into_iter().map(|(asset_base, change)| { + ( + asset_base, + f(asset_base) + .with_change(change) + .expect("must be valid change"), + ) + })); issued_assets } @@ -179,3 +192,13 @@ impl std::ops::Add for IssuedAssetsChange { } } } + +impl IntoIterator for IssuedAssetsChange { + type Item = (AssetBase, AssetStateChange); + + type IntoIter = std::collections::hash_map::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} diff --git a/zebra-state/src/error.rs b/zebra-state/src/error.rs index cf495311efb..4a20f5c29d1 100644 --- a/zebra-state/src/error.rs +++ b/zebra-state/src/error.rs @@ -264,6 +264,12 @@ pub enum ValidateContextError { tx_index_in_block: Option, transaction_hash: transaction::Hash, }, + + #[error("burn amounts must be less than issued asset supply")] + InvalidBurn, + + #[error("must not issue finalized assets")] + InvalidIssuance, } /// Trait for creating the corresponding duplicate nullifier error from a nullifier. diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 7d852dba638..51fa95917c8 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -365,7 +365,7 @@ impl FinalizedBlock { transaction_hashes: block.transaction_hashes, treestate, deferred_balance: block.deferred_balance, - issued_assets: block.issued_assets_changes.combine(), + issued_assets: block.issued_assets_changes, } } } diff --git a/zebra-state/src/service/check.rs b/zebra-state/src/service/check.rs index ced63bfea16..d2eaeff4e5a 100644 --- a/zebra-state/src/service/check.rs +++ b/zebra-state/src/service/check.rs @@ -28,6 +28,7 @@ use crate::service::non_finalized_state::Chain; pub(crate) mod anchors; pub(crate) mod difficulty; +pub(crate) mod issuance; pub(crate) mod nullifier; pub(crate) mod utxo; diff --git a/zebra-state/src/service/check/issuance.rs b/zebra-state/src/service/check/issuance.rs new file mode 100644 index 00000000000..610825dd2f2 --- /dev/null +++ b/zebra-state/src/service/check/issuance.rs @@ -0,0 +1,58 @@ +//! Checks for issuance and burn validity. + +use std::{collections::HashMap, sync::Arc}; + +use zebra_chain::orchard_zsa::IssuedAssets; + +use crate::{IssuedAssetsOrChanges, SemanticallyVerifiedBlock, ValidateContextError, ZebraDb}; + +use super::Chain; + +pub fn valid_burns_and_issuance( + finalized_state: &ZebraDb, + parent_chain: &Arc, + semantically_verified: &SemanticallyVerifiedBlock, +) -> Result { + let IssuedAssetsOrChanges::BurnAndIssuanceChanges { burns, issuance } = + semantically_verified.issued_assets_changes.clone() + else { + panic!("unexpected variant in semantically verified block") + }; + + let mut issued_assets = HashMap::new(); + + for (asset_base, burn_change) in burns.clone() { + // TODO: Move this to a read fn. + let updated_asset_state = parent_chain + .issued_asset(&asset_base) + .or_else(|| finalized_state.issued_asset(&asset_base)) + .ok_or(ValidateContextError::InvalidBurn)? + .with_change(burn_change) + .ok_or(ValidateContextError::InvalidBurn)?; + + issued_assets + .insert(asset_base, updated_asset_state) + .expect("transactions must have only one burn item per asset base"); + } + + for (asset_base, issuance_change) in issuance.clone() { + // TODO: Move this to a read fn. + let Some(asset_state) = issued_assets + .get(&asset_base) + .copied() + .or_else(|| parent_chain.issued_asset(&asset_base)) + .or_else(|| finalized_state.issued_asset(&asset_base)) + else { + continue; + }; + + let _ = issued_assets.insert( + asset_base, + asset_state + .with_change(issuance_change) + .ok_or(ValidateContextError::InvalidIssuance)?, + ); + } + + Ok(issued_assets.into()) +} diff --git a/zebra-state/src/service/non_finalized_state.rs b/zebra-state/src/service/non_finalized_state.rs index 08d64455024..11ec27be68c 100644 --- a/zebra-state/src/service/non_finalized_state.rs +++ b/zebra-state/src/service/non_finalized_state.rs @@ -325,6 +325,9 @@ impl NonFinalizedState { finalized_state, )?; + let _issued_assets = + check::issuance::valid_burns_and_issuance(finalized_state, &new_chain, &prepared)?; + // Reads from disk check::anchors::block_sapling_orchard_anchors_refer_to_final_treestates( finalized_state, diff --git a/zebra-state/src/service/non_finalized_state/chain.rs b/zebra-state/src/service/non_finalized_state/chain.rs index d0ce3eee904..2af1df7daeb 100644 --- a/zebra-state/src/service/non_finalized_state/chain.rs +++ b/zebra-state/src/service/non_finalized_state/chain.rs @@ -16,13 +16,16 @@ use zebra_chain::{ block::{self, Height}, history_tree::HistoryTree, orchard, + orchard_zsa::{AssetBase, AssetState}, parallel::tree::NoteCommitmentTrees, parameters::Network, primitives::Groth16Proof, sapling, sprout, subtree::{NoteCommitmentSubtree, NoteCommitmentSubtreeData, NoteCommitmentSubtreeIndex}, - transaction::Transaction::*, - transaction::{self, Transaction}, + transaction::{ + self, + Transaction::{self, *}, + }, transparent, value_balance::ValueBalance, work::difficulty::PartialCumulativeWork, @@ -937,6 +940,13 @@ impl Chain { } } + /// Returns the Orchard issued asset state if one is present in + /// the chain for the provided asset base. + pub fn issued_asset(&self, _asset_base: &AssetBase) -> Option { + // self.orchard_issued_assets.get(asset_base).cloned() + None + } + /// Adds the Orchard `tree` to the tree and anchor indexes at `height`. /// /// `height` can be either: From bb62c67ba0d69cd9b8be455cb7c4460a19e69d39 Mon Sep 17 00:00:00 2001 From: Arya Date: Tue, 12 Nov 2024 02:07:43 -0500 Subject: [PATCH 04/13] Adds `issued_assets` fields on `ChainInner` and `ContextuallyValidatedBlock` --- zebra-state/src/arbitrary.rs | 9 +++++++-- zebra-state/src/request.rs | 6 ++++++ zebra-state/src/service/non_finalized_state.rs | 4 +++- zebra-state/src/service/non_finalized_state/chain.rs | 11 ++++++++--- .../src/service/non_finalized_state/tests/prop.rs | 6 ++++-- 5 files changed, 28 insertions(+), 8 deletions(-) diff --git a/zebra-state/src/arbitrary.rs b/zebra-state/src/arbitrary.rs index 2c7ff4dd166..348e8fa6026 100644 --- a/zebra-state/src/arbitrary.rs +++ b/zebra-state/src/arbitrary.rs @@ -103,8 +103,12 @@ impl ContextuallyVerifiedBlock { .map(|outpoint| (outpoint, zero_utxo.clone())) .collect(); - ContextuallyVerifiedBlock::with_block_and_spent_utxos(block, zero_spent_utxos) - .expect("all UTXOs are provided with zero values") + ContextuallyVerifiedBlock::with_block_and_spent_utxos( + block, + zero_spent_utxos, + Default::default(), + ) + .expect("all UTXOs are provided with zero values") } /// Create a [`ContextuallyVerifiedBlock`] from a [`Block`] or [`SemanticallyVerifiedBlock`], @@ -133,6 +137,7 @@ impl ContextuallyVerifiedBlock { spent_outputs: new_outputs, transaction_hashes, chain_value_pool_change: ValueBalance::zero(), + issued_assets: Default::default(), } } } diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 51fa95917c8..71fc6049c50 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -227,6 +227,10 @@ pub struct ContextuallyVerifiedBlock { /// The sum of the chain value pool changes of all transactions in this block. pub(crate) chain_value_pool_change: ValueBalance, + + /// A partial map of `issued_assets` with entries for asset states that were updated in + /// this block. + pub(crate) issued_assets: IssuedAssets, } /// Wraps note commitment trees and the history tree together. @@ -431,6 +435,7 @@ impl ContextuallyVerifiedBlock { pub fn with_block_and_spent_utxos( semantically_verified: SemanticallyVerifiedBlock, mut spent_outputs: HashMap, + issued_assets: IssuedAssets, ) -> Result { let SemanticallyVerifiedBlock { block, @@ -459,6 +464,7 @@ impl ContextuallyVerifiedBlock { &utxos_from_ordered_utxos(spent_outputs), deferred_balance, )?, + issued_assets, }) } } diff --git a/zebra-state/src/service/non_finalized_state.rs b/zebra-state/src/service/non_finalized_state.rs index 11ec27be68c..1ca33cb43f4 100644 --- a/zebra-state/src/service/non_finalized_state.rs +++ b/zebra-state/src/service/non_finalized_state.rs @@ -325,7 +325,7 @@ impl NonFinalizedState { finalized_state, )?; - let _issued_assets = + let issued_assets = check::issuance::valid_burns_and_issuance(finalized_state, &new_chain, &prepared)?; // Reads from disk @@ -346,6 +346,8 @@ impl NonFinalizedState { let contextual = ContextuallyVerifiedBlock::with_block_and_spent_utxos( prepared.clone(), spent_utxos.clone(), + // TODO: Refactor this into repeated `With::with()` calls, see http_request_compatibility module. + issued_assets, ) .map_err(|value_balance_error| { ValidateContextError::CalculateBlockChainValueChange { diff --git a/zebra-state/src/service/non_finalized_state/chain.rs b/zebra-state/src/service/non_finalized_state/chain.rs index 2af1df7daeb..31ee8c027ba 100644 --- a/zebra-state/src/service/non_finalized_state/chain.rs +++ b/zebra-state/src/service/non_finalized_state/chain.rs @@ -177,6 +177,11 @@ pub struct ChainInner { pub(crate) orchard_subtrees: BTreeMap>, + /// A partial map of `issued_assets` with entries for asset states that were updated in + /// this chain. + // TODO: Add reference to ZIP + pub(crate) issued_assets: HashMap, + // Nullifiers // /// The Sprout nullifiers revealed by `blocks`. @@ -240,6 +245,7 @@ impl Chain { orchard_anchors_by_height: Default::default(), orchard_trees_by_height: Default::default(), orchard_subtrees: Default::default(), + issued_assets: Default::default(), sprout_nullifiers: Default::default(), sapling_nullifiers: Default::default(), orchard_nullifiers: Default::default(), @@ -942,9 +948,8 @@ impl Chain { /// Returns the Orchard issued asset state if one is present in /// the chain for the provided asset base. - pub fn issued_asset(&self, _asset_base: &AssetBase) -> Option { - // self.orchard_issued_assets.get(asset_base).cloned() - None + pub fn issued_asset(&self, asset_base: &AssetBase) -> Option { + self.issued_assets.get(asset_base).cloned() } /// Adds the Orchard `tree` to the tree and anchor indexes at `height`. diff --git a/zebra-state/src/service/non_finalized_state/tests/prop.rs b/zebra-state/src/service/non_finalized_state/tests/prop.rs index 2a1adf65c20..16f3ee84f70 100644 --- a/zebra-state/src/service/non_finalized_state/tests/prop.rs +++ b/zebra-state/src/service/non_finalized_state/tests/prop.rs @@ -52,6 +52,7 @@ fn push_genesis_chain() -> Result<()> { ContextuallyVerifiedBlock::with_block_and_spent_utxos( block, only_chain.unspent_utxos(), + Default::default(), ) .map_err(|e| (e, chain_values.clone())) .expect("invalid block value pool change"); @@ -148,6 +149,7 @@ fn forked_equals_pushed_genesis() -> Result<()> { let block = ContextuallyVerifiedBlock::with_block_and_spent_utxos( block, partial_chain.unspent_utxos(), + Default::default() )?; partial_chain = partial_chain .push(block) @@ -167,7 +169,7 @@ fn forked_equals_pushed_genesis() -> Result<()> { for block in chain.iter().cloned() { let block = - ContextuallyVerifiedBlock::with_block_and_spent_utxos(block, full_chain.unspent_utxos())?; + ContextuallyVerifiedBlock::with_block_and_spent_utxos(block, full_chain.unspent_utxos(), Default::default())?; // Check some properties of the genesis block and don't push it to the chain. if block.height == block::Height(0) { @@ -210,7 +212,7 @@ fn forked_equals_pushed_genesis() -> Result<()> { // same original full chain. for block in chain.iter().skip(fork_at_count).cloned() { let block = - ContextuallyVerifiedBlock::with_block_and_spent_utxos(block, forked.unspent_utxos())?; + ContextuallyVerifiedBlock::with_block_and_spent_utxos(block, forked.unspent_utxos(), Default::default())?; forked = forked.push(block).expect("forked chain push is valid"); } From 3d00b812682737f19966543d1ac3011ae82d1bf5 Mon Sep 17 00:00:00 2001 From: Arya Date: Tue, 12 Nov 2024 02:48:15 -0500 Subject: [PATCH 05/13] Adds issued assets map to non-finalized chains --- zebra-chain/src/orchard_zsa/asset_state.rs | 47 +++++++++++++++---- zebra-consensus/src/block.rs | 2 +- zebra-state/src/arbitrary.rs | 2 +- zebra-state/src/request.rs | 20 ++------ zebra-state/src/service/check/issuance.rs | 4 +- .../zebra_db/block/tests/vectors.rs | 3 +- .../finalized_state/zebra_db/shielded.rs | 2 +- .../src/service/non_finalized_state/chain.rs | 40 +++++++++++++++- 8 files changed, 90 insertions(+), 30 deletions(-) diff --git a/zebra-chain/src/orchard_zsa/asset_state.rs b/zebra-chain/src/orchard_zsa/asset_state.rs index 8255da9cce7..7514b478a43 100644 --- a/zebra-chain/src/orchard_zsa/asset_state.rs +++ b/zebra-chain/src/orchard_zsa/asset_state.rs @@ -5,7 +5,7 @@ use std::{collections::HashMap, sync::Arc}; use orchard::issuance::IssueAction; pub use orchard::note::AssetBase; -use crate::block::Block; +use crate::transaction::Transaction; use super::BurnItem; @@ -30,9 +30,9 @@ pub struct AssetStateChange { } impl AssetState { - /// Updates and returns self with the provided [`AssetStateChange`] if the change is valid, or - /// returns None otherwise. - pub fn with_change(mut self, change: AssetStateChange) -> Option { + /// Updates and returns self with the provided [`AssetStateChange`] if + /// the change is valid, or returns None otherwise. + pub fn apply_change(mut self, change: AssetStateChange) -> Option { if self.is_finalized { return None; } @@ -41,6 +41,15 @@ impl AssetState { self.total_supply = self.total_supply.checked_add_signed(change.supply_change)?; Some(self) } + + /// Reverts the provided [`AssetStateChange`]. + pub fn revert_change(&mut self, change: AssetStateChange) { + self.is_finalized &= !change.is_finalized; + self.total_supply = self + .total_supply + .checked_add_signed(-change.supply_change) + .expect("reversions must not overflow"); + } } impl From> for IssuedAssets { @@ -108,6 +117,11 @@ impl IssuedAssets { Self(HashMap::new()) } + /// Returns an iterator of the inner HashMap. + pub fn iter(&self) -> impl Iterator { + self.0.iter() + } + fn update<'a>(&mut self, issued_assets: impl Iterator + 'a) { for (asset_base, asset_state) in issued_assets { self.0.insert(asset_base, asset_state); @@ -140,15 +154,15 @@ impl IssuedAssetsChange { } } - /// Accepts a reference to an [`Arc`]. + /// Accepts a slice of [`Arc`]s. /// /// Returns a tuple, ([`IssuedAssetsChange`], [`IssuedAssetsChange`]), where /// the first item is from burns and the second one is for issuance. - pub fn from_block(block: &Arc) -> (Self, Self) { + pub fn from_transactions(transactions: &[Arc]) -> (Self, Self) { let mut burn_change = Self::new(); let mut issuance_change = Self::new(); - for transaction in &block.transactions { + for transaction in transactions { burn_change.update(AssetStateChange::from_burns(transaction.orchard_burns())); issuance_change.update(AssetStateChange::from_issue_actions( transaction.orchard_issue_actions(), @@ -158,6 +172,23 @@ impl IssuedAssetsChange { (burn_change, issuance_change) } + /// Accepts a slice of [`Arc`]s. + /// + /// Returns an [`IssuedAssetsChange`] representing all of the changes to the issued assets + /// map that should be applied for the provided transactions. + pub fn combined_from_transactions(transactions: &[Arc]) -> Self { + let mut issued_assets_change = Self::new(); + + for transaction in transactions { + issued_assets_change.update(AssetStateChange::from_burns(transaction.orchard_burns())); + issued_assets_change.update(AssetStateChange::from_issue_actions( + transaction.orchard_issue_actions(), + )); + } + + issued_assets_change + } + /// Consumes self and accepts a closure for looking up previous asset states. /// /// Applies changes in self to the previous asset state. @@ -170,7 +201,7 @@ impl IssuedAssetsChange { ( asset_base, f(asset_base) - .with_change(change) + .apply_change(change) .expect("must be valid change"), ) })); diff --git a/zebra-consensus/src/block.rs b/zebra-consensus/src/block.rs index 46ca02e0a08..31fe5a24e9d 100644 --- a/zebra-consensus/src/block.rs +++ b/zebra-consensus/src/block.rs @@ -315,7 +315,7 @@ where let new_outputs = Arc::into_inner(known_utxos) .expect("all verification tasks using known_utxos are complete"); - let (burns, issuance) = IssuedAssetsChange::from_block(&block); + let (burns, issuance) = IssuedAssetsChange::from_transactions(&block.transactions); let prepared_block = zs::SemanticallyVerifiedBlock { block, hash, diff --git a/zebra-state/src/arbitrary.rs b/zebra-state/src/arbitrary.rs index 348e8fa6026..fb92a8fe7d7 100644 --- a/zebra-state/src/arbitrary.rs +++ b/zebra-state/src/arbitrary.rs @@ -32,7 +32,7 @@ impl Prepare for Arc { let transaction_hashes: Arc<[_]> = block.transactions.iter().map(|tx| tx.hash()).collect(); let new_outputs = transparent::new_ordered_outputs_with_height(&block, height, &transaction_hashes); - let (burns, issuance) = IssuedAssetsChange::from_block(&block); + let (burns, issuance) = IssuedAssetsChange::from_transactions(&block.transactions); SemanticallyVerifiedBlock { block, diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 71fc6049c50..92a9d162594 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -313,7 +313,7 @@ pub struct FinalizedBlock { #[derive(Clone, Debug, PartialEq, Eq)] pub enum IssuedAssetsOrChanges { /// A map of updated issued assets. - State(IssuedAssets), + Updated(IssuedAssets), /// A map of changes to apply to the issued assets map. Change(IssuedAssetsChange), @@ -499,7 +499,7 @@ impl SemanticallyVerifiedBlock { .expect("semantically verified block should have a coinbase height"); let transaction_hashes: Arc<[_]> = block.transactions.iter().map(|tx| tx.hash()).collect(); let new_outputs = transparent::new_ordered_outputs(&block, &transaction_hashes); - let (burns, issuance) = IssuedAssetsChange::from_block(&block); + let (burns, issuance) = IssuedAssetsChange::from_transactions(&block.transactions); Self { block, @@ -537,7 +537,7 @@ impl From> for SemanticallyVerifiedBlock { .expect("semantically verified block should have a coinbase height"); let transaction_hashes: Arc<[_]> = block.transactions.iter().map(|tx| tx.hash()).collect(); let new_outputs = transparent::new_ordered_outputs(&block, &transaction_hashes); - let (burns, issuance) = IssuedAssetsChange::from_block(&block); + let (burns, issuance) = IssuedAssetsChange::from_transactions(&block.transactions); Self { block, @@ -556,8 +556,6 @@ impl From> for SemanticallyVerifiedBlock { impl From for SemanticallyVerifiedBlock { fn from(valid: ContextuallyVerifiedBlock) -> Self { - let (burns, issuance) = IssuedAssetsChange::from_block(&valid.block); - Self { block: valid.block, hash: valid.hash, @@ -571,18 +569,13 @@ impl From for SemanticallyVerifiedBlock { .constrain::() .expect("deferred balance in a block must me non-negative"), ), - issued_assets_changes: IssuedAssetsOrChanges::BurnAndIssuanceChanges { - burns, - issuance, - }, + issued_assets_changes: IssuedAssetsOrChanges::Updated(valid.issued_assets), } } } impl From for SemanticallyVerifiedBlock { fn from(finalized: FinalizedBlock) -> Self { - let (burns, issuance) = IssuedAssetsChange::from_block(&finalized.block); - Self { block: finalized.block, hash: finalized.hash, @@ -590,10 +583,7 @@ impl From for SemanticallyVerifiedBlock { new_outputs: finalized.new_outputs, transaction_hashes: finalized.transaction_hashes, deferred_balance: finalized.deferred_balance, - issued_assets_changes: IssuedAssetsOrChanges::BurnAndIssuanceChanges { - burns, - issuance, - }, + issued_assets_changes: finalized.issued_assets, } } } diff --git a/zebra-state/src/service/check/issuance.rs b/zebra-state/src/service/check/issuance.rs index 610825dd2f2..5454e85290f 100644 --- a/zebra-state/src/service/check/issuance.rs +++ b/zebra-state/src/service/check/issuance.rs @@ -27,7 +27,7 @@ pub fn valid_burns_and_issuance( .issued_asset(&asset_base) .or_else(|| finalized_state.issued_asset(&asset_base)) .ok_or(ValidateContextError::InvalidBurn)? - .with_change(burn_change) + .apply_change(burn_change) .ok_or(ValidateContextError::InvalidBurn)?; issued_assets @@ -49,7 +49,7 @@ pub fn valid_burns_and_issuance( let _ = issued_assets.insert( asset_base, asset_state - .with_change(issuance_change) + .apply_change(issuance_change) .ok_or(ValidateContextError::InvalidIssuance)?, ); } diff --git a/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs b/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs index 45555b969bf..7a98ea0e5dd 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs @@ -130,7 +130,8 @@ fn test_block_db_round_trip_with( .collect(); let new_outputs = new_ordered_outputs_with_height(&original_block, Height(0), &transaction_hashes); - let (burns, issuance) = IssuedAssetsChange::from_block(&original_block); + let (burns, issuance) = + IssuedAssetsChange::from_transactions(&original_block.transactions); CheckpointVerifiedBlock(SemanticallyVerifiedBlock { block: original_block.clone(), 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 02756265999..3fc2a57ef3e 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/shielded.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/shielded.rs @@ -520,7 +520,7 @@ impl DiskWriteBatch { let mut batch = zebra_db.issued_assets_cf().with_batch_for_writing(self); let updated_issued_assets = match issued_assets_or_changes.clone().combine() { - IssuedAssetsOrChanges::State(issued_assets) => issued_assets, + IssuedAssetsOrChanges::Updated(issued_assets) => issued_assets, IssuedAssetsOrChanges::Change(issued_assets_change) => issued_assets_change .apply_with(|asset_base| zebra_db.issued_asset(&asset_base).unwrap_or_default()), IssuedAssetsOrChanges::BurnAndIssuanceChanges { .. } => { diff --git a/zebra-state/src/service/non_finalized_state/chain.rs b/zebra-state/src/service/non_finalized_state/chain.rs index 31ee8c027ba..403b29999f2 100644 --- a/zebra-state/src/service/non_finalized_state/chain.rs +++ b/zebra-state/src/service/non_finalized_state/chain.rs @@ -16,7 +16,7 @@ use zebra_chain::{ block::{self, Height}, history_tree::HistoryTree, orchard, - orchard_zsa::{AssetBase, AssetState}, + orchard_zsa::{AssetBase, AssetState, IssuedAssets, IssuedAssetsChange}, parallel::tree::NoteCommitmentTrees, parameters::Network, primitives::Groth16Proof, @@ -952,6 +952,36 @@ impl Chain { self.issued_assets.get(asset_base).cloned() } + /// Remove the History tree index at `height`. + fn revert_issued_assets( + &mut self, + position: RevertPosition, + issued_assets: &IssuedAssets, + transactions: &[Arc], + ) { + if position == RevertPosition::Root { + trace!(?position, "removing unmodified issued assets"); + for (asset_base, &asset_state) in issued_assets.iter() { + if self + .issued_asset(asset_base) + .expect("issued assets for chain should include those in all blocks") + == asset_state + { + self.issued_assets.remove(asset_base); + } + } + } else { + trace!(?position, "reverting changes to issued assets"); + for (asset_base, change) in IssuedAssetsChange::combined_from_transactions(transactions) + { + self.issued_assets + .entry(asset_base) + .or_default() + .revert_change(change); + } + } + } + /// Adds the Orchard `tree` to the tree and anchor indexes at `height`. /// /// `height` can be either: @@ -1454,6 +1484,9 @@ impl Chain { self.add_history_tree(height, history_tree); + self.issued_assets + .extend(contextually_valid.issued_assets.clone()); + Ok(()) } @@ -1682,6 +1715,7 @@ impl UpdateWith for Chain { spent_outputs, transaction_hashes, chain_value_pool_change, + issued_assets, ) = ( contextually_valid.block.as_ref(), contextually_valid.hash, @@ -1690,6 +1724,7 @@ impl UpdateWith for Chain { &contextually_valid.spent_outputs, &contextually_valid.transaction_hashes, &contextually_valid.chain_value_pool_change, + &contextually_valid.issued_assets, ); // remove the blocks hash from `height_by_hash` @@ -1788,6 +1823,9 @@ impl UpdateWith for Chain { // TODO: move this to the history tree UpdateWith.revert...()? self.remove_history_tree(position, height); + // revert the issued assets map, if needed + self.revert_issued_assets(position, issued_assets, &block.transactions); + // revert the chain value pool balances, if needed self.revert_chain_with(chain_value_pool_change, position); } From 2daf84f6a6a74b28048f5f80327d59b2f58e2d51 Mon Sep 17 00:00:00 2001 From: Arya Date: Tue, 12 Nov 2024 03:24:04 -0500 Subject: [PATCH 06/13] adds new column family to list of state column families --- zebra-chain/src/orchard_zsa/asset_state.rs | 5 ++++- zebra-state/src/service/finalized_state.rs | 1 + .../disk_format/tests/snapshots/column_family_names.snap | 1 + .../tests/snapshots/empty_column_families@mainnet_0.snap | 1 + .../tests/snapshots/empty_column_families@mainnet_1.snap | 1 + .../tests/snapshots/empty_column_families@mainnet_2.snap | 1 + .../tests/snapshots/empty_column_families@no_blocks.snap | 1 + .../tests/snapshots/empty_column_families@testnet_0.snap | 1 + .../tests/snapshots/empty_column_families@testnet_1.snap | 1 + .../tests/snapshots/empty_column_families@testnet_2.snap | 1 + 10 files changed, 13 insertions(+), 1 deletion(-) diff --git a/zebra-chain/src/orchard_zsa/asset_state.rs b/zebra-chain/src/orchard_zsa/asset_state.rs index 7514b478a43..e7413ad7604 100644 --- a/zebra-chain/src/orchard_zsa/asset_state.rs +++ b/zebra-chain/src/orchard_zsa/asset_state.rs @@ -20,7 +20,10 @@ pub struct AssetState { } /// A change to apply to the issued assets map. -// TODO: Reference ZIP +// TODO: +// - Reference ZIP +// - Make this an enum of _either_ a finalization _or_ a supply change +// (applying the finalize flag for each issuance note will cause unexpected panics). #[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct AssetStateChange { /// Whether the asset should be finalized such that no more of it can be issued. diff --git a/zebra-state/src/service/finalized_state.rs b/zebra-state/src/service/finalized_state.rs index f8c9bade5c1..94328d9e51f 100644 --- a/zebra-state/src/service/finalized_state.rs +++ b/zebra-state/src/service/finalized_state.rs @@ -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/tests/snapshots/column_family_names.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/column_family_names.snap index d37e037cac7..33f1c76717b 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/column_family_names.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/column_family_names.snap @@ -12,6 +12,7 @@ expression: cf_names "height_by_hash", "history_tree", "orchard_anchors", + "orchard_issued_assets", "orchard_note_commitment_subtree", "orchard_note_commitment_tree", "orchard_nullifiers", diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_0.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_0.snap index 3c333a9fc43..abd4ae001ec 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_0.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_0.snap @@ -5,6 +5,7 @@ expression: empty_column_families [ "balance_by_transparent_addr: no entries", "history_tree: no entries", + "orchard_issued_assets: no entries", "orchard_note_commitment_subtree: no entries", "orchard_nullifiers: no entries", "sapling_note_commitment_subtree: no entries", diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_1.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_1.snap index cb8ac5f6aed..8b114ddce4d 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_1.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_1.snap @@ -4,6 +4,7 @@ expression: empty_column_families --- [ "history_tree: no entries", + "orchard_issued_assets: no entries", "orchard_note_commitment_subtree: no entries", "orchard_nullifiers: no entries", "sapling_note_commitment_subtree: no entries", diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_2.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_2.snap index cb8ac5f6aed..8b114ddce4d 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_2.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_2.snap @@ -4,6 +4,7 @@ expression: empty_column_families --- [ "history_tree: no entries", + "orchard_issued_assets: no entries", "orchard_note_commitment_subtree: no entries", "orchard_nullifiers: no entries", "sapling_note_commitment_subtree: no entries", diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@no_blocks.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@no_blocks.snap index a2abce2083b..2d119139d26 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@no_blocks.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@no_blocks.snap @@ -11,6 +11,7 @@ expression: empty_column_families "height_by_hash: no entries", "history_tree: no entries", "orchard_anchors: no entries", + "orchard_issued_assets: no entries", "orchard_note_commitment_subtree: no entries", "orchard_note_commitment_tree: no entries", "orchard_nullifiers: no entries", diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_0.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_0.snap index 3c333a9fc43..abd4ae001ec 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_0.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_0.snap @@ -5,6 +5,7 @@ expression: empty_column_families [ "balance_by_transparent_addr: no entries", "history_tree: no entries", + "orchard_issued_assets: no entries", "orchard_note_commitment_subtree: no entries", "orchard_nullifiers: no entries", "sapling_note_commitment_subtree: no entries", diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_1.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_1.snap index cb8ac5f6aed..8b114ddce4d 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_1.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_1.snap @@ -4,6 +4,7 @@ expression: empty_column_families --- [ "history_tree: no entries", + "orchard_issued_assets: no entries", "orchard_note_commitment_subtree: no entries", "orchard_nullifiers: no entries", "sapling_note_commitment_subtree: no entries", diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_2.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_2.snap index cb8ac5f6aed..8b114ddce4d 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_2.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_2.snap @@ -4,6 +4,7 @@ expression: empty_column_families --- [ "history_tree: no entries", + "orchard_issued_assets: no entries", "orchard_note_commitment_subtree: no entries", "orchard_nullifiers: no entries", "sapling_note_commitment_subtree: no entries", From c6c099b20cdf67947b3d0b84567872ad6fe8a3f1 Mon Sep 17 00:00:00 2001 From: Arya Date: Wed, 13 Nov 2024 21:45:23 -0500 Subject: [PATCH 07/13] Updates AssetState, AssetStateChange, IssuedAssetsOrChange, & SemanticallyVerifiedBlock types, updates `IssuedAssetsChange::from_transactions()` method return type --- zebra-chain/src/orchard_zsa/asset_state.rs | 218 ++++++++++++------ zebra-consensus/src/block.rs | 11 +- zebra-consensus/src/checkpoint.rs | 5 +- zebra-consensus/src/error.rs | 3 + zebra-state/src/arbitrary.rs | 12 +- zebra-state/src/lib.rs | 2 +- zebra-state/src/request.rs | 97 ++++---- zebra-state/src/service/chain_tip.rs | 2 +- zebra-state/src/service/check/issuance.rs | 51 ++-- .../finalized_state/disk_format/shielded.rs | 2 +- .../zebra_db/block/tests/vectors.rs | 12 +- .../finalized_state/zebra_db/shielded.rs | 13 +- .../src/service/non_finalized_state/chain.rs | 3 +- 13 files changed, 242 insertions(+), 189 deletions(-) diff --git a/zebra-chain/src/orchard_zsa/asset_state.rs b/zebra-chain/src/orchard_zsa/asset_state.rs index e7413ad7604..d355c1d0825 100644 --- a/zebra-chain/src/orchard_zsa/asset_state.rs +++ b/zebra-chain/src/orchard_zsa/asset_state.rs @@ -1,6 +1,9 @@ //! Defines and implements the issued asset state types -use std::{collections::HashMap, sync::Arc}; +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; use orchard::issuance::IssueAction; pub use orchard::note::AssetBase; @@ -16,42 +19,108 @@ pub struct AssetState { pub is_finalized: bool, /// The circulating supply that has been issued for an asset. - pub total_supply: u128, + pub total_supply: u64, } /// A change to apply to the issued assets map. -// TODO: -// - Reference ZIP -// - Make this an enum of _either_ a finalization _or_ a supply change -// (applying the finalize flag for each issuance note will cause unexpected panics). +// TODO: Reference ZIP #[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct AssetStateChange { /// Whether the asset should be finalized such that no more of it can be issued. pub is_finalized: bool, - /// The change in supply from newly issued assets or burned assets. - pub supply_change: i128, + /// The change in supply from newly issued assets or burned assets, if any. + pub supply_change: SupplyChange, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +/// An asset supply change to apply to the issued assets map. +pub enum SupplyChange { + Issuance(u64), + Burn(u64), +} + +impl Default for SupplyChange { + fn default() -> Self { + Self::Issuance(0) + } +} + +impl SupplyChange { + fn apply_to(self, total_supply: u64) -> Option { + match self { + SupplyChange::Issuance(amount) => total_supply.checked_add(amount), + SupplyChange::Burn(amount) => total_supply.checked_sub(amount), + } + } + + fn as_i128(self) -> i128 { + match self { + SupplyChange::Issuance(amount) => i128::from(amount), + SupplyChange::Burn(amount) => -i128::from(amount), + } + } + + fn add(&mut self, rhs: Self) -> bool { + if let Some(result) = self + .as_i128() + .checked_add(rhs.as_i128()) + .and_then(|signed| match signed { + 0.. => signed.try_into().ok().map(Self::Issuance), + ..0 => signed.try_into().ok().map(Self::Burn), + }) + { + *self = result; + true + } else { + false + } + } +} + +impl std::ops::Neg for SupplyChange { + type Output = Self; + + fn neg(self) -> Self::Output { + match self { + Self::Issuance(amount) => Self::Burn(amount), + Self::Burn(amount) => Self::Issuance(amount), + } + } } impl AssetState { /// Updates and returns self with the provided [`AssetStateChange`] if /// the change is valid, or returns None otherwise. - pub fn apply_change(mut self, change: AssetStateChange) -> Option { + pub fn apply_change(self, change: AssetStateChange) -> Option { + self.apply_finalization(change.is_finalized)? + .apply_supply_change(change.supply_change) + } + + fn apply_finalization(mut self, is_finalized: bool) -> Option { if self.is_finalized { - return None; + None + } else { + self.is_finalized = is_finalized; + Some(self) } + } - self.is_finalized |= change.is_finalized; - self.total_supply = self.total_supply.checked_add_signed(change.supply_change)?; + fn apply_supply_change(mut self, supply_change: SupplyChange) -> Option { + self.total_supply = supply_change.apply_to(self.total_supply)?; Some(self) } /// Reverts the provided [`AssetStateChange`]. pub fn revert_change(&mut self, change: AssetStateChange) { - self.is_finalized &= !change.is_finalized; - self.total_supply = self - .total_supply - .checked_add_signed(-change.supply_change) - .expect("reversions must not overflow"); + *self = self + .revert_finalization(change.is_finalized) + .apply_supply_change(-change.supply_change) + .expect("reverted change should be validated"); + } + + fn revert_finalization(mut self, is_finalized: bool) -> Self { + self.is_finalized &= !is_finalized; + self } } @@ -62,50 +131,79 @@ impl From> for IssuedAssets { } impl AssetStateChange { - fn from_note(is_finalized: bool, note: orchard::Note) -> (AssetBase, Self) { + fn new( + asset_base: AssetBase, + supply_change: SupplyChange, + is_finalized: bool, + ) -> (AssetBase, Self) { ( - note.asset(), + asset_base, Self { is_finalized, - supply_change: note.value().inner().into(), + supply_change, }, ) } - fn from_notes( - is_finalized: bool, - notes: &[orchard::Note], - ) -> impl Iterator + '_ { - notes - .iter() - .map(move |note| Self::from_note(is_finalized, *note)) + fn from_transaction(tx: &Arc) -> impl Iterator + '_ { + Self::from_burns(tx.orchard_burns()) + .chain(Self::from_issue_actions(tx.orchard_issue_actions())) } fn from_issue_actions<'a>( actions: impl Iterator + 'a, ) -> impl Iterator + 'a { - actions.flat_map(|action| Self::from_notes(action.is_finalized(), action.notes())) + actions.flat_map(Self::from_issue_action) } - fn from_burn(burn: &BurnItem) -> (AssetBase, Self) { - ( - burn.asset(), - Self { - is_finalized: false, - supply_change: -i128::from(burn.amount()), - }, + fn from_issue_action(action: &IssueAction) -> impl Iterator + '_ { + let supply_changes = Self::from_notes(action.notes()); + let finalize_changes = action + .is_finalized() + .then(|| { + action + .notes() + .iter() + .map(orchard::Note::asset) + .collect::>() + }) + .unwrap_or_default() + .into_iter() + .map(|asset_base| Self::new(asset_base, SupplyChange::Issuance(0), true)); + + supply_changes.chain(finalize_changes) + } + + fn from_notes(notes: &[orchard::Note]) -> impl Iterator + '_ { + notes.iter().copied().map(Self::from_note) + } + + fn from_note(note: orchard::Note) -> (AssetBase, Self) { + Self::new( + note.asset(), + SupplyChange::Issuance(note.value().inner()), + false, ) } fn from_burns(burns: &[BurnItem]) -> impl Iterator + '_ { burns.iter().map(Self::from_burn) } -} -impl std::ops::AddAssign for AssetStateChange { - fn add_assign(&mut self, rhs: Self) { - self.is_finalized |= rhs.is_finalized; - self.supply_change += rhs.supply_change; + fn from_burn(burn: &BurnItem) -> (AssetBase, Self) { + Self::new(burn.asset(), SupplyChange::Burn(burn.amount()), false) + } + + /// Updates and returns self with the provided [`AssetStateChange`] if + /// the change is valid, or returns None otherwise. + pub fn apply_change(&mut self, change: AssetStateChange) -> bool { + self.is_finalized |= change.is_finalized; + self.supply_change.add(change.supply_change) + } + + /// Returns true if the AssetStateChange is for an asset burn. + pub fn is_burn(&self) -> bool { + matches!(self.supply_change, SupplyChange::Burn(_)) } } @@ -143,7 +241,7 @@ impl IntoIterator for IssuedAssets { } /// A map of changes to apply to the issued assets map. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct IssuedAssetsChange(HashMap); impl IssuedAssetsChange { @@ -151,45 +249,33 @@ impl IssuedAssetsChange { Self(HashMap::new()) } - fn update<'a>(&mut self, changes: impl Iterator + 'a) { + fn update<'a>( + &mut self, + changes: impl Iterator + 'a, + ) -> bool { for (asset_base, change) in changes { - *self.0.entry(asset_base).or_default() += change; - } - } - - /// Accepts a slice of [`Arc`]s. - /// - /// Returns a tuple, ([`IssuedAssetsChange`], [`IssuedAssetsChange`]), where - /// the first item is from burns and the second one is for issuance. - pub fn from_transactions(transactions: &[Arc]) -> (Self, Self) { - let mut burn_change = Self::new(); - let mut issuance_change = Self::new(); - - for transaction in transactions { - burn_change.update(AssetStateChange::from_burns(transaction.orchard_burns())); - issuance_change.update(AssetStateChange::from_issue_actions( - transaction.orchard_issue_actions(), - )); + if !self.0.entry(asset_base).or_default().apply_change(change) { + return false; + } } - (burn_change, issuance_change) + true } /// Accepts a slice of [`Arc`]s. /// /// Returns an [`IssuedAssetsChange`] representing all of the changes to the issued assets /// map that should be applied for the provided transactions. - pub fn combined_from_transactions(transactions: &[Arc]) -> Self { + pub fn from_transactions(transactions: &[Arc]) -> Option { let mut issued_assets_change = Self::new(); for transaction in transactions { - issued_assets_change.update(AssetStateChange::from_burns(transaction.orchard_burns())); - issued_assets_change.update(AssetStateChange::from_issue_actions( - transaction.orchard_issue_actions(), - )); + if !issued_assets_change.update(AssetStateChange::from_transaction(transaction)) { + return None; + } } - issued_assets_change + Some(issued_assets_change) } /// Consumes self and accepts a closure for looking up previous asset states. diff --git a/zebra-consensus/src/block.rs b/zebra-consensus/src/block.rs index 31fe5a24e9d..6728bd9be66 100644 --- a/zebra-consensus/src/block.rs +++ b/zebra-consensus/src/block.rs @@ -29,7 +29,7 @@ use zebra_chain::{ transparent, work::equihash, }; -use zebra_state::{self as zs, IssuedAssetsOrChanges}; +use zebra_state as zs; use crate::{error::*, transaction as tx, BoxError}; @@ -315,7 +315,9 @@ where let new_outputs = Arc::into_inner(known_utxos) .expect("all verification tasks using known_utxos are complete"); - let (burns, issuance) = IssuedAssetsChange::from_transactions(&block.transactions); + let issued_assets_change = IssuedAssetsChange::from_transactions(&block.transactions) + .ok_or(TransactionError::InvalidAssetIssuanceOrBurn)?; + let prepared_block = zs::SemanticallyVerifiedBlock { block, hash, @@ -323,10 +325,7 @@ where new_outputs, transaction_hashes, deferred_balance: Some(expected_deferred_amount), - issued_assets_changes: IssuedAssetsOrChanges::BurnAndIssuanceChanges { - burns, - issuance, - }, + issued_assets_change: Some(issued_assets_change), }; // Return early for proposal requests when getblocktemplate-rpcs feature is enabled diff --git a/zebra-consensus/src/checkpoint.rs b/zebra-consensus/src/checkpoint.rs index 039ea6e33e3..f6520ba5564 100644 --- a/zebra-consensus/src/checkpoint.rs +++ b/zebra-consensus/src/checkpoint.rs @@ -42,7 +42,7 @@ use crate::{ Progress::{self, *}, TargetHeight::{self, *}, }, - error::{BlockError, SubsidyError}, + error::{BlockError, SubsidyError, TransactionError}, funding_stream_values, BoxError, ParameterCheckpoint as _, }; @@ -619,7 +619,8 @@ where }; // don't do precalculation until the block passes basic difficulty checks - let block = CheckpointVerifiedBlock::new(block, Some(hash), expected_deferred_amount); + let block = CheckpointVerifiedBlock::new(block, Some(hash), expected_deferred_amount) + .ok_or_else(|| VerifyBlockError::from(TransactionError::InvalidAssetIssuanceOrBurn))?; crate::block::check::merkle_root_validity( &self.network, diff --git a/zebra-consensus/src/error.rs b/zebra-consensus/src/error.rs index 8fe14c62d52..9aa41103910 100644 --- a/zebra-consensus/src/error.rs +++ b/zebra-consensus/src/error.rs @@ -239,6 +239,9 @@ pub enum TransactionError { #[error("failed to verify ZIP-317 transaction rules, transaction was not inserted to mempool")] #[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))] Zip317(#[from] zebra_chain::transaction::zip317::Error), + + #[error("failed to validate asset issuance and/or burns")] + InvalidAssetIssuanceOrBurn, } impl From for TransactionError { diff --git a/zebra-state/src/arbitrary.rs b/zebra-state/src/arbitrary.rs index fb92a8fe7d7..c5ec8ef3a82 100644 --- a/zebra-state/src/arbitrary.rs +++ b/zebra-state/src/arbitrary.rs @@ -5,15 +5,13 @@ use std::sync::Arc; use zebra_chain::{ amount::Amount, block::{self, Block}, - orchard_zsa::IssuedAssetsChange, transaction::Transaction, transparent, value_balance::ValueBalance, }; use crate::{ - request::{ContextuallyVerifiedBlock, IssuedAssetsOrChanges}, - service::chain_tip::ChainTipBlock, + request::ContextuallyVerifiedBlock, service::chain_tip::ChainTipBlock, SemanticallyVerifiedBlock, }; @@ -32,7 +30,6 @@ impl Prepare for Arc { let transaction_hashes: Arc<[_]> = block.transactions.iter().map(|tx| tx.hash()).collect(); let new_outputs = transparent::new_ordered_outputs_with_height(&block, height, &transaction_hashes); - let (burns, issuance) = IssuedAssetsChange::from_transactions(&block.transactions); SemanticallyVerifiedBlock { block, @@ -41,10 +38,7 @@ impl Prepare for Arc { new_outputs, transaction_hashes, deferred_balance: None, - issued_assets_changes: IssuedAssetsOrChanges::BurnAndIssuanceChanges { - burns, - issuance, - }, + issued_assets_change: None, } } } @@ -123,7 +117,7 @@ impl ContextuallyVerifiedBlock { new_outputs, transaction_hashes, deferred_balance: _, - issued_assets_changes: _, + issued_assets_change: _, } = block.into(); Self { diff --git a/zebra-state/src/lib.rs b/zebra-state/src/lib.rs index 58010fb648e..7cfc8304bdd 100644 --- a/zebra-state/src/lib.rs +++ b/zebra-state/src/lib.rs @@ -42,7 +42,7 @@ pub use error::{ ValidateContextError, }; pub use request::{ - CheckpointVerifiedBlock, HashOrHeight, IssuedAssetsOrChanges, ReadRequest, Request, + CheckpointVerifiedBlock, HashOrHeight, IssuedAssetsOrChange, ReadRequest, Request, SemanticallyVerifiedBlock, }; pub use response::{KnownBlock, MinedTx, ReadResponse, Response}; diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 92a9d162594..3c0e3834278 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -166,7 +166,7 @@ pub struct SemanticallyVerifiedBlock { pub deferred_balance: Option>, /// A map of burns to be applied to the issued assets map. // TODO: Reference ZIP. - pub issued_assets_changes: IssuedAssetsOrChanges, + pub issued_assets_change: Option, } /// A block ready to be committed directly to the finalized state with @@ -304,51 +304,47 @@ pub struct FinalizedBlock { /// Either changes to be applied to the previous `issued_assets` map for the finalized tip, or /// updates asset states to be inserted into the finalized state, replacing the previous /// asset states for those asset bases. - pub issued_assets: IssuedAssetsOrChanges, + pub issued_assets: IssuedAssetsOrChange, } /// Either changes to be applied to the previous `issued_assets` map for the finalized tip, or /// updates asset states to be inserted into the finalized state, replacing the previous /// asset states for those asset bases. #[derive(Clone, Debug, PartialEq, Eq)] -pub enum IssuedAssetsOrChanges { +pub enum IssuedAssetsOrChange { /// A map of updated issued assets. Updated(IssuedAssets), /// A map of changes to apply to the issued assets map. Change(IssuedAssetsChange), - - /// A map of changes from burns and issuance to apply to the issued assets map. - BurnAndIssuanceChanges { - /// A map of changes from burns to apply to the issued assets map. - burns: IssuedAssetsChange, - /// A map of changes from issuance to apply to the issued assets map. - issuance: IssuedAssetsChange, - }, } -impl IssuedAssetsOrChanges { - /// Combines fields in the `BurnAndIssuanceChanges` variant then returns a `Change` variant, or - /// returns self unmodified. - pub fn combine(self) -> Self { - let Self::BurnAndIssuanceChanges { burns, issuance } = self else { - return self; - }; - - Self::Change(burns + issuance) +impl From for IssuedAssetsOrChange { + fn from(change: IssuedAssetsChange) -> Self { + Self::Change(change) } } -impl From for IssuedAssetsOrChanges { - fn from(change: IssuedAssetsChange) -> Self { - Self::Change(change) +impl From for IssuedAssetsOrChange { + fn from(updated_issued_assets: IssuedAssets) -> Self { + Self::Updated(updated_issued_assets) } } impl FinalizedBlock { /// Constructs [`FinalizedBlock`] from [`CheckpointVerifiedBlock`] and its [`Treestate`]. pub fn from_checkpoint_verified(block: CheckpointVerifiedBlock, treestate: Treestate) -> Self { - Self::from_semantically_verified(SemanticallyVerifiedBlock::from(block), treestate) + let issued_assets = block + .issued_assets_change + .clone() + .expect("checkpoint verified block should have issued assets change") + .into(); + + Self::from_semantically_verified( + SemanticallyVerifiedBlock::from(block), + treestate, + issued_assets, + ) } /// Constructs [`FinalizedBlock`] from [`ContextuallyVerifiedBlock`] and its [`Treestate`]. @@ -356,11 +352,20 @@ impl FinalizedBlock { block: ContextuallyVerifiedBlock, treestate: Treestate, ) -> Self { - Self::from_semantically_verified(SemanticallyVerifiedBlock::from(block), treestate) + let issued_assets = block.issued_assets.clone().into(); + Self::from_semantically_verified( + SemanticallyVerifiedBlock::from(block), + treestate, + issued_assets, + ) } /// Constructs [`FinalizedBlock`] from [`SemanticallyVerifiedBlock`] and its [`Treestate`]. - fn from_semantically_verified(block: SemanticallyVerifiedBlock, treestate: Treestate) -> Self { + fn from_semantically_verified( + block: SemanticallyVerifiedBlock, + treestate: Treestate, + issued_assets: IssuedAssetsOrChange, + ) -> Self { Self { block: block.block, hash: block.hash, @@ -369,7 +374,7 @@ impl FinalizedBlock { transaction_hashes: block.transaction_hashes, treestate, deferred_balance: block.deferred_balance, - issued_assets: block.issued_assets_changes, + issued_assets, } } } @@ -444,7 +449,7 @@ impl ContextuallyVerifiedBlock { new_outputs, transaction_hashes, deferred_balance, - issued_assets_changes: _, + issued_assets_change: _, } = semantically_verified; // This is redundant for the non-finalized state, @@ -476,11 +481,14 @@ impl CheckpointVerifiedBlock { block: Arc, hash: Option, deferred_balance: Option>, - ) -> Self { + ) -> Option { + let issued_assets_change = IssuedAssetsChange::from_transactions(&block.transactions)?; let mut block = Self::with_hash(block.clone(), hash.unwrap_or(block.hash())); block.deferred_balance = deferred_balance; - block + block.issued_assets_change = Some(issued_assets_change); + Some(block) } + /// Creates a block that's ready to be committed to the finalized state, /// using a precalculated [`block::Hash`]. /// @@ -499,7 +507,6 @@ impl SemanticallyVerifiedBlock { .expect("semantically verified block should have a coinbase height"); let transaction_hashes: Arc<[_]> = block.transactions.iter().map(|tx| tx.hash()).collect(); let new_outputs = transparent::new_ordered_outputs(&block, &transaction_hashes); - let (burns, issuance) = IssuedAssetsChange::from_transactions(&block.transactions); Self { block, @@ -508,11 +515,7 @@ impl SemanticallyVerifiedBlock { new_outputs, transaction_hashes, deferred_balance: None, - issued_assets_changes: IssuedAssetsOrChanges::BurnAndIssuanceChanges { - burns, - issuance, - } - .combine(), + issued_assets_change: None, } } @@ -537,7 +540,6 @@ impl From> for SemanticallyVerifiedBlock { .expect("semantically verified block should have a coinbase height"); let transaction_hashes: Arc<[_]> = block.transactions.iter().map(|tx| tx.hash()).collect(); let new_outputs = transparent::new_ordered_outputs(&block, &transaction_hashes); - let (burns, issuance) = IssuedAssetsChange::from_transactions(&block.transactions); Self { block, @@ -546,10 +548,7 @@ impl From> for SemanticallyVerifiedBlock { new_outputs, transaction_hashes, deferred_balance: None, - issued_assets_changes: IssuedAssetsOrChanges::BurnAndIssuanceChanges { - burns, - issuance, - }, + issued_assets_change: None, } } } @@ -569,21 +568,7 @@ impl From for SemanticallyVerifiedBlock { .constrain::() .expect("deferred balance in a block must me non-negative"), ), - issued_assets_changes: IssuedAssetsOrChanges::Updated(valid.issued_assets), - } - } -} - -impl From for SemanticallyVerifiedBlock { - fn from(finalized: FinalizedBlock) -> Self { - Self { - block: finalized.block, - hash: finalized.hash, - height: finalized.height, - new_outputs: finalized.new_outputs, - transaction_hashes: finalized.transaction_hashes, - deferred_balance: finalized.deferred_balance, - issued_assets_changes: finalized.issued_assets, + issued_assets_change: None, } } } diff --git a/zebra-state/src/service/chain_tip.rs b/zebra-state/src/service/chain_tip.rs index 8a0ed517766..95306b54282 100644 --- a/zebra-state/src/service/chain_tip.rs +++ b/zebra-state/src/service/chain_tip.rs @@ -116,7 +116,7 @@ impl From for ChainTipBlock { new_outputs: _, transaction_hashes, deferred_balance: _, - issued_assets_changes: _, + issued_assets_change: _, } = prepared; Self { diff --git a/zebra-state/src/service/check/issuance.rs b/zebra-state/src/service/check/issuance.rs index 5454e85290f..daf7635e68a 100644 --- a/zebra-state/src/service/check/issuance.rs +++ b/zebra-state/src/service/check/issuance.rs @@ -4,7 +4,7 @@ use std::{collections::HashMap, sync::Arc}; use zebra_chain::orchard_zsa::IssuedAssets; -use crate::{IssuedAssetsOrChanges, SemanticallyVerifiedBlock, ValidateContextError, ZebraDb}; +use crate::{SemanticallyVerifiedBlock, ValidateContextError, ZebraDb}; use super::Chain; @@ -13,45 +13,34 @@ pub fn valid_burns_and_issuance( parent_chain: &Arc, semantically_verified: &SemanticallyVerifiedBlock, ) -> Result { - let IssuedAssetsOrChanges::BurnAndIssuanceChanges { burns, issuance } = - semantically_verified.issued_assets_changes.clone() - else { - panic!("unexpected variant in semantically verified block") + let Some(issued_assets_change) = semantically_verified.issued_assets_change.clone() else { + return Ok(IssuedAssets::default()); }; let mut issued_assets = HashMap::new(); - for (asset_base, burn_change) in burns.clone() { - // TODO: Move this to a read fn. - let updated_asset_state = parent_chain - .issued_asset(&asset_base) - .or_else(|| finalized_state.issued_asset(&asset_base)) - .ok_or(ValidateContextError::InvalidBurn)? - .apply_change(burn_change) - .ok_or(ValidateContextError::InvalidBurn)?; - - issued_assets - .insert(asset_base, updated_asset_state) - .expect("transactions must have only one burn item per asset base"); - } - - for (asset_base, issuance_change) in issuance.clone() { - // TODO: Move this to a read fn. - let Some(asset_state) = issued_assets + for (asset_base, change) in issued_assets_change { + let asset_state = issued_assets .get(&asset_base) .copied() .or_else(|| parent_chain.issued_asset(&asset_base)) - .or_else(|| finalized_state.issued_asset(&asset_base)) - else { - continue; - }; + .or_else(|| finalized_state.issued_asset(&asset_base)); - let _ = issued_assets.insert( - asset_base, + let updated_asset_state = if change.is_burn() { asset_state - .apply_change(issuance_change) - .ok_or(ValidateContextError::InvalidIssuance)?, - ); + .ok_or(ValidateContextError::InvalidBurn)? + .apply_change(change) + .ok_or(ValidateContextError::InvalidBurn)? + } else { + asset_state + .unwrap_or_default() + .apply_change(change) + .ok_or(ValidateContextError::InvalidIssuance)? + }; + + issued_assets + .insert(asset_base, updated_asset_state) + .expect("transactions must have only one burn item per asset base"); } Ok(issued_assets.into()) 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 953815cae4c..cb2844d4c08 100644 --- a/zebra-state/src/service/finalized_state/disk_format/shielded.rs +++ b/zebra-state/src/service/finalized_state/disk_format/shielded.rs @@ -233,7 +233,7 @@ impl FromDisk for AssetState { Self { is_finalized: is_finalized_byte != 0, - total_supply: u64::from_be_bytes(total_supply_bytes).into(), + total_supply: u64::from_be_bytes(total_supply_bytes), } } } diff --git a/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs b/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs index 7a98ea0e5dd..7b35af87a75 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs @@ -29,7 +29,7 @@ use zebra_test::vectors::{MAINNET_BLOCKS, TESTNET_BLOCKS}; use crate::{ constants::{state_database_format_version_in_code, STATE_DATABASE_KIND}, - request::{FinalizedBlock, IssuedAssetsOrChanges, Treestate}, + request::{FinalizedBlock, Treestate}, service::finalized_state::{disk_db::DiskWriteBatch, ZebraDb, STATE_COLUMN_FAMILIES_IN_CODE}, CheckpointVerifiedBlock, Config, SemanticallyVerifiedBlock, }; @@ -130,8 +130,9 @@ fn test_block_db_round_trip_with( .collect(); let new_outputs = new_ordered_outputs_with_height(&original_block, Height(0), &transaction_hashes); - let (burns, issuance) = - IssuedAssetsChange::from_transactions(&original_block.transactions); + let issued_assets_change = + IssuedAssetsChange::from_transactions(&original_block.transactions) + .expect("issued assets should be valid"); CheckpointVerifiedBlock(SemanticallyVerifiedBlock { block: original_block.clone(), @@ -140,10 +141,7 @@ fn test_block_db_round_trip_with( new_outputs, transaction_hashes, deferred_balance: None, - issued_assets_changes: IssuedAssetsOrChanges::BurnAndIssuanceChanges { - burns, - issuance, - }, + issued_assets_change: Some(issued_assets_change), }) }; 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 3fc2a57ef3e..1ca7e9cd3dc 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/shielded.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/shielded.rs @@ -34,7 +34,7 @@ use crate::{ disk_format::RawBytes, zebra_db::ZebraDb, }, - BoxError, IssuedAssetsOrChanges, TypedColumnFamily, + BoxError, IssuedAssetsOrChange, TypedColumnFamily, }; // Doc-only items @@ -515,17 +515,14 @@ impl DiskWriteBatch { pub fn prepare_issued_assets_batch( &mut self, zebra_db: &ZebraDb, - issued_assets_or_changes: &IssuedAssetsOrChanges, + issued_assets_or_changes: &IssuedAssetsOrChange, ) -> Result<(), BoxError> { let mut batch = zebra_db.issued_assets_cf().with_batch_for_writing(self); - let updated_issued_assets = match issued_assets_or_changes.clone().combine() { - IssuedAssetsOrChanges::Updated(issued_assets) => issued_assets, - IssuedAssetsOrChanges::Change(issued_assets_change) => issued_assets_change + let updated_issued_assets = match issued_assets_or_changes.clone() { + IssuedAssetsOrChange::Updated(issued_assets) => issued_assets, + IssuedAssetsOrChange::Change(issued_assets_change) => issued_assets_change .apply_with(|asset_base| zebra_db.issued_asset(&asset_base).unwrap_or_default()), - IssuedAssetsOrChanges::BurnAndIssuanceChanges { .. } => { - panic!("unexpected variant returned from `combine()`") - } }; for (asset_base, updated_issued_asset_state) in updated_issued_assets { diff --git a/zebra-state/src/service/non_finalized_state/chain.rs b/zebra-state/src/service/non_finalized_state/chain.rs index 403b29999f2..638ecd1ae70 100644 --- a/zebra-state/src/service/non_finalized_state/chain.rs +++ b/zebra-state/src/service/non_finalized_state/chain.rs @@ -972,7 +972,8 @@ impl Chain { } } else { trace!(?position, "reverting changes to issued assets"); - for (asset_base, change) in IssuedAssetsChange::combined_from_transactions(transactions) + for (asset_base, change) in IssuedAssetsChange::from_transactions(transactions) + .expect("blocks in chain state must be valid") { self.issued_assets .entry(asset_base) From 9e0e043175359e98ee5bf8007a6eae97bf41b417 Mon Sep 17 00:00:00 2001 From: Arya Date: Wed, 13 Nov 2024 22:17:01 -0500 Subject: [PATCH 08/13] Fixes tests by computing an `IssuedAssetsChange` for conversions to CheckpointVerifiedBlock --- zebra-rpc/src/sync.rs | 6 +++--- zebra-state/src/arbitrary.rs | 4 +++- zebra-state/src/request.rs | 6 +++++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/zebra-rpc/src/sync.rs b/zebra-rpc/src/sync.rs index fd323ef64bb..787b2e7c5a8 100644 --- a/zebra-rpc/src/sync.rs +++ b/zebra-rpc/src/sync.rs @@ -13,8 +13,8 @@ use zebra_chain::{ }; use zebra_node_services::rpc_client::RpcRequestClient; use zebra_state::{ - spawn_init_read_only, ChainTipBlock, ChainTipChange, ChainTipSender, CheckpointVerifiedBlock, - LatestChainTip, NonFinalizedState, ReadStateService, SemanticallyVerifiedBlock, ZebraDb, + spawn_init_read_only, ChainTipBlock, ChainTipChange, ChainTipSender, LatestChainTip, + NonFinalizedState, ReadStateService, SemanticallyVerifiedBlock, ZebraDb, MAX_BLOCK_REORG_HEIGHT, }; @@ -262,7 +262,7 @@ impl TrustedChainSync { tokio::task::spawn_blocking(move || { let (height, hash) = db.tip()?; db.block(height.into()) - .map(|block| CheckpointVerifiedBlock::with_hash(block, hash)) + .map(|block| SemanticallyVerifiedBlock::with_hash(block, hash)) .map(ChainTipBlock::from) }) .wait_for_panics() diff --git a/zebra-state/src/arbitrary.rs b/zebra-state/src/arbitrary.rs index c5ec8ef3a82..2c1efef3753 100644 --- a/zebra-state/src/arbitrary.rs +++ b/zebra-state/src/arbitrary.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use zebra_chain::{ amount::Amount, block::{self, Block}, + orchard_zsa::IssuedAssetsChange, transaction::Transaction, transparent, value_balance::ValueBalance, @@ -30,6 +31,7 @@ impl Prepare for Arc { let transaction_hashes: Arc<[_]> = block.transactions.iter().map(|tx| tx.hash()).collect(); let new_outputs = transparent::new_ordered_outputs_with_height(&block, height, &transaction_hashes); + let issued_assets_change = IssuedAssetsChange::from_transactions(&block.transactions); SemanticallyVerifiedBlock { block, @@ -38,7 +40,7 @@ impl Prepare for Arc { new_outputs, transaction_hashes, deferred_balance: None, - issued_assets_change: None, + issued_assets_change, } } } diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 3c0e3834278..bdce59adf0f 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -528,7 +528,11 @@ impl SemanticallyVerifiedBlock { impl From> for CheckpointVerifiedBlock { fn from(block: Arc) -> Self { - CheckpointVerifiedBlock(SemanticallyVerifiedBlock::from(block)) + let mut block = SemanticallyVerifiedBlock::from(block); + block.issued_assets_change = + IssuedAssetsChange::from_transactions(&block.block.transactions); + + CheckpointVerifiedBlock(block) } } From 8f26a891516a559c655b2c798da303d07c2f0788 Mon Sep 17 00:00:00 2001 From: Arya Date: Wed, 13 Nov 2024 22:58:10 -0500 Subject: [PATCH 09/13] fixes finalization checks --- zebra-chain/src/orchard_zsa/asset_state.rs | 28 +++++++++++++++------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/zebra-chain/src/orchard_zsa/asset_state.rs b/zebra-chain/src/orchard_zsa/asset_state.rs index d355c1d0825..49e5191dcc2 100644 --- a/zebra-chain/src/orchard_zsa/asset_state.rs +++ b/zebra-chain/src/orchard_zsa/asset_state.rs @@ -92,21 +92,20 @@ impl AssetState { /// Updates and returns self with the provided [`AssetStateChange`] if /// the change is valid, or returns None otherwise. pub fn apply_change(self, change: AssetStateChange) -> Option { - self.apply_finalization(change.is_finalized)? - .apply_supply_change(change.supply_change) + self.apply_finalization(change)?.apply_supply_change(change) } - fn apply_finalization(mut self, is_finalized: bool) -> Option { - if self.is_finalized { + fn apply_finalization(mut self, change: AssetStateChange) -> Option { + if self.is_finalized && change.is_issuance() { None } else { - self.is_finalized = is_finalized; + self.is_finalized |= change.is_finalized; Some(self) } } - fn apply_supply_change(mut self, supply_change: SupplyChange) -> Option { - self.total_supply = supply_change.apply_to(self.total_supply)?; + fn apply_supply_change(mut self, change: AssetStateChange) -> Option { + self.total_supply = change.supply_change.apply_to(self.total_supply)?; Some(self) } @@ -114,7 +113,7 @@ impl AssetState { pub fn revert_change(&mut self, change: AssetStateChange) { *self = self .revert_finalization(change.is_finalized) - .apply_supply_change(-change.supply_change) + .revert_supply_change(change) .expect("reverted change should be validated"); } @@ -122,6 +121,11 @@ impl AssetState { self.is_finalized &= !is_finalized; self } + + fn revert_supply_change(mut self, change: AssetStateChange) -> Option { + self.total_supply = (-change.supply_change).apply_to(self.total_supply)?; + Some(self) + } } impl From> for IssuedAssets { @@ -197,6 +201,9 @@ impl AssetStateChange { /// Updates and returns self with the provided [`AssetStateChange`] if /// the change is valid, or returns None otherwise. pub fn apply_change(&mut self, change: AssetStateChange) -> bool { + if self.is_finalized && change.is_issuance() { + return false; + } self.is_finalized |= change.is_finalized; self.supply_change.add(change.supply_change) } @@ -205,6 +212,11 @@ impl AssetStateChange { pub fn is_burn(&self) -> bool { matches!(self.supply_change, SupplyChange::Burn(_)) } + + /// Returns true if the AssetStateChange is for an asset burn. + pub fn is_issuance(&self) -> bool { + matches!(self.supply_change, SupplyChange::Issuance(_)) + } } /// An `issued_asset` map From e063729bcdb55ed368e7fe7c3a654b9206e9da6b Mon Sep 17 00:00:00 2001 From: Arya Date: Fri, 15 Nov 2024 17:52:49 -0500 Subject: [PATCH 10/13] Adds documentation to types and methods in `asset_state` module, fixes several bugs. --- zebra-chain/src/orchard_zsa/asset_state.rs | 122 ++++++++++++------ zebra-consensus/src/block.rs | 36 +++--- zebra-consensus/src/transaction.rs | 6 + zebra-state/src/arbitrary.rs | 7 +- zebra-state/src/request.rs | 40 +++--- zebra-state/src/service/chain_tip.rs | 2 +- zebra-state/src/service/check/issuance.rs | 70 ++++++---- .../zebra_db/block/tests/vectors.rs | 4 +- .../src/service/non_finalized_state/chain.rs | 14 +- 9 files changed, 185 insertions(+), 116 deletions(-) diff --git a/zebra-chain/src/orchard_zsa/asset_state.rs b/zebra-chain/src/orchard_zsa/asset_state.rs index 49e5191dcc2..e8ebdf57109 100644 --- a/zebra-chain/src/orchard_zsa/asset_state.rs +++ b/zebra-chain/src/orchard_zsa/asset_state.rs @@ -27,7 +27,9 @@ pub struct AssetState { #[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct AssetStateChange { /// Whether the asset should be finalized such that no more of it can be issued. - pub is_finalized: bool, + pub should_finalize: bool, + /// Whether the asset should be finalized such that no more of it can be issued. + pub includes_issuance: bool, /// The change in supply from newly issued assets or burned assets, if any. pub supply_change: SupplyChange, } @@ -35,7 +37,10 @@ pub struct AssetStateChange { #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] /// An asset supply change to apply to the issued assets map. pub enum SupplyChange { + /// An issuance that should increase the total supply of an asset Issuance(u64), + + /// A burn that should reduce the total supply of an asset. Burn(u64), } @@ -46,6 +51,9 @@ impl Default for SupplyChange { } impl SupplyChange { + /// Applies `self` to a provided `total_supply` of an asset. + /// + /// Returns the updated total supply after the [`SupplyChange`] has been applied. fn apply_to(self, total_supply: u64) -> Option { match self { SupplyChange::Issuance(amount) => total_supply.checked_add(amount), @@ -53,6 +61,8 @@ impl SupplyChange { } } + /// Returns the [`SupplyChange`] amount as an [`i128`] where burned amounts + /// are negative. fn as_i128(self) -> i128 { match self { SupplyChange::Issuance(amount) => i128::from(amount), @@ -60,11 +70,16 @@ impl SupplyChange { } } + /// Attempts to add another supply change to `self`. + /// + /// Returns true if successful or false if the result would be invalid. fn add(&mut self, rhs: Self) -> bool { if let Some(result) = self .as_i128() .checked_add(rhs.as_i128()) .and_then(|signed| match signed { + // Burn amounts MUST not be 0 + // TODO: Reference ZIP 0.. => signed.try_into().ok().map(Self::Issuance), ..0 => signed.try_into().ok().map(Self::Burn), }) @@ -75,6 +90,11 @@ impl SupplyChange { false } } + + /// Returns true if this [`SupplyChange`] is an issuance. + pub fn is_issuance(&self) -> bool { + matches!(self, SupplyChange::Issuance(_)) + } } impl std::ops::Neg for SupplyChange { @@ -95,15 +115,19 @@ impl AssetState { self.apply_finalization(change)?.apply_supply_change(change) } + /// Updates the `is_finalized` field on `self` if the change is valid and + /// returns `self`, or returns None otherwise. fn apply_finalization(mut self, change: AssetStateChange) -> Option { - if self.is_finalized && change.is_issuance() { + if self.is_finalized && change.includes_issuance { None } else { - self.is_finalized |= change.is_finalized; + self.is_finalized |= change.should_finalize; Some(self) } } + /// Updates the `supply_change` field on `self` if the change is valid and + /// returns `self`, or returns None otherwise. fn apply_supply_change(mut self, change: AssetStateChange) -> Option { self.total_supply = change.supply_change.apply_to(self.total_supply)?; Some(self) @@ -112,16 +136,18 @@ impl AssetState { /// Reverts the provided [`AssetStateChange`]. pub fn revert_change(&mut self, change: AssetStateChange) { *self = self - .revert_finalization(change.is_finalized) + .revert_finalization(change.should_finalize) .revert_supply_change(change) .expect("reverted change should be validated"); } - fn revert_finalization(mut self, is_finalized: bool) -> Self { - self.is_finalized &= !is_finalized; + /// Reverts the changes to `is_finalized` from the provied [`AssetStateChange`]. + fn revert_finalization(mut self, should_finalize: bool) -> Self { + self.is_finalized &= !should_finalize; self } + /// Reverts the changes to `supply_change` from the provied [`AssetStateChange`]. fn revert_supply_change(mut self, change: AssetStateChange) -> Option { self.total_supply = (-change.supply_change).apply_to(self.total_supply)?; Some(self) @@ -135,31 +161,40 @@ impl From> for IssuedAssets { } impl AssetStateChange { + /// Creates a new [`AssetStateChange`] from an asset base, supply change, and + /// `should_finalize` flag. fn new( asset_base: AssetBase, supply_change: SupplyChange, - is_finalized: bool, + should_finalize: bool, ) -> (AssetBase, Self) { ( asset_base, Self { - is_finalized, + should_finalize, + includes_issuance: supply_change.is_issuance(), supply_change, }, ) } + /// Accepts a transaction and returns an iterator of asset bases and issued asset state changes + /// that should be applied to those asset bases when committing the transaction to the chain state. fn from_transaction(tx: &Arc) -> impl Iterator + '_ { Self::from_burns(tx.orchard_burns()) .chain(Self::from_issue_actions(tx.orchard_issue_actions())) } + /// Accepts an iterator of [`IssueAction`]s and returns an iterator of asset bases and issued asset state changes + /// that should be applied to those asset bases when committing the provided issue actions to the chain state. fn from_issue_actions<'a>( actions: impl Iterator + 'a, ) -> impl Iterator + 'a { actions.flat_map(Self::from_issue_action) } + /// Accepts an [`IssueAction`] and returns an iterator of asset bases and issued asset state changes + /// that should be applied to those asset bases when committing the provided issue action to the chain state. fn from_issue_action(action: &IssueAction) -> impl Iterator + '_ { let supply_changes = Self::from_notes(action.notes()); let finalize_changes = action @@ -178,10 +213,14 @@ impl AssetStateChange { supply_changes.chain(finalize_changes) } + /// Accepts an iterator of [`orchard::Note`]s and returns an iterator of asset bases and issued asset state changes + /// that should be applied to those asset bases when committing the provided orchard notes to the chain state. fn from_notes(notes: &[orchard::Note]) -> impl Iterator + '_ { notes.iter().copied().map(Self::from_note) } + /// Accepts an [`orchard::Note`] and returns an iterator of asset bases and issued asset state changes + /// that should be applied to those asset bases when committing the provided orchard note to the chain state. fn from_note(note: orchard::Note) -> (AssetBase, Self) { Self::new( note.asset(), @@ -190,10 +229,14 @@ impl AssetStateChange { ) } + /// Accepts an iterator of [`BurnItem`]s and returns an iterator of asset bases and issued asset state changes + /// that should be applied to those asset bases when committing the provided asset burns to the chain state. fn from_burns(burns: &[BurnItem]) -> impl Iterator + '_ { burns.iter().map(Self::from_burn) } + /// Accepts an [`BurnItem`] and returns an iterator of asset bases and issued asset state changes + /// that should be applied to those asset bases when committing the provided burn to the chain state. fn from_burn(burn: &BurnItem) -> (AssetBase, Self) { Self::new(burn.asset(), SupplyChange::Burn(burn.amount()), false) } @@ -201,25 +244,16 @@ impl AssetStateChange { /// Updates and returns self with the provided [`AssetStateChange`] if /// the change is valid, or returns None otherwise. pub fn apply_change(&mut self, change: AssetStateChange) -> bool { - if self.is_finalized && change.is_issuance() { + if self.should_finalize && change.includes_issuance { return false; } - self.is_finalized |= change.is_finalized; + self.should_finalize |= change.should_finalize; + self.includes_issuance |= change.includes_issuance; self.supply_change.add(change.supply_change) } - - /// Returns true if the AssetStateChange is for an asset burn. - pub fn is_burn(&self) -> bool { - matches!(self.supply_change, SupplyChange::Burn(_)) - } - - /// Returns true if the AssetStateChange is for an asset burn. - pub fn is_issuance(&self) -> bool { - matches!(self.supply_change, SupplyChange::Issuance(_)) - } } -/// An `issued_asset` map +/// An map of issued asset states by asset base. // TODO: Reference ZIP #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct IssuedAssets(HashMap); @@ -235,10 +269,9 @@ impl IssuedAssets { self.0.iter() } - fn update<'a>(&mut self, issued_assets: impl Iterator + 'a) { - for (asset_base, asset_state) in issued_assets { - self.0.insert(asset_base, asset_state); - } + /// Extends inner [`HashMap`] with updated asset states from the provided iterator + fn extend<'a>(&mut self, issued_assets: impl Iterator + 'a) { + self.0.extend(issued_assets); } } @@ -257,10 +290,12 @@ impl IntoIterator for IssuedAssets { pub struct IssuedAssetsChange(HashMap); impl IssuedAssetsChange { + /// Creates a new [`IssuedAssetsChange`]. fn new() -> Self { Self(HashMap::new()) } + /// Applies changes in the provided iterator to an [`IssuedAssetsChange`]. fn update<'a>( &mut self, changes: impl Iterator + 'a, @@ -274,22 +309,28 @@ impl IssuedAssetsChange { true } - /// Accepts a slice of [`Arc`]s. + /// Accepts a [`Arc`]. /// /// Returns an [`IssuedAssetsChange`] representing all of the changes to the issued assets - /// map that should be applied for the provided transactions. - pub fn from_transactions(transactions: &[Arc]) -> Option { + /// map that should be applied for the provided transaction, or `None` if the change would be invalid. + pub fn from_transaction(transaction: &Arc) -> Option { let mut issued_assets_change = Self::new(); - for transaction in transactions { - if !issued_assets_change.update(AssetStateChange::from_transaction(transaction)) { - return None; - } + if !issued_assets_change.update(AssetStateChange::from_transaction(transaction)) { + return None; } Some(issued_assets_change) } + /// Accepts a slice of [`Arc`]s. + /// + /// Returns an [`IssuedAssetsChange`] representing all of the changes to the issued assets + /// map that should be applied for the provided transactions. + pub fn from_transactions(transactions: &[Arc]) -> Option> { + transactions.iter().map(Self::from_transaction).collect() + } + /// Consumes self and accepts a closure for looking up previous asset states. /// /// Applies changes in self to the previous asset state. @@ -298,7 +339,7 @@ impl IssuedAssetsChange { pub fn apply_with(self, f: impl Fn(AssetBase) -> AssetState) -> IssuedAssets { let mut issued_assets = IssuedAssets::new(); - issued_assets.update(self.0.into_iter().map(|(asset_base, change)| { + issued_assets.extend(self.0.into_iter().map(|(asset_base, change)| { ( asset_base, f(asset_base) @@ -309,6 +350,11 @@ impl IssuedAssetsChange { issued_assets } + + /// Iterates over the inner [`HashMap`] of asset bases and state changes. + pub fn iter(&self) -> impl Iterator + '_ { + self.0.iter().map(|(&base, &state)| (base, state)) + } } impl std::ops::Add for IssuedAssetsChange { @@ -324,13 +370,3 @@ impl std::ops::Add for IssuedAssetsChange { } } } - -impl IntoIterator for IssuedAssetsChange { - type Item = (AssetBase, AssetStateChange); - - type IntoIter = std::collections::hash_map::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} diff --git a/zebra-consensus/src/block.rs b/zebra-consensus/src/block.rs index 6728bd9be66..aa4bf94c72f 100644 --- a/zebra-consensus/src/block.rs +++ b/zebra-consensus/src/block.rs @@ -15,7 +15,7 @@ use std::{ }; use chrono::Utc; -use futures::stream::FuturesUnordered; +use futures::stream::FuturesOrdered; use futures_util::FutureExt; use thiserror::Error; use tower::{Service, ServiceExt}; @@ -24,7 +24,6 @@ use tracing::Instrument; use zebra_chain::{ amount::Amount, block, - orchard_zsa::IssuedAssetsChange, parameters::{subsidy::FundingStreamReceiver, Network}, transparent, work::equihash, @@ -227,7 +226,7 @@ where tx::check::coinbase_outputs_are_decryptable(&coinbase_tx, &network, height)?; // Send transactions to the transaction verifier to be checked - let mut async_checks = FuturesUnordered::new(); + let mut async_checks = FuturesOrdered::new(); let known_utxos = Arc::new(transparent::new_ordered_outputs( &block, @@ -244,7 +243,7 @@ where height, time: block.header.time, }); - async_checks.push(rsp); + async_checks.push_back(rsp); } tracing::trace!(len = async_checks.len(), "built async tx checks"); @@ -253,26 +252,32 @@ where // Sum up some block totals from the transaction responses. let mut legacy_sigop_count = 0; let mut block_miner_fees = Ok(Amount::zero()); + let mut issued_assets_changes = Vec::new(); use futures::StreamExt; while let Some(result) = async_checks.next().await { tracing::trace!(?result, remaining = async_checks.len()); - let response = result + let crate::transaction::Response::Block { + tx_id: _, + miner_fee, + legacy_sigop_count: tx_legacy_sigop_count, + issued_assets_change, + } = result .map_err(Into::into) - .map_err(VerifyBlockError::Transaction)?; - - assert!( - matches!(response, tx::Response::Block { .. }), - "unexpected response from transaction verifier: {response:?}" - ); + .map_err(VerifyBlockError::Transaction)? + else { + panic!("unexpected response from transaction verifier"); + }; - legacy_sigop_count += response.legacy_sigop_count(); + legacy_sigop_count += tx_legacy_sigop_count; // Coinbase transactions consume the miner fee, // so they don't add any value to the block's total miner fee. - if let Some(miner_fee) = response.miner_fee() { + if let Some(miner_fee) = miner_fee { block_miner_fees += miner_fee; } + + issued_assets_changes.push(issued_assets_change); } // Check the summed block totals @@ -315,9 +320,6 @@ where let new_outputs = Arc::into_inner(known_utxos) .expect("all verification tasks using known_utxos are complete"); - let issued_assets_change = IssuedAssetsChange::from_transactions(&block.transactions) - .ok_or(TransactionError::InvalidAssetIssuanceOrBurn)?; - let prepared_block = zs::SemanticallyVerifiedBlock { block, hash, @@ -325,7 +327,7 @@ where new_outputs, transaction_hashes, deferred_balance: Some(expected_deferred_amount), - issued_assets_change: Some(issued_assets_change), + issued_assets_changes: issued_assets_changes.into(), }; // Return early for proposal requests when getblocktemplate-rpcs feature is enabled diff --git a/zebra-consensus/src/transaction.rs b/zebra-consensus/src/transaction.rs index e91f576f2a5..11af08c3907 100644 --- a/zebra-consensus/src/transaction.rs +++ b/zebra-consensus/src/transaction.rs @@ -19,6 +19,7 @@ use tracing::Instrument; use zebra_chain::{ amount::{Amount, NonNegative}, block, orchard, + orchard_zsa::IssuedAssetsChange, parameters::{Network, NetworkUpgrade}, primitives::Groth16Proof, sapling, @@ -143,6 +144,10 @@ pub enum Response { /// The number of legacy signature operations in this transaction's /// transparent inputs and outputs. legacy_sigop_count: u64, + + /// The changes to the issued assets map that should be applied for + /// this transaction. + issued_assets_change: IssuedAssetsChange, }, /// A response to a mempool transaction verification request. @@ -473,6 +478,7 @@ where tx_id, miner_fee, legacy_sigop_count, + issued_assets_change: IssuedAssetsChange::from_transaction(&tx).ok_or(TransactionError::InvalidAssetIssuanceOrBurn)?, }, Request::Mempool { transaction, .. } => { let transaction = VerifiedUnminedTx::new( diff --git a/zebra-state/src/arbitrary.rs b/zebra-state/src/arbitrary.rs index 2c1efef3753..183567b5794 100644 --- a/zebra-state/src/arbitrary.rs +++ b/zebra-state/src/arbitrary.rs @@ -31,7 +31,8 @@ impl Prepare for Arc { let transaction_hashes: Arc<[_]> = block.transactions.iter().map(|tx| tx.hash()).collect(); let new_outputs = transparent::new_ordered_outputs_with_height(&block, height, &transaction_hashes); - let issued_assets_change = IssuedAssetsChange::from_transactions(&block.transactions); + let issued_assets_changes = IssuedAssetsChange::from_transactions(&block.transactions) + .expect("prepared blocks should be semantically valid"); SemanticallyVerifiedBlock { block, @@ -40,7 +41,7 @@ impl Prepare for Arc { new_outputs, transaction_hashes, deferred_balance: None, - issued_assets_change, + issued_assets_changes, } } } @@ -119,7 +120,7 @@ impl ContextuallyVerifiedBlock { new_outputs, transaction_hashes, deferred_balance: _, - issued_assets_change: _, + issued_assets_changes: _, } = block.into(); Self { diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index bdce59adf0f..0cfa791001c 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -164,9 +164,9 @@ pub struct SemanticallyVerifiedBlock { pub transaction_hashes: Arc<[transaction::Hash]>, /// This block's contribution to the deferred pool. pub deferred_balance: Option>, - /// A map of burns to be applied to the issued assets map. - // TODO: Reference ZIP. - pub issued_assets_change: Option, + /// A precomputed list of the [`IssuedAssetsChange`]s for the transactions in this block, + /// in the same order as `block.transactions`. + pub issued_assets_changes: Arc<[IssuedAssetsChange]>, } /// A block ready to be committed directly to the finalized state with @@ -319,9 +319,15 @@ pub enum IssuedAssetsOrChange { Change(IssuedAssetsChange), } -impl From for IssuedAssetsOrChange { - fn from(change: IssuedAssetsChange) -> Self { - Self::Change(change) +impl From> for IssuedAssetsOrChange { + fn from(change: Arc<[IssuedAssetsChange]>) -> Self { + Self::Change( + change + .iter() + .cloned() + .reduce(|a, b| a + b) + .unwrap_or_default(), + ) } } @@ -334,11 +340,7 @@ impl From for IssuedAssetsOrChange { impl FinalizedBlock { /// Constructs [`FinalizedBlock`] from [`CheckpointVerifiedBlock`] and its [`Treestate`]. pub fn from_checkpoint_verified(block: CheckpointVerifiedBlock, treestate: Treestate) -> Self { - let issued_assets = block - .issued_assets_change - .clone() - .expect("checkpoint verified block should have issued assets change") - .into(); + let issued_assets = block.issued_assets_changes.clone().into(); Self::from_semantically_verified( SemanticallyVerifiedBlock::from(block), @@ -449,7 +451,7 @@ impl ContextuallyVerifiedBlock { new_outputs, transaction_hashes, deferred_balance, - issued_assets_change: _, + issued_assets_changes: _, } = semantically_verified; // This is redundant for the non-finalized state, @@ -485,7 +487,7 @@ impl CheckpointVerifiedBlock { let issued_assets_change = IssuedAssetsChange::from_transactions(&block.transactions)?; let mut block = Self::with_hash(block.clone(), hash.unwrap_or(block.hash())); block.deferred_balance = deferred_balance; - block.issued_assets_change = Some(issued_assets_change); + block.issued_assets_changes = issued_assets_change; Some(block) } @@ -515,7 +517,7 @@ impl SemanticallyVerifiedBlock { new_outputs, transaction_hashes, deferred_balance: None, - issued_assets_change: None, + issued_assets_changes: Arc::new([]), } } @@ -528,11 +530,7 @@ impl SemanticallyVerifiedBlock { impl From> for CheckpointVerifiedBlock { fn from(block: Arc) -> Self { - let mut block = SemanticallyVerifiedBlock::from(block); - block.issued_assets_change = - IssuedAssetsChange::from_transactions(&block.block.transactions); - - CheckpointVerifiedBlock(block) + Self(SemanticallyVerifiedBlock::from(block)) } } @@ -552,7 +550,7 @@ impl From> for SemanticallyVerifiedBlock { new_outputs, transaction_hashes, deferred_balance: None, - issued_assets_change: None, + issued_assets_changes: Arc::new([]), } } } @@ -572,7 +570,7 @@ impl From for SemanticallyVerifiedBlock { .constrain::() .expect("deferred balance in a block must me non-negative"), ), - issued_assets_change: None, + issued_assets_changes: Arc::new([]), } } } diff --git a/zebra-state/src/service/chain_tip.rs b/zebra-state/src/service/chain_tip.rs index 95306b54282..8a0ed517766 100644 --- a/zebra-state/src/service/chain_tip.rs +++ b/zebra-state/src/service/chain_tip.rs @@ -116,7 +116,7 @@ impl From for ChainTipBlock { new_outputs: _, transaction_hashes, deferred_balance: _, - issued_assets_change: _, + issued_assets_changes: _, } = prepared; Self { diff --git a/zebra-state/src/service/check/issuance.rs b/zebra-state/src/service/check/issuance.rs index daf7635e68a..abff882c33c 100644 --- a/zebra-state/src/service/check/issuance.rs +++ b/zebra-state/src/service/check/issuance.rs @@ -2,45 +2,67 @@ use std::{collections::HashMap, sync::Arc}; -use zebra_chain::orchard_zsa::IssuedAssets; +use zebra_chain::orchard_zsa::{AssetBase, AssetState, IssuedAssets}; use crate::{SemanticallyVerifiedBlock, ValidateContextError, ZebraDb}; use super::Chain; +// TODO: Factor out chain/disk read to a fn in the `read` module. +fn asset_state( + finalized_state: &ZebraDb, + parent_chain: &Arc, + issued_assets: &HashMap, + asset_base: &AssetBase, +) -> Option { + issued_assets + .get(asset_base) + .copied() + .or_else(|| parent_chain.issued_asset(asset_base)) + .or_else(|| finalized_state.issued_asset(asset_base)) +} + pub fn valid_burns_and_issuance( finalized_state: &ZebraDb, parent_chain: &Arc, semantically_verified: &SemanticallyVerifiedBlock, ) -> Result { - let Some(issued_assets_change) = semantically_verified.issued_assets_change.clone() else { - return Ok(IssuedAssets::default()); - }; - let mut issued_assets = HashMap::new(); - for (asset_base, change) in issued_assets_change { - let asset_state = issued_assets - .get(&asset_base) - .copied() - .or_else(|| parent_chain.issued_asset(&asset_base)) - .or_else(|| finalized_state.issued_asset(&asset_base)); + for (issued_assets_change, transaction) in semantically_verified + .issued_assets_changes + .iter() + .zip(&semantically_verified.block.transactions) + { + // Check that no burn item attempts to burn more than the issued supply for an asset + for burn in transaction.orchard_burns() { + let asset_base = burn.asset(); + let asset_state = + asset_state(finalized_state, parent_chain, &issued_assets, &asset_base) + .ok_or(ValidateContextError::InvalidBurn)?; - let updated_asset_state = if change.is_burn() { - asset_state - .ok_or(ValidateContextError::InvalidBurn)? - .apply_change(change) - .ok_or(ValidateContextError::InvalidBurn)? - } else { - asset_state - .unwrap_or_default() + if asset_state.total_supply < burn.amount() { + return Err(ValidateContextError::InvalidBurn); + } else { + // Any burned asset bases in the transaction will also be present in the issued assets change, + // adding a copy of initial asset state to `issued_assets` avoids duplicate disk reads. + issued_assets.insert(asset_base, asset_state); + } + } + + for (asset_base, change) in issued_assets_change.iter() { + let asset_state = + asset_state(finalized_state, parent_chain, &issued_assets, &asset_base) + .unwrap_or_default(); + + let updated_asset_state = asset_state .apply_change(change) - .ok_or(ValidateContextError::InvalidIssuance)? - }; + .ok_or(ValidateContextError::InvalidIssuance)?; - issued_assets - .insert(asset_base, updated_asset_state) - .expect("transactions must have only one burn item per asset base"); + issued_assets + .insert(asset_base, updated_asset_state) + .expect("transactions must have only one burn item per asset base"); + } } Ok(issued_assets.into()) diff --git a/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs b/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs index 7b35af87a75..d7df21fda0e 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs @@ -130,7 +130,7 @@ fn test_block_db_round_trip_with( .collect(); let new_outputs = new_ordered_outputs_with_height(&original_block, Height(0), &transaction_hashes); - let issued_assets_change = + let issued_assets_changes = IssuedAssetsChange::from_transactions(&original_block.transactions) .expect("issued assets should be valid"); @@ -141,7 +141,7 @@ fn test_block_db_round_trip_with( new_outputs, transaction_hashes, deferred_balance: None, - issued_assets_change: Some(issued_assets_change), + issued_assets_changes, }) }; diff --git a/zebra-state/src/service/non_finalized_state/chain.rs b/zebra-state/src/service/non_finalized_state/chain.rs index 638ecd1ae70..30f838afbab 100644 --- a/zebra-state/src/service/non_finalized_state/chain.rs +++ b/zebra-state/src/service/non_finalized_state/chain.rs @@ -972,13 +972,17 @@ impl Chain { } } else { trace!(?position, "reverting changes to issued assets"); - for (asset_base, change) in IssuedAssetsChange::from_transactions(transactions) + for issued_assets_change in IssuedAssetsChange::from_transactions(transactions) .expect("blocks in chain state must be valid") + .iter() + .rev() { - self.issued_assets - .entry(asset_base) - .or_default() - .revert_change(change); + for (asset_base, change) in issued_assets_change.iter() { + self.issued_assets + .entry(asset_base) + .or_default() + .revert_change(change); + } } } } From 3f7e68af6991406ca6aa53f5e2f8ac07c360c5e7 Mon Sep 17 00:00:00 2001 From: Arya Date: Fri, 15 Nov 2024 18:13:17 -0500 Subject: [PATCH 11/13] adds import --- zebra-chain/src/orchard_zsa/issuance.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/zebra-chain/src/orchard_zsa/issuance.rs b/zebra-chain/src/orchard_zsa/issuance.rs index 4b4de1e0fce..966dd85938d 100644 --- a/zebra-chain/src/orchard_zsa/issuance.rs +++ b/zebra-chain/src/orchard_zsa/issuance.rs @@ -7,6 +7,7 @@ use halo2::pasta::pallas; // For pallas::Base::from_repr only use group::ff::PrimeField; +use nonempty::NonEmpty; use zcash_primitives::transaction::components::issuance::{read_v6_bundle, write_v6_bundle}; use orchard::{ From 62bfef3fe0798b3293defbc5269d55ace03dbfff Mon Sep 17 00:00:00 2001 From: Arya Date: Fri, 15 Nov 2024 19:40:30 -0500 Subject: [PATCH 12/13] adds test for issuance check --- zebra-chain/src/orchard_zsa.rs | 4 +- zebra-chain/src/orchard_zsa/tests.rs | 4 +- zebra-chain/src/orchard_zsa/tests/issuance.rs | 10 ++- zebra-chain/src/orchard_zsa/tests/vectors.rs | 16 ++++ zebra-state/src/service/check/issuance.rs | 4 +- zebra-state/src/service/check/tests.rs | 3 + .../src/service/check/tests/issuance.rs | 83 +++++++++++++++++++ 7 files changed, 116 insertions(+), 8 deletions(-) create mode 100644 zebra-state/src/service/check/tests/issuance.rs diff --git a/zebra-chain/src/orchard_zsa.rs b/zebra-chain/src/orchard_zsa.rs index 8a22afec070..2b3fafb1381 100644 --- a/zebra-chain/src/orchard_zsa.rs +++ b/zebra-chain/src/orchard_zsa.rs @@ -4,8 +4,8 @@ #[cfg(any(test, feature = "proptest-impl"))] pub(crate) mod arbitrary; -#[cfg(test)] -mod tests; +#[cfg(any(test, feature = "proptest-impl"))] +pub mod tests; mod asset_state; mod burn; diff --git a/zebra-chain/src/orchard_zsa/tests.rs b/zebra-chain/src/orchard_zsa/tests.rs index a9301a7461e..0f61d2fcdd8 100644 --- a/zebra-chain/src/orchard_zsa/tests.rs +++ b/zebra-chain/src/orchard_zsa/tests.rs @@ -1,2 +1,4 @@ +#[cfg(test)] mod issuance; -mod vectors; + +pub mod vectors; diff --git a/zebra-chain/src/orchard_zsa/tests/issuance.rs b/zebra-chain/src/orchard_zsa/tests/issuance.rs index 9dc90d881c7..6eb0c586641 100644 --- a/zebra-chain/src/orchard_zsa/tests/issuance.rs +++ b/zebra-chain/src/orchard_zsa/tests/issuance.rs @@ -1,11 +1,17 @@ -use crate::{block::Block, serialization::ZcashDeserialize, transaction::Transaction}; +use crate::{ + block::Block, orchard_zsa::IssuedAssetsChange, serialization::ZcashDeserialize, + transaction::Transaction, +}; use super::vectors::BLOCKS; #[test] fn issuance_block() { let issuance_block = - Block::zcash_deserialize(BLOCKS[0].as_ref()).expect("issuance block should deserialize"); + Block::zcash_deserialize(BLOCKS[0]).expect("issuance block should deserialize"); + + IssuedAssetsChange::from_transactions(&issuance_block.transactions) + .expect("issuance in block should be valid"); for transaction in issuance_block.transactions { if let Transaction::V6 { diff --git a/zebra-chain/src/orchard_zsa/tests/vectors.rs b/zebra-chain/src/orchard_zsa/tests/vectors.rs index d5664e50b19..2812ee86c35 100644 --- a/zebra-chain/src/orchard_zsa/tests/vectors.rs +++ b/zebra-chain/src/orchard_zsa/tests/vectors.rs @@ -1,3 +1,19 @@ mod blocks; +use std::sync::Arc; + pub(crate) use blocks::BLOCKS; +use itertools::Itertools; + +use crate::{block::Block, serialization::ZcashDeserializeInto}; + +// TODO: Move this to zebra-test. +pub fn valid_issuance_blocks() -> Vec> { + BLOCKS + .iter() + .copied() + .map(ZcashDeserializeInto::zcash_deserialize_into) + .map(|result| result.map(Arc::new)) + .try_collect() + .expect("hard-coded block data must deserialize successfully") +} diff --git a/zebra-state/src/service/check/issuance.rs b/zebra-state/src/service/check/issuance.rs index abff882c33c..b130e1c9f90 100644 --- a/zebra-state/src/service/check/issuance.rs +++ b/zebra-state/src/service/check/issuance.rs @@ -59,9 +59,7 @@ pub fn valid_burns_and_issuance( .apply_change(change) .ok_or(ValidateContextError::InvalidIssuance)?; - issued_assets - .insert(asset_base, updated_asset_state) - .expect("transactions must have only one burn item per asset base"); + issued_assets.insert(asset_base, updated_asset_state); } } diff --git a/zebra-state/src/service/check/tests.rs b/zebra-state/src/service/check/tests.rs index 9608105766d..8d51105ea26 100644 --- a/zebra-state/src/service/check/tests.rs +++ b/zebra-state/src/service/check/tests.rs @@ -4,3 +4,6 @@ mod anchors; mod nullifier; mod utxo; mod vectors; + +#[cfg(feature = "tx-v6")] +mod issuance; diff --git a/zebra-state/src/service/check/tests/issuance.rs b/zebra-state/src/service/check/tests/issuance.rs new file mode 100644 index 00000000000..71db34328bf --- /dev/null +++ b/zebra-state/src/service/check/tests/issuance.rs @@ -0,0 +1,83 @@ +use std::sync::Arc; + +use zebra_chain::{ + block::{self, genesis::regtest_genesis_block, Block}, + orchard_zsa::{tests::vectors::valid_issuance_blocks, IssuedAssets}, + parameters::Network, +}; + +use crate::{ + check::{self, Chain}, + service::{finalized_state::FinalizedState, write::validate_and_commit_non_finalized}, + CheckpointVerifiedBlock, Config, NonFinalizedState, +}; + +#[test] +fn check_burns_and_issuance() { + let _init_guard = zebra_test::init(); + + let network = Network::new_regtest(Some(1), None, Some(1)); + + let mut finalized_state = FinalizedState::new_with_debug( + &Config::ephemeral(), + &network, + true, + #[cfg(feature = "elasticsearch")] + false, + false, + ); + + let mut non_finalized_state = NonFinalizedState::new(&network); + + let regtest_genesis_block = regtest_genesis_block(); + let regtest_genesis_hash = regtest_genesis_block.hash(); + + finalized_state + .commit_finalized_direct(regtest_genesis_block.into(), None, "test") + .expect("unexpected invalid genesis block test vector"); + + let block = valid_issuance_blocks().first().unwrap().clone(); + let mut header = Arc::::unwrap_or_clone(block.header.clone()); + header.previous_block_hash = regtest_genesis_hash; + header.commitment_bytes = [0; 32].into(); + let block = Arc::new(Block { + header: Arc::new(header), + transactions: block.transactions.clone(), + }); + + let CheckpointVerifiedBlock(block) = CheckpointVerifiedBlock::new(block, None, None) + .expect("semantic validation of issued assets changes should pass"); + + let empty_chain = Chain::new( + &network, + finalized_state + .db + .finalized_tip_height() + .unwrap_or(block::Height::MIN), + finalized_state.db.sprout_tree_for_tip(), + finalized_state.db.sapling_tree_for_tip(), + finalized_state.db.orchard_tree_for_tip(), + finalized_state.db.history_tree(), + finalized_state.db.finalized_value_pool(), + ); + + let block_1_issued_assets = check::issuance::valid_burns_and_issuance( + &finalized_state.db, + &Arc::new(empty_chain), + &block, + ) + .expect("test transactions should be valid"); + + validate_and_commit_non_finalized(&finalized_state.db, &mut non_finalized_state, block) + .expect("validation should succeed"); + + let best_chain = non_finalized_state + .best_chain() + .expect("should have a non-finalized chain"); + + assert_eq!( + IssuedAssets::from(best_chain.issued_assets.clone()), + block_1_issued_assets, + "issued assets for chain should match those of block 1" + ); +} From b365c9d173cb50786d4a924f3b574b4baa91ac43 Mon Sep 17 00:00:00 2001 From: Arya Date: Fri, 15 Nov 2024 20:41:23 -0500 Subject: [PATCH 13/13] adds test for semantic validation --- zebra-consensus/src/router/tests.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/zebra-consensus/src/router/tests.rs b/zebra-consensus/src/router/tests.rs index 8fe304e3364..05c3a11f7fc 100644 --- a/zebra-consensus/src/router/tests.rs +++ b/zebra-consensus/src/router/tests.rs @@ -2,12 +2,14 @@ use std::{sync::Arc, time::Duration}; +use block::genesis::regtest_genesis_block; use color_eyre::eyre::Report; use once_cell::sync::Lazy; use tower::{layer::Layer, timeout::TimeoutLayer}; use zebra_chain::{ block::Block, + orchard_zsa::tests::vectors::valid_issuance_blocks, serialization::{ZcashDeserialize, ZcashDeserializeInto}, }; use zebra_state as zs; @@ -270,3 +272,30 @@ async fn verify_fail_add_block_checkpoint() -> Result<(), Report> { Ok(()) } + +#[tokio::test(flavor = "multi_thread")] +async fn verify_issuance_blocks_test() -> Result<(), Report> { + let _init_guard = zebra_test::init(); + + let network = Network::new_regtest(Some(1), None, Some(1)); + let (block_verifier_router, _state_service) = verifiers_from_network(network.clone()).await; + + let block_verifier_router = + TimeoutLayer::new(Duration::from_secs(VERIFY_TIMEOUT_SECONDS)).layer(block_verifier_router); + + let commit_genesis = [( + Request::Commit(regtest_genesis_block()), + Ok(network.genesis_hash()), + )]; + + let commit_issuance_blocks = valid_issuance_blocks() + .into_iter() + .map(|block| (Request::Commit(block.clone()), Ok(block.hash()))); + + Transcript::from(commit_genesis.into_iter().chain(commit_issuance_blocks)) + .check(block_verifier_router.clone()) + .await + .unwrap(); + + Ok(()) +}