Adds issued assets to the finalized state

This commit is contained in:
Arya 2024-11-12 01:07:20 -05:00
parent 1af120ea98
commit cc8bc0da97
11 changed files with 267 additions and 51 deletions

View File

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

View File

@ -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<AssetBase, AssetState>);
impl IssuedAssets {
fn new() -> Self {
Self(HashMap::new())
}
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);
}
}
}
impl IntoIterator for IssuedAssets {
type Item = (AssetBase, AssetState);
type IntoIter = std::collections::hash_map::IntoIter<AssetBase, AssetState>;
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<AssetBase, AssetStateChange>);
@ -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
}
}
}

View File

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

View File

@ -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<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_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<Block> {
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 {

View File

@ -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::{

View File

@ -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<Amount<NonNegative>>,
/// 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<Amount<NonNegative>>,
/// 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<IssuedAssetsChange> 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<Arc<Block>> 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<Arc<Block>> 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<ContextuallyVerifiedBlock> 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<ContextuallyVerifiedBlock> for SemanticallyVerifiedBlock {
.constrain::<NonNegative>()
.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<FinalizedBlock> 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<FinalizedBlock> 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,
},
}
}
}

View File

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

View File

@ -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<Node: FromDisk> FromDisk for NoteCommitmentSubtreeData<Node> {
)
}
}
// 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()
}
}

View File

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

View File

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

View File

@ -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<AssetState> {
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).
///