diff --git a/Cargo.lock b/Cargo.lock index cad04052478..62b4bf0d7c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6034,6 +6034,7 @@ dependencies = [ "incrementalmerkletree", "itertools 0.13.0", "jubjub", + "k256", "lazy_static", "nonempty", "num-integer", diff --git a/zebra-chain/Cargo.toml b/zebra-chain/Cargo.toml index 82cc28f3045..96469b0fb65 100644 --- a/zebra-chain/Cargo.toml +++ b/zebra-chain/Cargo.toml @@ -157,6 +157,8 @@ rand_chacha = { version = "0.3.1", optional = true } zebra-test = { path = "../zebra-test/", version = "1.0.0-beta.41", optional = true } +k256 = "0.13.3" + [dev-dependencies] # Benchmarks criterion = { version = "0.5.1", features = ["html_reports"] } diff --git a/zebra-chain/src/orchard_zsa.rs b/zebra-chain/src/orchard_zsa.rs index 5779da32e01..00a27360742 100644 --- a/zebra-chain/src/orchard_zsa.rs +++ b/zebra-chain/src/orchard_zsa.rs @@ -9,7 +9,7 @@ pub(crate) mod arbitrary; #[cfg(any(test, feature = "proptest-impl"))] pub mod tests; -mod asset_state; +pub mod asset_state; mod burn; mod issuance; diff --git a/zebra-chain/src/orchard_zsa/asset_state.rs b/zebra-chain/src/orchard_zsa/asset_state.rs index bdb6cd0e21b..264951bfd52 100644 --- a/zebra-chain/src/orchard_zsa/asset_state.rs +++ b/zebra-chain/src/orchard_zsa/asset_state.rs @@ -8,12 +8,12 @@ use std::{ use orchard::issuance::IssueAction; pub use orchard::note::AssetBase; -use crate::transaction::Transaction; +use crate::{serialization::ZcashSerialize, transaction::Transaction}; use super::BurnItem; /// The circulating supply and whether that supply has been finalized. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)] pub struct AssetState { /// Indicates whether the asset is finalized such that no more of it can be issued. pub is_finalized: bool, @@ -28,8 +28,7 @@ pub struct AssetState { pub struct AssetStateChange { /// Whether the asset should be finalized such that no more of it can be issued. pub should_finalize: bool, - // FIXME: is this a correct comment? - /// Whether the asset should be finalized such that no more of it can be issued. + /// Whether the asset has been issued in this change. pub includes_issuance: bool, /// The change in supply from newly issued assets or burned assets, if any. pub supply_change: SupplyChange, @@ -211,7 +210,6 @@ impl AssetStateChange { }) .unwrap_or_default() .into_iter() - // FIXME: We use 0 as a value - is that correct? .map(|asset_base| Self::new(asset_base, SupplyChange::Issuance(0), true)); supply_changes.chain(finalize_changes) @@ -374,3 +372,39 @@ impl std::ops::Add for IssuedAssetsChange { } } } + +impl From> for IssuedAssetsChange { + fn from(change: Arc<[IssuedAssetsChange]>) -> Self { + change + .iter() + .cloned() + .reduce(|a, b| a + b) + .unwrap_or_default() + } +} + +/// Used in snapshot test for `getassetstate` RPC method. +// TODO: Replace with `AssetBase::random()` or a known value. +pub trait RandomAssetBase { + /// Generates a ZSA random asset. + /// + /// This is only used in tests. + fn random_serialized() -> String; +} + +impl RandomAssetBase for AssetBase { + fn random_serialized() -> String { + let isk = orchard::keys::IssuanceAuthorizingKey::from_bytes( + k256::NonZeroScalar::random(&mut rand_core::OsRng) + .to_bytes() + .into(), + ) + .unwrap(); + let ik = orchard::keys::IssuanceValidatingKey::from(&isk); + let asset_descr = b"zsa_asset".to_vec(); + AssetBase::derive(&ik, &asset_descr) + .zcash_serialize_to_vec() + .map(hex::encode) + .expect("random asset base should serialize") + } +} diff --git a/zebra-consensus/src/block.rs b/zebra-consensus/src/block.rs index aa4bf94c72f..207a202f6ea 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::FuturesOrdered; +use futures::stream::FuturesUnordered; use futures_util::FutureExt; use thiserror::Error; use tower::{Service, ServiceExt}; @@ -226,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 = FuturesOrdered::new(); + let mut async_checks = FuturesUnordered::new(); let known_utxos = Arc::new(transparent::new_ordered_outputs( &block, @@ -243,7 +243,7 @@ where height, time: block.header.time, }); - async_checks.push_back(rsp); + async_checks.push(rsp); } tracing::trace!(len = async_checks.len(), "built async tx checks"); @@ -252,7 +252,6 @@ 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 { @@ -261,7 +260,6 @@ where tx_id: _, miner_fee, legacy_sigop_count: tx_legacy_sigop_count, - issued_assets_change, } = result .map_err(Into::into) .map_err(VerifyBlockError::Transaction)? @@ -276,8 +274,6 @@ where if let Some(miner_fee) = miner_fee { block_miner_fees += miner_fee; } - - issued_assets_changes.push(issued_assets_change); } // Check the summed block totals @@ -327,7 +323,6 @@ where new_outputs, transaction_hashes, deferred_balance: Some(expected_deferred_amount), - issued_assets_changes: issued_assets_changes.into(), }; // 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 f6520ba5564..039ea6e33e3 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, TransactionError}, + error::{BlockError, SubsidyError}, funding_stream_values, BoxError, ParameterCheckpoint as _, }; @@ -619,8 +619,7 @@ where }; // don't do precalculation until the block passes basic difficulty checks - let block = CheckpointVerifiedBlock::new(block, Some(hash), expected_deferred_amount) - .ok_or_else(|| VerifyBlockError::from(TransactionError::InvalidAssetIssuanceOrBurn))?; + let block = CheckpointVerifiedBlock::new(block, Some(hash), expected_deferred_amount); crate::block::check::merkle_root_validity( &self.network, diff --git a/zebra-consensus/src/error.rs b/zebra-consensus/src/error.rs index 9aa41103910..8fe14c62d52 100644 --- a/zebra-consensus/src/error.rs +++ b/zebra-consensus/src/error.rs @@ -239,9 +239,6 @@ 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-consensus/src/transaction.rs b/zebra-consensus/src/transaction.rs index ca06395b68c..8c5a6d69c92 100644 --- a/zebra-consensus/src/transaction.rs +++ b/zebra-consensus/src/transaction.rs @@ -19,7 +19,6 @@ use tracing::Instrument; use zebra_chain::{ amount::{Amount, NonNegative}, block, orchard, - orchard_zsa::IssuedAssetsChange, parameters::{Network, NetworkUpgrade}, primitives::Groth16Proof, sapling, @@ -144,10 +143,6 @@ 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. @@ -485,7 +480,6 @@ 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-rpc/src/methods.rs b/zebra-rpc/src/methods.rs index 8becc5bb79c..f38a6e4108f 100644 --- a/zebra-rpc/src/methods.rs +++ b/zebra-rpc/src/methods.rs @@ -23,7 +23,7 @@ use zebra_chain::{ block::{self, Height, SerializedBlock}, chain_tip::{ChainTip, NetworkChainTipHeightEstimator}, parameters::{ConsensusBranchId, Network, NetworkUpgrade}, - serialization::ZcashDeserialize, + serialization::{ZcashDeserialize, ZcashDeserializeInto}, subtree::NoteCommitmentSubtreeIndex, transaction::{self, SerializedTransaction, Transaction, UnminedTx}, transparent::{self, Address}, @@ -302,6 +302,17 @@ pub trait Rpc { address_strings: AddressStrings, ) -> BoxFuture>>; + /// Returns the asset state of the provided asset base at the best chain tip or finalized chain tip. + /// + /// method: post + /// tags: blockchain + #[rpc(name = "getassetstate")] + fn get_asset_state( + &self, + asset_base: String, + include_non_finalized: Option, + ) -> BoxFuture>; + /// Stop the running zebrad process. /// /// # Notes @@ -1358,6 +1369,36 @@ where .boxed() } + fn get_asset_state( + &self, + asset_base: String, + include_non_finalized: Option, + ) -> BoxFuture> { + let state = self.state.clone(); + let include_non_finalized = include_non_finalized.unwrap_or(true); + + async move { + let asset_base = hex::decode(asset_base) + .map_server_error()? + .zcash_deserialize_into() + .map_server_error()?; + + let request = zebra_state::ReadRequest::AssetState { + asset_base, + include_non_finalized, + }; + + let zebra_state::ReadResponse::AssetState(asset_state) = + state.oneshot(request).await.map_server_error()? + else { + unreachable!("unexpected response from state service"); + }; + + asset_state.ok_or_server_error("asset base not found") + } + .boxed() + } + fn stop(&self) -> Result { #[cfg(not(target_os = "windows"))] if self.network.is_regtest() { diff --git a/zebra-rpc/src/methods/tests/snapshot.rs b/zebra-rpc/src/methods/tests/snapshot.rs index f4d7804088e..fe9e9cccd7f 100644 --- a/zebra-rpc/src/methods/tests/snapshot.rs +++ b/zebra-rpc/src/methods/tests/snapshot.rs @@ -14,6 +14,7 @@ use zebra_chain::{ block::Block, chain_tip::mock::MockChainTip, orchard, + orchard_zsa::{asset_state::RandomAssetBase, AssetBase, AssetState}, parameters::{ subsidy::POST_NU6_FUNDING_STREAMS_TESTNET, testnet::{self, ConfiguredActivationHeights, Parameters}, @@ -536,6 +537,41 @@ async fn test_mocked_rpc_response_data_for_network(network: &Network) { settings.bind(|| { insta::assert_json_snapshot!(format!("z_get_subtrees_by_index_for_orchard"), subtrees) }); + + // Test the response format from `getassetstate`. + + // Prepare the state response and make the RPC request. + let rsp = state + .expect_request_that(|req| matches!(req, ReadRequest::AssetState { .. })) + .map(|responder| responder.respond(ReadResponse::AssetState(None))); + let req = rpc.get_asset_state(AssetBase::random_serialized(), None); + + // Get the RPC error response. + let (asset_state_rsp, ..) = tokio::join!(req, rsp); + let asset_state = asset_state_rsp.expect_err("The RPC response should be an error"); + + // Check the error response. + settings + .bind(|| insta::assert_json_snapshot!(format!("get_asset_state_not_found"), asset_state)); + + // Prepare the state response and make the RPC request. + let rsp = state + .expect_request_that(|req| matches!(req, ReadRequest::AssetState { .. })) + .map(|responder| { + responder.respond(ReadResponse::AssetState(Some(AssetState { + is_finalized: true, + total_supply: 1000, + }))) + }); + let req = rpc.get_asset_state(AssetBase::random_serialized(), None); + + // Get the RPC response. + let (asset_state_rsp, ..) = tokio::join!(req, rsp); + let asset_state = + asset_state_rsp.expect("The RPC response should contain a `AssetState` struct."); + + // Check the response. + settings.bind(|| insta::assert_json_snapshot!(format!("get_asset_state"), asset_state)); } /// Snapshot `getinfo` response, using `cargo insta` and JSON serialization. diff --git a/zebra-rpc/src/methods/tests/snapshots/get_asset_state@mainnet.snap b/zebra-rpc/src/methods/tests/snapshots/get_asset_state@mainnet.snap new file mode 100644 index 00000000000..9085ab62c88 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_asset_state@mainnet.snap @@ -0,0 +1,8 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: asset_state +--- +{ + "is_finalized": true, + "total_supply": 1000 +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_asset_state@testnet.snap b/zebra-rpc/src/methods/tests/snapshots/get_asset_state@testnet.snap new file mode 100644 index 00000000000..9085ab62c88 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_asset_state@testnet.snap @@ -0,0 +1,8 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: asset_state +--- +{ + "is_finalized": true, + "total_supply": 1000 +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_asset_state_not_found@mainnet.snap b/zebra-rpc/src/methods/tests/snapshots/get_asset_state_not_found@mainnet.snap new file mode 100644 index 00000000000..9efcfd5868f --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_asset_state_not_found@mainnet.snap @@ -0,0 +1,8 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: asset_state +--- +{ + "code": 0, + "message": "asset base not found" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_asset_state_not_found@testnet.snap b/zebra-rpc/src/methods/tests/snapshots/get_asset_state_not_found@testnet.snap new file mode 100644 index 00000000000..9efcfd5868f --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_asset_state_not_found@testnet.snap @@ -0,0 +1,8 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: asset_state +--- +{ + "code": 0, + "message": "asset base not found" +} diff --git a/zebra-state/src/arbitrary.rs b/zebra-state/src/arbitrary.rs index 183567b5794..352ad550159 100644 --- a/zebra-state/src/arbitrary.rs +++ b/zebra-state/src/arbitrary.rs @@ -5,7 +5,6 @@ use std::sync::Arc; use zebra_chain::{ amount::Amount, block::{self, Block}, - orchard_zsa::IssuedAssetsChange, transaction::Transaction, transparent, value_balance::ValueBalance, @@ -31,8 +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 issued_assets_changes = IssuedAssetsChange::from_transactions(&block.transactions) - .expect("prepared blocks should be semantically valid"); SemanticallyVerifiedBlock { block, @@ -41,7 +38,6 @@ impl Prepare for Arc { new_outputs, transaction_hashes, deferred_balance: None, - issued_assets_changes, } } } @@ -120,7 +116,6 @@ impl ContextuallyVerifiedBlock { new_outputs, transaction_hashes, deferred_balance: _, - issued_assets_changes: _, } = block.into(); Self { diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 0cfa791001c..cd71173caae 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::{IssuedAssets, IssuedAssetsChange}, + orchard_zsa::{AssetBase, IssuedAssets, IssuedAssetsChange}, parallel::tree::NoteCommitmentTrees, sapling, serialization::SerializationError, @@ -164,9 +164,6 @@ pub struct SemanticallyVerifiedBlock { pub transaction_hashes: Arc<[transaction::Hash]>, /// This block's contribution to the deferred pool. pub deferred_balance: 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 @@ -301,10 +298,9 @@ 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 + /// Updated asset states to be inserted into the finalized state, replacing the previous /// asset states for those asset bases. - pub issued_assets: IssuedAssetsOrChange, + pub issued_assets: Option, } /// Either changes to be applied to the previous `issued_assets` map for the finalized tip, or @@ -319,18 +315,6 @@ pub enum IssuedAssetsOrChange { Change(IssuedAssetsChange), } -impl From> for IssuedAssetsOrChange { - fn from(change: Arc<[IssuedAssetsChange]>) -> Self { - Self::Change( - change - .iter() - .cloned() - .reduce(|a, b| a + b) - .unwrap_or_default(), - ) - } -} - impl From for IssuedAssetsOrChange { fn from(updated_issued_assets: IssuedAssets) -> Self { Self::Updated(updated_issued_assets) @@ -340,13 +324,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_changes.clone().into(); - - Self::from_semantically_verified( - SemanticallyVerifiedBlock::from(block), - treestate, - issued_assets, - ) + Self::from_semantically_verified(SemanticallyVerifiedBlock::from(block), treestate, None) } /// Constructs [`FinalizedBlock`] from [`ContextuallyVerifiedBlock`] and its [`Treestate`]. @@ -354,7 +332,7 @@ impl FinalizedBlock { block: ContextuallyVerifiedBlock, treestate: Treestate, ) -> Self { - let issued_assets = block.issued_assets.clone().into(); + let issued_assets = Some(block.issued_assets.clone()); Self::from_semantically_verified( SemanticallyVerifiedBlock::from(block), treestate, @@ -366,7 +344,7 @@ impl FinalizedBlock { fn from_semantically_verified( block: SemanticallyVerifiedBlock, treestate: Treestate, - issued_assets: IssuedAssetsOrChange, + issued_assets: Option, ) -> Self { Self { block: block.block, @@ -451,7 +429,6 @@ impl ContextuallyVerifiedBlock { new_outputs, transaction_hashes, deferred_balance, - issued_assets_changes: _, } = semantically_verified; // This is redundant for the non-finalized state, @@ -483,12 +460,10 @@ impl CheckpointVerifiedBlock { block: Arc, hash: Option, deferred_balance: Option>, - ) -> Option { - let issued_assets_change = IssuedAssetsChange::from_transactions(&block.transactions)?; + ) -> Self { let mut block = Self::with_hash(block.clone(), hash.unwrap_or(block.hash())); block.deferred_balance = deferred_balance; - block.issued_assets_changes = issued_assets_change; - Some(block) + block } /// Creates a block that's ready to be committed to the finalized state, @@ -517,7 +492,6 @@ impl SemanticallyVerifiedBlock { new_outputs, transaction_hashes, deferred_balance: None, - issued_assets_changes: Arc::new([]), } } @@ -550,7 +524,6 @@ impl From> for SemanticallyVerifiedBlock { new_outputs, transaction_hashes, deferred_balance: None, - issued_assets_changes: Arc::new([]), } } } @@ -570,7 +543,6 @@ impl From for SemanticallyVerifiedBlock { .constrain::() .expect("deferred balance in a block must me non-negative"), ), - issued_assets_changes: Arc::new([]), } } } @@ -1122,6 +1094,17 @@ pub enum ReadRequest { /// Returns [`ReadResponse::TipBlockSize(usize)`](ReadResponse::TipBlockSize) /// with the current best chain tip block size in bytes. TipBlockSize, + + #[cfg(feature = "tx-v6")] + /// Returns [`ReadResponse::AssetState`] with an [`AssetState`](zebra_chain::orchard_zsa::AssetState) + /// of the provided [`AssetBase`] if it exists for the best chain tip or finalized chain tip (depending + /// on the `include_non_finalized` flag). + AssetState { + /// The [`AssetBase`] to return the asset state for. + asset_base: AssetBase, + /// Whether to include the issued asset state changes in the non-finalized state. + include_non_finalized: bool, + }, } impl ReadRequest { @@ -1159,6 +1142,8 @@ impl ReadRequest { ReadRequest::CheckBlockProposalValidity(_) => "check_block_proposal_validity", #[cfg(feature = "getblocktemplate-rpcs")] ReadRequest::TipBlockSize => "tip_block_size", + #[cfg(feature = "tx-v6")] + ReadRequest::AssetState { .. } => "asset_state", } } diff --git a/zebra-state/src/response.rs b/zebra-state/src/response.rs index 77c252b0c75..4372832a231 100644 --- a/zebra-state/src/response.rs +++ b/zebra-state/src/response.rs @@ -5,7 +5,9 @@ use std::{collections::BTreeMap, sync::Arc}; use zebra_chain::{ amount::{Amount, NonNegative}, block::{self, Block}, - orchard, sapling, + orchard, + orchard_zsa::AssetState, + sapling, serialization::DateTime32, subtree::{NoteCommitmentSubtreeData, NoteCommitmentSubtreeIndex}, transaction::{self, Transaction}, @@ -233,6 +235,10 @@ pub enum ReadResponse { #[cfg(feature = "getblocktemplate-rpcs")] /// Response to [`ReadRequest::TipBlockSize`] TipBlockSize(Option), + + #[cfg(feature = "tx-v6")] + /// Response to [`ReadRequest::AssetState`] + AssetState(Option), } /// A structure with the information needed from the state to build a `getblocktemplate` RPC response. @@ -322,6 +328,9 @@ impl TryFrom for Response { ReadResponse::ChainInfo(_) | ReadResponse::SolutionRate(_) | ReadResponse::TipBlockSize(_) => { Err("there is no corresponding Response for this ReadResponse") } + + #[cfg(feature = "tx-v6")] + ReadResponse::AssetState(_) => Err("there is no corresponding Response for this ReadResponse"), } } } diff --git a/zebra-state/src/service.rs b/zebra-state/src/service.rs index adc61f887ae..4f17950a312 100644 --- a/zebra-state/src/service.rs +++ b/zebra-state/src/service.rs @@ -1947,6 +1947,30 @@ impl Service for ReadStateService { }) .wait_for_panics() } + + #[cfg(feature = "tx-v6")] + ReadRequest::AssetState { + asset_base, + include_non_finalized, + } => { + let state = self.clone(); + + tokio::task::spawn_blocking(move || { + span.in_scope(move || { + let best_chain = include_non_finalized + .then(|| state.latest_best_chain()) + .flatten(); + + let response = read::asset_state(best_chain, &state.db, &asset_base); + + // The work is done in the future. + timer.finish(module_path!(), line!(), "ReadRequest::AssetState"); + + Ok(ReadResponse::AssetState(response)) + }) + }) + .wait_for_panics() + } } } } diff --git a/zebra-state/src/service/chain_tip.rs b/zebra-state/src/service/chain_tip.rs index 8a0ed517766..04ea61d6982 100644 --- a/zebra-state/src/service/chain_tip.rs +++ b/zebra-state/src/service/chain_tip.rs @@ -116,7 +116,6 @@ impl From for ChainTipBlock { new_outputs: _, transaction_hashes, deferred_balance: _, - issued_assets_changes: _, } = prepared; Self { diff --git a/zebra-state/src/service/check/issuance.rs b/zebra-state/src/service/check/issuance.rs index c54febef437..472ffd61fd8 100644 --- a/zebra-state/src/service/check/issuance.rs +++ b/zebra-state/src/service/check/issuance.rs @@ -2,26 +2,12 @@ use std::{collections::HashMap, sync::Arc}; -use zebra_chain::orchard_zsa::{AssetBase, AssetState, IssuedAssets}; +use zebra_chain::orchard_zsa::{AssetBase, AssetState, IssuedAssets, IssuedAssetsChange}; -use crate::{SemanticallyVerifiedBlock, ValidateContextError, ZebraDb}; +use crate::{service::read, 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, @@ -29,24 +15,21 @@ pub fn valid_burns_and_issuance( ) -> Result { let mut issued_assets = HashMap::new(); - // FIXME: Do all checks (for duplication, existence etc.) need to be performed per tranaction, not per - // the entire block? - for (issued_assets_change, transaction) in semantically_verified - .issued_assets_changes - .iter() - .zip(&semantically_verified.block.transactions) - { + // Burns need to be checked and asset state changes need to be applied per tranaction, in case + // the asset being burned was also issued in an earlier transaction in the same block. + for transaction in &semantically_verified.block.transactions { + let issued_assets_change = IssuedAssetsChange::from_transaction(transaction) + .ok_or(ValidateContextError::InvalidIssuance)?; + // 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) + // The asset being burned should have been issued by a previous transaction, and + // any assets issued in previous transactions should be present in the issued assets map. .ok_or(ValidateContextError::InvalidBurn)?; - // FIXME: The check that we can't burn an asset before we issued it is implicit - - // through the check it total_supply < burn.amount (the total supply is zero if the - // asset is not issued). May be validation functions from the orcharfd crate need to be - // reused in a some way? if asset_state.total_supply < burn.raw_amount() { return Err(ValidateContextError::InvalidBurn); } else { @@ -56,13 +39,8 @@ pub fn valid_burns_and_issuance( } } - // FIXME: Not sure: it looks like semantically_verified.issued_assets_changes is already - // filled with burn and issuance items in zebra-consensus, see Verifier::call function in - // zebra-consensus/src/transaction.rs (it uses from_burn and from_issue_action AssetStateChange - // methods from ebra-chain/src/orchard_zsa/asset_state.rs). Can't it cause a duplication? - // Can we collect all change items here, not in zebra-consensus (and so we don't need - // SemanticallyVerifiedBlock::issued_assets_changes at all), or performing part of the - // checks in zebra-consensus is important for the consensus checks order in a some way? + // TODO: Remove the `issued_assets_change` field from `SemanticallyVerifiedBlock` and get the changes + // directly from transactions here and when writing blocks to disk. for (asset_base, change) in issued_assets_change.iter() { let asset_state = asset_state(finalized_state, parent_chain, &issued_assets, &asset_base) @@ -72,15 +50,23 @@ pub fn valid_burns_and_issuance( .apply_change(change) .ok_or(ValidateContextError::InvalidIssuance)?; - // FIXME: Is it correct to do nothing if the issued_assets aready has asset_base? Now it'd be - // replaced with updated_asset_state in this case (where the duplicated value is added to - // the supply). Block may have two burn records for the same asset but in different - // transactions - it's allowed, that's why the check has been removed. On the other - // hand, there needs to be a check that denies duplicated burn records for the same - // asset inside the same transaction. + // TODO: Update `Burn` to `HashMap)` and return an error during deserialization if + // any asset base is burned twice in the same transaction issued_assets.insert(asset_base, updated_asset_state); } } Ok(issued_assets.into()) } + +fn asset_state( + finalized_state: &ZebraDb, + parent_chain: &Arc, + issued_assets: &HashMap, + asset_base: &AssetBase, +) -> Option { + issued_assets + .get(asset_base) + .copied() + .or_else(|| read::asset_state(Some(parent_chain), finalized_state, asset_base)) +} 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 d7df21fda0e..194f2202a87 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,7 +20,6 @@ use zebra_chain::{ }, Block, Height, }, - orchard_zsa::IssuedAssetsChange, parameters::Network::{self, *}, serialization::{ZcashDeserializeInto, ZcashSerialize}, transparent::new_ordered_outputs_with_height, @@ -130,9 +129,6 @@ 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_changes = - IssuedAssetsChange::from_transactions(&original_block.transactions) - .expect("issued assets should be valid"); CheckpointVerifiedBlock(SemanticallyVerifiedBlock { block: original_block.clone(), @@ -141,7 +137,6 @@ fn test_block_db_round_trip_with( new_outputs, transaction_hashes, deferred_balance: None, - issued_assets_changes, }) }; 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 1ca7e9cd3dc..7e5664f80ea 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/shielded.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/shielded.rs @@ -20,7 +20,7 @@ use std::{ use zebra_chain::{ block::Height, orchard::{self}, - orchard_zsa::{AssetBase, AssetState}, + orchard_zsa::{AssetBase, AssetState, IssuedAssetsChange}, parallel::tree::NoteCommitmentTrees, sapling, sprout, subtree::{NoteCommitmentSubtreeData, NoteCommitmentSubtreeIndex}, @@ -34,7 +34,7 @@ use crate::{ disk_format::RawBytes, zebra_db::ZebraDb, }, - BoxError, IssuedAssetsOrChange, TypedColumnFamily, + BoxError, TypedColumnFamily, }; // Doc-only items @@ -470,7 +470,7 @@ impl DiskWriteBatch { self.prepare_nullifier_batch(&zebra_db.db, transaction)?; } - self.prepare_issued_assets_batch(zebra_db, &finalized.issued_assets)?; + self.prepare_issued_assets_batch(zebra_db, finalized)?; Ok(()) } @@ -515,18 +515,23 @@ impl DiskWriteBatch { pub fn prepare_issued_assets_batch( &mut self, zebra_db: &ZebraDb, - issued_assets_or_changes: &IssuedAssetsOrChange, + finalized: &FinalizedBlock, ) -> 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() { - 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()), - }; - - for (asset_base, updated_issued_asset_state) in updated_issued_assets { - batch = batch.zs_insert(&asset_base, &updated_issued_asset_state); + let updated_issued_assets = + if let Some(updated_issued_assets) = finalized.issued_assets.as_ref() { + updated_issued_assets + } else { + &IssuedAssetsChange::from( + IssuedAssetsChange::from_transactions(&finalized.block.transactions) + .ok_or(BoxError::from("invalid issued assets changes"))?, + ) + .apply_with(|asset_base| zebra_db.issued_asset(&asset_base).unwrap_or_default()) + }; + + for (asset_base, updated_issued_asset_state) in updated_issued_assets.iter() { + batch = batch.zs_insert(asset_base, updated_issued_asset_state); } Ok(()) diff --git a/zebra-state/src/service/read.rs b/zebra-state/src/service/read.rs index 0188ca1bf5e..f2aa2f9adf8 100644 --- a/zebra-state/src/service/read.rs +++ b/zebra-state/src/service/read.rs @@ -34,8 +34,8 @@ pub use block::{ any_utxo, block, block_header, mined_transaction, transaction_hashes_for_block, unspent_utxo, }; pub use find::{ - best_tip, block_locator, depth, finalized_state_contains_block_hash, find_chain_hashes, - find_chain_headers, hash_by_height, height_by_hash, next_median_time_past, + asset_state, best_tip, block_locator, depth, finalized_state_contains_block_hash, + find_chain_hashes, find_chain_headers, hash_by_height, height_by_hash, next_median_time_past, non_finalized_state_contains_block_hash, tip, tip_height, tip_with_value_balance, }; pub use tree::{orchard_subtrees, orchard_tree, sapling_subtrees, sapling_tree}; diff --git a/zebra-state/src/service/read/find.rs b/zebra-state/src/service/read/find.rs index e9d557dbfb2..74347e61d04 100644 --- a/zebra-state/src/service/read/find.rs +++ b/zebra-state/src/service/read/find.rs @@ -21,6 +21,7 @@ use chrono::{DateTime, Utc}; use zebra_chain::{ amount::NonNegative, block::{self, Block, Height}, + orchard_zsa::{AssetBase, AssetState}, serialization::DateTime32, value_balance::ValueBalance, }; @@ -679,3 +680,13 @@ pub(crate) fn calculate_median_time_past(relevant_chain: Vec>) -> Dat DateTime32::try_from(median_time_past).expect("valid blocks have in-range times") } + +/// Return the [`AssetState`] for the provided [`AssetBase`], if it exists in the provided chain. +pub fn asset_state(chain: Option, db: &ZebraDb, asset_base: &AssetBase) -> Option +where + C: AsRef, +{ + chain + .and_then(|chain| chain.as_ref().issued_asset(asset_base)) + .or_else(|| db.issued_asset(asset_base)) +}