Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tracks issued assets state and contextually validates issue actions and asset burns #26

Open
wants to merge 10 commits into
base: zsa-integration-issuance-commitments
Choose a base branch
from
13 changes: 11 additions & 2 deletions zebra-chain/src/orchard/orchard_flavor_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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`.
Expand Down
5 changes: 4 additions & 1 deletion zebra-chain/src/orchard_zsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, IssuedAssets, IssuedAssetsChange};
238 changes: 238 additions & 0 deletions zebra-chain/src/orchard_zsa/asset_state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
//! 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::transaction::Transaction;

use super::BurnItem;

/// The circulating supply and whether that supply has been finalized.
#[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,

/// 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
// - 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.
pub is_finalized: bool,
/// The change in supply from newly issued assets or burned assets.
pub supply_change: i128,
}

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<Self> {
if self.is_finalized {
return None;
}

self.is_finalized |= change.is_finalized;
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<HashMap<AssetBase, AssetState>> for IssuedAssets {
fn from(issued_assets: HashMap<AssetBase, AssetState>) -> Self {
Self(issued_assets)
}
}

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<Item = (AssetBase, Self)> + '_ {
notes
.iter()
.map(move |note| Self::from_note(is_finalized, *note))
}

fn from_issue_actions<'a>(
actions: impl Iterator<Item = &'a IssueAction> + 'a,
) -> impl Iterator<Item = (AssetBase, Self)> + '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<Item = (AssetBase, Self)> + '_ {
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;
}
}

/// An `issued_asset` map
// TODO: Reference ZIP
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct IssuedAssets(HashMap<AssetBase, AssetState>);

impl IssuedAssets {
/// Creates a new [`IssuedAssets`].
pub fn new() -> Self {
Self(HashMap::new())
}

/// Returns an iterator of the inner HashMap.
pub fn iter(&self) -> impl Iterator<Item = (&AssetBase, &AssetState)> {
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);
}
}
}

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

impl IssuedAssetsChange {
fn new() -> Self {
Self(HashMap::new())
}

fn update<'a>(&mut self, changes: impl Iterator<Item = (AssetBase, AssetStateChange)> + 'a) {
for (asset_base, change) in changes {
*self.0.entry(asset_base).or_default() += change;
}
}

/// Accepts a slice of [`Arc<Transaction>`]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<Transaction>]) -> (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(),
));
}

(burn_change, issuance_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 combined_from_transactions(transactions: &[Arc<Transaction>]) -> 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.
///
/// 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)
.apply_change(change)
.expect("must be valid 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
}
}
}

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()
}
}
39 changes: 32 additions & 7 deletions zebra-chain/src/orchard_zsa/burn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
use std::io;

use crate::{
amount::Amount,
block::MAX_BLOCK_BYTES,
serialization::{SerializationError, TrustedPreallocate, ZcashDeserialize, ZcashSerialize},
};
Expand All @@ -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<Self, Self::Error> {
Ok(Self(item.0, item.1.inner().try_into()?))
Ok(Self(item.0, item.1.inner()))
}
}

Expand All @@ -36,17 +47,19 @@ 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(())
}
}

impl ZcashDeserialize for BurnItem {
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
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),
))
}
}
Expand Down Expand Up @@ -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?
Expand All @@ -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<W: io::Write>(&self, mut _writer: W) -> Result<(), io::Error> {
Ok(())
Expand All @@ -115,6 +134,12 @@ impl From<Vec<BurnItem>> for Burn {
}
}

impl AsRef<[BurnItem]> for Burn {
fn as_ref(&self) -> &[BurnItem] {
&self.0
}
}

impl ZcashSerialize for Burn {
fn zcash_serialize<W: io::Write>(&self, writer: W) -> Result<(), io::Error> {
self.0.zcash_serialize(writer)
Expand Down
5 changes: 5 additions & 0 deletions zebra-chain/src/orchard_zsa/issuance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ impl IssueData {
})
})
}

/// Returns issuance actions
pub fn actions(&self) -> &NonEmpty<IssueAction> {
self.0.actions()
}
}

// Sizes of the serialized values for types in bytes (used for TrustedPreallocate impls)
Expand Down
Loading