Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 27 additions & 43 deletions zebra-chain/src/orchard_zsa/asset_state.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
//! Defines and implements the issued asset state types

use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use std::{collections::HashMap, sync::Arc};

use orchard::issuance::IssueAction;
pub use orchard::note::AssetBase;

use crate::{serialization::ZcashSerialize, transaction::Transaction};

use super::BurnItem;
use super::{BurnItem, IssueData};

/// The circulating supply and whether that supply has been finalized.
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
Expand Down Expand Up @@ -183,52 +180,39 @@ impl AssetStateChange {
/// 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()))
Self::from_burns(tx.orchard_burns()).chain(
tx.orchard_issue_data()
.iter()
.flat_map(Self::from_issue_data),
)
}

/// Accepts an iterator of [`IssueAction`]s and returns an iterator of asset bases and issued asset state changes
/// Accepts an [`IssueData`] 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)
fn from_issue_data(issue_data: &IssueData) -> impl Iterator<Item = (AssetBase, Self)> + '_ {
let ik = issue_data.inner().ik();
issue_data.actions().flat_map(|action| {
let issue_asset = AssetBase::derive(ik, action.asset_desc());
Self::from_issue_action(issue_asset, 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
.is_finalized()
.then(|| {
action
.notes()
.iter()
.map(orchard::Note::asset)
.collect::<HashSet<AssetBase>>()
})
.unwrap_or_default()
fn from_issue_action(
issue_asset: AssetBase,
action: &IssueAction,
) -> impl Iterator<Item = (AssetBase, Self)> + '_ {
(action.is_finalized() && action.notes().is_empty())
.then(|| Self::new(issue_asset, SupplyChange::Issuance(0), true))
.into_iter()
.map(|asset_base| Self::new(asset_base, SupplyChange::Issuance(0), true));

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(),
SupplyChange::Issuance(note.value().inner()),
false,
)
.chain(action.notes().iter().map(|note| {
Self::new(
note.asset(),
SupplyChange::Issuance(note.value().inner()),
action.is_finalized(),
)
}))
}

/// Accepts an iterator of [`BurnItem`]s and returns an iterator of asset bases and issued asset state changes
Expand Down
11 changes: 1 addition & 10 deletions zebra-chain/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1071,7 +1071,7 @@ 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<orchard_zsa::IssueData> {
pub fn orchard_issue_data(&self) -> &Option<orchard_zsa::IssueData> {
match self {
Transaction::V1 { .. }
| Transaction::V2 { .. }
Expand All @@ -1086,15 +1086,6 @@ impl Transaction {
}
}

/// 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<Item = &::orchard::issuance::IssueAction> {
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")]
Expand Down
65 changes: 53 additions & 12 deletions zebra-consensus/src/orchard_zsa/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ use color_eyre::eyre::Report;
use tower::ServiceExt;

use orchard::{
asset_record::AssetRecord, issuance::IssueAction, note::AssetBase, value::NoteValue,
asset_record::AssetRecord, issuance::IssueAction, keys::IssuanceValidatingKey, note::AssetBase,
value::NoteValue,
};

use zebra_chain::{
Expand All @@ -35,6 +36,7 @@ type TranscriptItem = (Request, Result<Hash, ExpectedTranscriptError>);
#[derive(Debug)]
enum AssetRecordsError {
BurnAssetMissing,
EmptyActionNotFinalized,
AmountOverflow,
MissingRefNote,
ModifyFinalized,
Expand Down Expand Up @@ -66,17 +68,38 @@ fn process_burns<'a, I: Iterator<Item = &'a BurnItem>>(
/// Processes orchard issue actions, increasing asset supply.
fn process_issue_actions<'a, I: Iterator<Item = &'a IssueAction>>(
asset_records: &mut AssetRecords,
issue_actions: I,
ik: &IssuanceValidatingKey,
actions: I,
) -> Result<(), AssetRecordsError> {
for action in issue_actions {
for action in actions {
let action_asset = AssetBase::derive(ik, action.asset_desc());
let reference_note = action.get_reference_note();
let is_finalized = action.is_finalized();

for note in action.notes() {
let amount = note.value();
let mut note_amounts = action.notes().into_iter().map(|note| {
if note.asset() == action_asset {
Ok(note.value())
} else {
Err(AssetRecordsError::BurnAssetMissing)
}
});

let first_note_amount = match note_amounts.next() {
Some(note_amount) => note_amount,
None => {
if is_finalized {
Ok(NoteValue::from_raw(0))
} else {
Err(AssetRecordsError::EmptyActionNotFinalized)
}
}
};

for amount_result in std::iter::once(first_note_amount).chain(note_amounts) {
let amount = amount_result?;

// FIXME: check for issuance specific errors?
match asset_records.entry(note.asset()) {
match asset_records.entry(action_asset) {
hash_map::Entry::Occupied(mut entry) => {
let asset_record = entry.get_mut();
asset_record.amount =
Expand Down Expand Up @@ -107,15 +130,22 @@ fn build_asset_records<'a, I: IntoIterator<Item = &'a TranscriptItem>>(
) -> Result<AssetRecords, AssetRecordsError> {
blocks
.into_iter()
.filter_map(|(request, _)| match request {
Request::Commit(block) => Some(&block.transactions),
#[cfg(feature = "getblocktemplate-rpcs")]
Request::CheckProposal(_) => None,
.filter_map(|(request, result)| match (request, result) {
(Request::Commit(block), Ok(_)) => Some(&block.transactions),
_ => None,
})
.flatten()
.try_fold(HashMap::new(), |mut asset_records, tx| {
process_burns(&mut asset_records, tx.orchard_burns())?;
process_issue_actions(&mut asset_records, tx.orchard_issue_actions())?;

if let Some(issue_data) = tx.orchard_issue_data() {
process_issue_actions(
&mut asset_records,
issue_data.inner().ik(),
issue_data.actions(),
)?;
}

Ok(asset_records)
})
}
Expand All @@ -130,7 +160,17 @@ fn create_transcript_data<'a, I: IntoIterator<Item = &'a Vec<u8>>>(

std::iter::once(regtest_genesis_block())
.chain(workflow_blocks)
.map(|block| (Request::Commit(block.clone()), Ok(block.hash())))
.enumerate()
.map(|(i, block)| {
(
Request::Commit(block.clone()),
if i == 5 {
Err(ExpectedTranscriptError::Any)
} else {
Ok(block.hash())
},
)
})
}

/// Queries the state service for the asset state of the given asset.
Expand Down Expand Up @@ -195,6 +235,7 @@ async fn check_zsa_workflow() -> Result<(), Report> {

assert_eq!(
asset_state.total_supply,
// FIXME: Fix it after chaning ValueSum to NoteValue in AssetSupply in orchard
u64::try_from(i128::from(asset_record.amount))
.expect("asset supply amount should be within u64 range"),
"Total supply mismatch for asset {:?}.",
Expand Down
52 changes: 39 additions & 13 deletions zebra-state/src/service/non_finalized_state/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1751,21 +1751,22 @@ impl UpdateWith<ContextuallyVerifiedBlock> for Chain {
for (transaction, transaction_hash) in
block.transactions.iter().zip(transaction_hashes.iter())
{
let (
inputs,
outputs,
joinsplit_data,
sapling_shielded_data_per_spend_anchor,
sapling_shielded_data_shared_anchor,
orchard_shielded_data,
) = match transaction.deref() {
let transaction_data = match transaction.deref() {
V4 {
inputs,
outputs,
joinsplit_data,
sapling_shielded_data,
..
} => (inputs, outputs, joinsplit_data, sapling_shielded_data, &None, &None),
} => (
inputs,
outputs,
joinsplit_data,
sapling_shielded_data,
&None,
&None,
#[cfg(feature = "tx-v6")]
&None),
V5 {
inputs,
outputs,
Expand All @@ -1779,28 +1780,51 @@ impl UpdateWith<ContextuallyVerifiedBlock> for Chain {
&None,
sapling_shielded_data,
orchard_shielded_data,
#[cfg(feature = "tx-v6")]
&None,
),
#[cfg(feature = "tx-v6")]
V6 {
inputs,
outputs,
sapling_shielded_data,
orchard_shielded_data: _,
orchard_shielded_data,
..
} => (
inputs,
outputs,
&None,
&None,
sapling_shielded_data,
// FIXME: support V6 shielded data?
&None, //orchard_shielded_data,
&None,
orchard_shielded_data,
),
V1 { .. } | V2 { .. } | V3 { .. } => unreachable!(
"older transaction versions only exist in finalized blocks, because of the mandatory canopy checkpoint",
),
};

#[cfg(not(feature = "tx-v6"))]
let (
inputs,
outputs,
joinsplit_data,
sapling_shielded_data_per_spend_anchor,
sapling_shielded_data_shared_anchor,
orchard_shielded_data_vanilla,
) = transaction_data;

#[cfg(feature = "tx-v6")]
let (
inputs,
outputs,
joinsplit_data,
sapling_shielded_data_per_spend_anchor,
sapling_shielded_data_shared_anchor,
orchard_shielded_data_vanilla,
orchard_shielded_data_zsa,
) = transaction_data;

// remove the utxos this produced
self.revert_chain_with(&(outputs, transaction_hash, new_outputs), position);
// reset the utxos this consumed
Expand All @@ -1817,7 +1841,9 @@ impl UpdateWith<ContextuallyVerifiedBlock> for Chain {
self.revert_chain_with(joinsplit_data, position);
self.revert_chain_with(sapling_shielded_data_per_spend_anchor, position);
self.revert_chain_with(sapling_shielded_data_shared_anchor, position);
self.revert_chain_with(orchard_shielded_data, position);
self.revert_chain_with(orchard_shielded_data_vanilla, position);
#[cfg(feature = "tx-v6")]
self.revert_chain_with(orchard_shielded_data_zsa, position);
}

// TODO: move these to the shielded UpdateWith.revert...()?
Expand Down
Loading
Loading