Adds documentation to types and methods in `asset_state` module, fixes several bugs.

This commit is contained in:
Arya 2024-11-15 17:52:49 -05:00
parent 8f26a89151
commit e063729bcd
9 changed files with 189 additions and 120 deletions

View File

@ -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<u64> {
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<Self> {
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> {
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> {
self.total_supply = (-change.supply_change).apply_to(self.total_supply)?;
Some(self)
@ -135,31 +161,40 @@ impl From<HashMap<AssetBase, AssetState>> 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<Transaction>) -> impl Iterator<Item = (AssetBase, Self)> + '_ {
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<Item = &'a IssueAction> + 'a,
) -> impl Iterator<Item = (AssetBase, Self)> + '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<Item = (AssetBase, Self)> + '_ {
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<Item = (AssetBase, Self)> + '_ {
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<Item = (AssetBase, Self)> + '_ {
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<AssetBase, AssetState>);
@ -235,10 +269,9 @@ impl IssuedAssets {
self.0.iter()
}
fn update<'a>(&mut self, issued_assets: impl Iterator<Item = (AssetBase, AssetState)> + '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<Item = (AssetBase, AssetState)> + 'a) {
self.0.extend(issued_assets);
}
}
@ -257,10 +290,12 @@ impl IntoIterator for IssuedAssets {
pub struct IssuedAssetsChange(HashMap<AssetBase, AssetStateChange>);
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<Item = (AssetBase, AssetStateChange)> + 'a,
@ -274,20 +309,26 @@ impl IssuedAssetsChange {
true
}
/// Accepts a [`Arc<Transaction>`].
///
/// Returns an [`IssuedAssetsChange`] representing all of the changes to the issued assets
/// map that should be applied for the provided transaction, or `None` if the change would be invalid.
pub fn from_transaction(transaction: &Arc<Transaction>) -> Option<Self> {
let mut issued_assets_change = Self::new();
if !issued_assets_change.update(AssetStateChange::from_transaction(transaction)) {
return None;
}
Some(issued_assets_change)
}
/// Accepts a slice of [`Arc<Transaction>`]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<Transaction>]) -> Option<Self> {
let mut issued_assets_change = Self::new();
for transaction in transactions {
if !issued_assets_change.update(AssetStateChange::from_transaction(transaction)) {
return None;
}
}
Some(issued_assets_change)
pub fn from_transactions(transactions: &[Arc<Transaction>]) -> Option<Arc<[Self]>> {
transactions.iter().map(Self::from_transaction).collect()
}
/// Consumes self and accepts a closure for looking up previous asset states.
@ -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<Item = (AssetBase, AssetStateChange)> + '_ {
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<AssetBase, AssetStateChange>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}

View File

@ -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)?;
.map_err(VerifyBlockError::Transaction)?
else {
panic!("unexpected response from transaction verifier");
};
assert!(
matches!(response, tx::Response::Block { .. }),
"unexpected response from transaction verifier: {response:?}"
);
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

View File

@ -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(

View File

@ -31,7 +31,8 @@ impl Prepare for Arc<Block> {
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<Block> {
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 {

View File

@ -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<Amount<NonNegative>>,
/// A map of burns to be applied to the issued assets map.
// TODO: Reference ZIP.
pub issued_assets_change: Option<IssuedAssetsChange>,
/// 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<IssuedAssetsChange> for IssuedAssetsOrChange {
fn from(change: IssuedAssetsChange) -> Self {
Self::Change(change)
impl From<Arc<[IssuedAssetsChange]>> 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<IssuedAssets> 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<Arc<Block>> for CheckpointVerifiedBlock {
fn from(block: Arc<Block>) -> 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<Arc<Block>> for SemanticallyVerifiedBlock {
new_outputs,
transaction_hashes,
deferred_balance: None,
issued_assets_change: None,
issued_assets_changes: Arc::new([]),
}
}
}
@ -572,7 +570,7 @@ impl From<ContextuallyVerifiedBlock> for SemanticallyVerifiedBlock {
.constrain::<NonNegative>()
.expect("deferred balance in a block must me non-negative"),
),
issued_assets_change: None,
issued_assets_changes: Arc::new([]),
}
}
}

View File

@ -116,7 +116,7 @@ impl From<SemanticallyVerifiedBlock> for ChainTipBlock {
new_outputs: _,
transaction_hashes,
deferred_balance: _,
issued_assets_change: _,
issued_assets_changes: _,
} = prepared;
Self {

View File

@ -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<Chain>,
issued_assets: &HashMap<AssetBase, AssetState>,
asset_base: &AssetBase,
) -> Option<AssetState> {
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<Chain>,
semantically_verified: &SemanticallyVerifiedBlock,
) -> Result<IssuedAssets, ValidateContextError> {
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()
.apply_change(change)
.ok_or(ValidateContextError::InvalidIssuance)?
};
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);
}
}
issued_assets
.insert(asset_base, updated_asset_state)
.expect("transactions must have only one burn item per asset base");
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)?;
issued_assets
.insert(asset_base, updated_asset_state)
.expect("transactions must have only one burn item per asset base");
}
}
Ok(issued_assets.into())

View File

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

View File

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