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/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000000..30e035b8e74 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.82.0" +components = [ "clippy", "rustfmt" ] 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/commitment.rs b/zebra-chain/src/orchard/commitment.rs index 2e6ea3ed32d..0724a2e9d1b 100644 --- a/zebra-chain/src/orchard/commitment.rs +++ b/zebra-chain/src/orchard/commitment.rs @@ -14,7 +14,7 @@ use halo2::{ use lazy_static::lazy_static; use rand_core::{CryptoRng, RngCore}; -use orchard::note::AssetBase; +use orchard::{note::AssetBase, value::NoteValue}; use crate::{ amount::Amount, @@ -255,8 +255,8 @@ impl ValueCommitment { /// Generate a new `ValueCommitment` from an existing `rcv on a `value` (ZSA version). #[cfg(feature = "tx-v6")] #[allow(non_snake_case)] - pub fn with_asset(rcv: pallas::Scalar, value: Amount, asset: &AssetBase) -> Self { - let v = pallas::Scalar::from(value); + pub fn with_asset(rcv: pallas::Scalar, value: NoteValue, asset: &AssetBase) -> Self { + let v = pallas::Scalar::from(value.inner()); let V_zsa = asset.cv_base(); Self::from(V_zsa * v + *R * rcv) } diff --git a/zebra-chain/src/orchard/orchard_flavor_ext.rs b/zebra-chain/src/orchard/orchard_flavor_ext.rs index 0ade1c75d39..faefaf6a8ba 100644 --- a/zebra-chain/src/orchard/orchard_flavor_ext.rs +++ b/zebra-chain/src/orchard/orchard_flavor_ext.rs @@ -15,7 +15,7 @@ use crate::{ }; #[cfg(feature = "tx-v6")] -use crate::orchard_zsa::{Burn, NoBurn}; +use crate::orchard_zsa::{Burn, BurnItem, NoBurn}; use super::note; @@ -58,7 +58,9 @@ pub trait OrchardFlavorExt: Clone + Debug { + Default + ZcashDeserialize + ZcashSerialize + // FIXME: consider using AsRef instead of Into, to avoid a redundancy + Into + + AsRef<[BurnItem]> + TestArbitrary; } diff --git a/zebra-chain/src/orchard_zsa.rs b/zebra-chain/src/orchard_zsa.rs index cb3dcd0aa28..d1e55db98b8 100644 --- a/zebra-chain/src/orchard_zsa.rs +++ b/zebra-chain/src/orchard_zsa.rs @@ -9,8 +9,14 @@ pub(crate) mod arbitrary; #[cfg(any(test, feature = "proptest-impl"))] pub mod tests; +pub mod asset_state; mod burn; mod issuance; pub(crate) use burn::{Burn, NoBurn}; pub(crate) use issuance::IssueData; + +pub use burn::BurnItem; + +// FIXME: should asset_state mod be pub and these structs be pub as well? +pub use asset_state::{AssetBase, AssetState, AssetStateChange, IssuedAssets, IssuedAssetsChange}; diff --git a/zebra-chain/src/orchard_zsa/arbitrary.rs b/zebra-chain/src/orchard_zsa/arbitrary.rs index 0dc89ce7080..f082c508025 100644 --- a/zebra-chain/src/orchard_zsa/arbitrary.rs +++ b/zebra-chain/src/orchard_zsa/arbitrary.rs @@ -19,13 +19,9 @@ impl Arbitrary for BurnItem { // FIXME: move arb_asset_to_burn out of BundleArb in orchard // as it does not depend on flavor (we pinned it here OrchardVanilla // just for certainty, as there's no difference, which flavor to use) - // FIXME: consider to use BurnItem(asset_base, value.try_into().expect("Invalid value for Amount")) - // instead of filtering non-convertable values // FIXME: should we filter/protect from including native assets into burn here? BundleArb::::arb_asset_to_burn() - .prop_filter_map("Conversion to Amount failed", |(asset_base, value)| { - BurnItem::try_from((asset_base, value)).ok() - }) + .prop_map(|(asset_base, value)| BurnItem::from((asset_base, value))) .boxed() } diff --git a/zebra-chain/src/orchard_zsa/asset_state.rs b/zebra-chain/src/orchard_zsa/asset_state.rs new file mode 100644 index 00000000000..264951bfd52 --- /dev/null +++ b/zebra-chain/src/orchard_zsa/asset_state.rs @@ -0,0 +1,410 @@ +//! Defines and implements the issued asset state types + +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; + +use orchard::issuance::IssueAction; +pub use orchard::note::AssetBase; + +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, serde::Serialize)] +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: u64, +} + +/// A change to apply to the issued assets map. +// TODO: Reference ZIP +#[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 should_finalize: bool, + /// 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, +} + +#[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), +} + +impl Default for SupplyChange { + fn default() -> Self { + Self::Issuance(0) + } +} + +// FIXME: can we reuse some functions from orchard crate?s +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 { + match self { + SupplyChange::Issuance(amount) => total_supply.checked_add(amount), + SupplyChange::Burn(amount) => total_supply.checked_sub(amount), + } + } + + /// 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), + SupplyChange::Burn(amount) => -i128::from(amount), + } + } + + /// 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), + // FIXME: (-signed) - is this a correct fix? + ..0 => (-signed).try_into().ok().map(Self::Burn), + }) + { + *self = result; + true + } else { + 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 { + type Output = Self; + + fn neg(self) -> Self::Output { + match self { + Self::Issuance(amount) => Self::Burn(amount), + Self::Burn(amount) => Self::Issuance(amount), + } + } +} + +impl AssetState { + /// Updates and returns self with the provided [`AssetStateChange`] if + /// the change is valid, or returns None otherwise. + pub fn apply_change(self, change: AssetStateChange) -> Option { + 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 { + if self.is_finalized && change.includes_issuance { + None + } else { + 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.total_supply = change.supply_change.apply_to(self.total_supply)?; + Some(self) + } + + /// Reverts the provided [`AssetStateChange`]. + pub fn revert_change(&mut self, change: AssetStateChange) { + *self = self + .revert_finalization(change.should_finalize) + .revert_supply_change(change) + .expect("reverted change should be validated"); + } + + /// 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.total_supply = (-change.supply_change).apply_to(self.total_supply)?; + Some(self) + } +} + +impl From> for IssuedAssets { + fn from(issued_assets: HashMap) -> Self { + Self(issued_assets) + } +} + +impl AssetStateChange { + /// Creates a new [`AssetStateChange`] from an asset base, supply change, and + /// `should_finalize` flag. + fn new( + asset_base: AssetBase, + supply_change: SupplyChange, + should_finalize: bool, + ) -> (AssetBase, Self) { + ( + asset_base, + Self { + 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) -> impl Iterator + '_ { + 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 + 'a, + ) -> impl Iterator + '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 + '_ { + let supply_changes = Self::from_notes(action.notes()); + let finalize_changes = action + .is_finalized() + .then(|| { + action + .notes() + .iter() + .map(orchard::Note::asset) + .collect::>() + }) + .unwrap_or_default() + .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 + '_ { + 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, + ) + } + + /// 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 + '_ { + 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.raw_amount()), false) + } + + /// 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.should_finalize && change.includes_issuance { + return false; + } + self.should_finalize |= change.should_finalize; + self.includes_issuance |= change.includes_issuance; + self.supply_change.add(change.supply_change) + } +} + +/// An map of issued asset states by asset base. +// TODO: Reference ZIP +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct IssuedAssets(HashMap); + +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 { + self.0.iter() + } + + /// Extends inner [`HashMap`] with updated asset states from the provided iterator + fn extend<'a>(&mut self, issued_assets: impl Iterator + 'a) { + self.0.extend(issued_assets); + } +} + +impl IntoIterator for IssuedAssets { + type Item = (AssetBase, AssetState); + + type IntoIter = std::collections::hash_map::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +/// A map of changes to apply to the issued assets map. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct IssuedAssetsChange(HashMap); + +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 + 'a, + ) -> bool { + for (asset_base, change) in changes { + if !self.0.entry(asset_base).or_default().apply_change(change) { + return false; + } + } + + true + } + + /// Accepts a [`Arc`]. + /// + /// 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) -> Option { + 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`]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]) -> Option> { + transactions.iter().map(Self::from_transaction).collect() + } + + /// 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.extend(self.0.into_iter().map(|(asset_base, change)| { + ( + asset_base, + f(asset_base) + .apply_change(change) + .expect("must be valid change"), + ) + })); + + issued_assets + } + + /// Iterates over the inner [`HashMap`] of asset bases and state changes. + pub fn iter(&self) -> impl Iterator + '_ { + self.0.iter().map(|(&base, &state)| (base, state)) + } +} + +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 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-chain/src/orchard_zsa/burn.rs b/zebra-chain/src/orchard_zsa/burn.rs index 75301a47008..fbbd1787ce4 100644 --- a/zebra-chain/src/orchard_zsa/burn.rs +++ b/zebra-chain/src/orchard_zsa/burn.rs @@ -4,8 +4,9 @@ use std::io; use halo2::pasta::pallas; +use group::prime::PrimeCurveAffine; + use crate::{ - amount::Amount, block::MAX_BLOCK_BYTES, orchard::ValueCommitment, serialization::{ @@ -37,16 +38,32 @@ const AMOUNT_SIZE: u64 = 8; // FIXME: is this a correct way to calculate (simple sum of sizes of components)? const BURN_ITEM_SIZE: u64 = ASSET_BASE_SIZE + AMOUNT_SIZE; +// FIXME: Define BurnItem (or, even Burn/NoBurn) in Orchard and reuse it here? /// Orchard ZSA burn item. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct BurnItem(AssetBase, Amount); +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct BurnItem(AssetBase, NoteValue); -// Convert from burn item type used in `orchard` crate -impl TryFrom<(AssetBase, NoteValue)> for BurnItem { - type Error = crate::amount::Error; +impl BurnItem { + /// Returns [`AssetBase`] being burned. + pub fn asset(&self) -> AssetBase { + self.0 + } + + /// Returns the amount being burned. + pub fn amount(&self) -> NoteValue { + self.1 + } + + /// Returns the raw [`u64`] amount being burned. + pub fn raw_amount(&self) -> u64 { + self.1.inner() + } +} - fn try_from(item: (AssetBase, NoteValue)) -> Result { - Ok(Self(item.0, item.1.inner().try_into()?)) +// Convert from burn item type used in `orchard` crate +impl From<(AssetBase, NoteValue)> for BurnItem { + fn from(item: (AssetBase, NoteValue)) -> Self { + Self(item.0, item.1) } } @@ -55,7 +72,7 @@ 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_bytes())?; Ok(()) } @@ -63,10 +80,10 @@ impl ZcashSerialize for BurnItem { impl ZcashDeserialize for BurnItem { fn zcash_deserialize(mut reader: R) -> Result { - Ok(Self( - AssetBase::zcash_deserialize(&mut reader)?, - Amount::zcash_deserialize(&mut reader)?, - )) + let asset_base = AssetBase::zcash_deserialize(&mut reader)?; + let mut amount_bytes = [0; 8]; + reader.read_exact(&mut amount_bytes)?; + Ok(Self(asset_base, NoteValue::from_bytes(amount_bytes))) } } @@ -85,7 +102,7 @@ impl serde::Serialize for BurnItem { S: serde::Serializer, { // FIXME: return a custom error with a meaningful description? - (self.0.to_bytes(), &self.1).serialize(serializer) + (self.0.to_bytes(), &self.1.inner()).serialize(serializer) } } @@ -95,13 +112,13 @@ 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? Option::from(AssetBase::from_bytes(&asset_base_bytes)) .ok_or_else(|| serde::de::Error::custom("Invalid orchard_zsa AssetBase"))?, - amount, + NoteValue::from_raw(amount), )) } } @@ -114,8 +131,13 @@ pub struct NoBurn; impl From for ValueCommitment { fn from(_burn: NoBurn) -> ValueCommitment { - // FIXME: is there a simpler way to get zero ValueCommitment? - ValueCommitment::new(pallas::Scalar::zero(), Amount::zero()) + ValueCommitment(pallas::Affine::identity()) + } +} + +impl AsRef<[BurnItem]> for NoBurn { + fn as_ref(&self) -> &[BurnItem] { + &[] } } @@ -153,6 +175,12 @@ impl From for ValueCommitment { } } +impl AsRef<[BurnItem]> for Burn { + fn as_ref(&self) -> &[BurnItem] { + &self.0 + } +} + impl ZcashSerialize for Burn { fn zcash_serialize(&self, writer: W) -> Result<(), io::Error> { self.0.zcash_serialize(writer) diff --git a/zebra-chain/src/orchard_zsa/issuance.rs b/zebra-chain/src/orchard_zsa/issuance.rs index 4a3cb968926..36b31f65de7 100644 --- a/zebra-chain/src/orchard_zsa/issuance.rs +++ b/zebra-chain/src/orchard_zsa/issuance.rs @@ -53,6 +53,11 @@ impl IssueData { }) }) } + + /// Returns issuance actions + pub fn actions(&self) -> impl Iterator { + self.0.actions().iter() + } } // Sizes of the serialized values for types in bytes (used for TrustedPreallocate impls) diff --git a/zebra-chain/src/transaction.rs b/zebra-chain/src/transaction.rs index 737253d6eab..c72002c4b76 100644 --- a/zebra-chain/src/transaction.rs +++ b/zebra-chain/src/transaction.rs @@ -78,6 +78,31 @@ macro_rules! orchard_shielded_data_iter { }; } +macro_rules! orchard_shielded_data_map { + ($self:expr, $mapper:expr, $mapper2:expr) => { + match $self { + Transaction::V5 { + orchard_shielded_data: Some(shielded_data), + .. + } => $mapper(shielded_data), + + #[cfg(feature = "tx-v6")] + Transaction::V6 { + orchard_shielded_data: Some(shielded_data), + .. + } => $mapper2(shielded_data), + + // No Orchard shielded data + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { .. } + | Transaction::V5 { .. } + | Transaction::V6 { .. } => &[], + } + }; +} + // FIXME: doc this // Move down macro_rules! orchard_shielded_data_field { @@ -1071,6 +1096,45 @@ 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 { + match self { + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { .. } + | Transaction::V5 { .. } => &None, + + Transaction::V6 { + orchard_zsa_issue_data, + .. + } => orchard_zsa_issue_data, + } + } + + /// 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 { + 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")] + pub fn orchard_burns<'a>(&'a self) -> &[orchard_zsa::BurnItem] { + use crate::orchard::{OrchardVanilla, OrchardZSA}; + orchard_shielded_data_map!( + self, + |data: &'a orchard::ShieldedData| data.burn.as_ref(), + |data: &'a orchard::ShieldedData| data.burn.as_ref() + ) + } + /// Access the [`orchard::Flags`] in this transaction, if there is any, /// regardless of version. pub fn orchard_flags(&self) -> Option { diff --git a/zebra-chain/src/transaction/serialize.rs b/zebra-chain/src/transaction/serialize.rs index b3ed12ebd7d..35f297dcae6 100644 --- a/zebra-chain/src/transaction/serialize.rs +++ b/zebra-chain/src/transaction/serialize.rs @@ -1105,7 +1105,8 @@ pub const MIN_TRANSPARENT_TX_V5_SIZE: u64 = MIN_TRANSPARENT_TX_SIZE + 4 + 4; /// The minimum transaction size for v6 transactions. /// -/// FIXME: uncomment this and specify a proper value and description. +#[allow(clippy::empty_line_after_doc_comments)] +/// FIXME: remove "clippy" line above, uncomment line below and specify a proper value and description. //pub const MIN_TRANSPARENT_TX_V6_SIZE: u64 = MIN_TRANSPARENT_TX_V5_SIZE; /// No valid Zcash message contains more transactions than can fit in a single block diff --git a/zebra-consensus/src/block.rs b/zebra-consensus/src/block.rs index 611aea2ceba..207a202f6ea 100644 --- a/zebra-consensus/src/block.rs +++ b/zebra-consensus/src/block.rs @@ -256,20 +256,22 @@ where 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, + } = result .map_err(Into::into) - .map_err(VerifyBlockError::Transaction)?; - - assert!( - matches!(response, tx::Response::Block { .. }), - "unexpected response from transaction verifier: {response:?}" - ); + .map_err(VerifyBlockError::Transaction)? + else { + panic!("unexpected response from transaction verifier"); + }; - 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; } } diff --git a/zebra-consensus/src/lib.rs b/zebra-consensus/src/lib.rs index c61a1fe408d..d2e0eb46357 100644 --- a/zebra-consensus/src/lib.rs +++ b/zebra-consensus/src/lib.rs @@ -66,4 +66,4 @@ pub use router::RouterError; /// A boxed [`std::error::Error`]. pub type BoxError = Box; -mod zsa; +mod orchard_zsa; diff --git a/zebra-consensus/src/zsa.rs b/zebra-consensus/src/orchard_zsa.rs similarity index 100% rename from zebra-consensus/src/zsa.rs rename to zebra-consensus/src/orchard_zsa.rs diff --git a/zebra-consensus/src/orchard_zsa/tests.rs b/zebra-consensus/src/orchard_zsa/tests.rs new file mode 100644 index 00000000000..b2220835d79 --- /dev/null +++ b/zebra-consensus/src/orchard_zsa/tests.rs @@ -0,0 +1,179 @@ +// FIXME: consider merging it with router/tests.rs + +use std::sync::Arc; + +use color_eyre::eyre::Report; +use tower::ServiceExt; + +use orchard::{ + issuance::Error as IssuanceError, + issuance::IssueAction, + note::AssetBase, + supply_info::{AssetSupply, SupplyInfo}, + value::ValueSum, +}; + +use zebra_chain::{ + block::{genesis::regtest_genesis_block, Block, Hash}, + orchard_zsa::{AssetState, BurnItem}, + parameters::Network, + serialization::ZcashDeserialize, +}; + +use zebra_state::{ReadRequest, ReadResponse, ReadStateService}; + +use zebra_test::{ + transcript::{ExpectedTranscriptError, Transcript}, + vectors::ORCHARD_ZSA_WORKFLOW_BLOCKS, +}; + +use crate::{block::Request, Config}; + +type TranscriptItem = (Request, Result); + +/// Processes orchard burns, decreasing asset supply. +fn process_burns<'a, I: Iterator>( + supply_info: &mut SupplyInfo, + burns: I, +) -> Result<(), IssuanceError> { + for burn in burns { + // Burns reduce supply, so negate the amount. + let amount = (-ValueSum::from(burn.amount())).ok_or(IssuanceError::ValueSumOverflow)?; + + supply_info.add_supply( + burn.asset(), + AssetSupply { + amount, + is_finalized: false, + }, + )?; + } + + Ok(()) +} + +/// Processes orchard issue actions, increasing asset supply. +fn process_issue_actions<'a, I: Iterator>( + supply_info: &mut SupplyInfo, + issue_actions: I, +) -> Result<(), IssuanceError> { + for action in issue_actions { + let is_finalized = action.is_finalized(); + + for note in action.notes() { + supply_info.add_supply( + note.asset(), + AssetSupply { + amount: note.value().into(), + is_finalized, + }, + )?; + } + } + + Ok(()) +} + +/// Calculates supply info for all assets in the given blocks. +fn calc_asset_supply_info<'a, I: IntoIterator>( + blocks: I, +) -> Result { + blocks + .into_iter() + .filter_map(|(request, _)| match request { + Request::Commit(block) => Some(&block.transactions), + #[cfg(feature = "getblocktemplate-rpcs")] + Request::CheckProposal(_) => None, + }) + .flatten() + .try_fold(SupplyInfo::new(), |mut supply_info, tx| { + process_burns(&mut supply_info, tx.orchard_burns().iter())?; + process_issue_actions(&mut supply_info, tx.orchard_issue_actions())?; + Ok(supply_info) + }) +} + +/// Creates transcript data from predefined workflow blocks. +fn create_transcript_data<'a, I: IntoIterator>>( + serialized_blocks: I, +) -> impl Iterator + use<'a, I> { + let workflow_blocks = serialized_blocks.into_iter().map(|block_bytes| { + Arc::new(Block::zcash_deserialize(&block_bytes[..]).expect("block should deserialize")) + }); + + std::iter::once(regtest_genesis_block()) + .chain(workflow_blocks) + .map(|block| (Request::Commit(block.clone()), Ok(block.hash()))) +} + +/// Queries the state service for the asset state of the given asset. +async fn request_asset_state( + read_state_service: &ReadStateService, + asset_base: AssetBase, +) -> Option { + let request = ReadRequest::AssetState { + asset_base, + include_non_finalized: true, + }; + + match read_state_service.clone().oneshot(request).await { + Ok(ReadResponse::AssetState(asset_state)) => asset_state, + _ => unreachable!("The state service returned an unexpected response."), + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn check_zsa_workflow() -> Result<(), Report> { + let _init_guard = zebra_test::init(); + + let network = Network::new_regtest(Some(1), Some(1), Some(1)); + + let (state_service, read_state_service, _, _) = zebra_state::init_test_services(&network); + + let (block_verifier_router, _tx_verifier, _groth16_download_handle, _max_checkpoint_height) = + crate::router::init(Config::default(), &network, state_service.clone()).await; + + let transcript_data = + create_transcript_data(ORCHARD_ZSA_WORKFLOW_BLOCKS.iter()).collect::>(); + + let asset_supply_info = + calc_asset_supply_info(&transcript_data).expect("should calculate asset_supply_info"); + + // Before applying the blocks, ensure that none of the assets exist in the state. + for &asset_base in asset_supply_info.assets.keys() { + assert!( + request_asset_state(&read_state_service, asset_base) + .await + .is_none(), + "State should initially have no info about this asset." + ); + } + + // Verify all blocks in the transcript against the consensus and the state. + Transcript::from(transcript_data) + .check(block_verifier_router.clone()) + .await?; + + // After processing the transcript blocks, verify that the state matches the expected supply info. + for (&asset_base, asset_supply) in &asset_supply_info.assets { + let asset_state = request_asset_state(&read_state_service, asset_base) + .await + .expect("State should contain this asset now."); + + assert_eq!( + asset_state.is_finalized, asset_supply.is_finalized, + "Finalized state does not match for asset {:?}.", + asset_base + ); + + assert_eq!( + asset_state.total_supply, + u64::try_from(i128::from(asset_supply.amount)) + .expect("asset supply amount should be within u64 range"), + "Total supply mismatch for asset {:?}.", + asset_base + ); + } + + Ok(()) +} diff --git a/zebra-consensus/src/zsa/tests.rs b/zebra-consensus/src/zsa/tests.rs deleted file mode 100644 index dbea27190bb..00000000000 --- a/zebra-consensus/src/zsa/tests.rs +++ /dev/null @@ -1,49 +0,0 @@ -// FIXME: consider merging it with router/tests.rs - -use std::sync::Arc; - -use color_eyre::eyre::Report; - -use zebra_chain::{ - block::{genesis::regtest_genesis_block, Block, Hash}, - parameters::Network, - serialization::ZcashDeserialize, -}; - -use zebra_test::{ - transcript::{ExpectedTranscriptError, Transcript}, - vectors::ZSA_WORKFLOW_BLOCKS, -}; - -use crate::{block::Request, Config}; - -fn create_transcript_data() -> impl Iterator)> -{ - let workflow_blocks = ZSA_WORKFLOW_BLOCKS.iter().map(|block_bytes| { - Arc::new(Block::zcash_deserialize(&block_bytes[..]).expect("block should deserialize")) - }); - - std::iter::once(regtest_genesis_block()) - .chain(workflow_blocks) - .map(|block| (Request::Commit(block.clone()), Ok(block.hash()))) -} - -#[tokio::test(flavor = "multi_thread")] -async fn check_zsa_workflow() -> Result<(), Report> { - let _init_guard = zebra_test::init(); - - let network = Network::new_regtest(Some(1), Some(1), Some(1)); - - let state_service = zebra_state::init_test(&network); - - let ( - block_verifier_router, - _transaction_verifier, - _groth16_download_handle, - _max_checkpoint_height, - ) = crate::router::init(Config::default(), &network, state_service.clone()).await; - - Transcript::from(create_transcript_data()) - .check(block_verifier_router.clone()) - .await -} 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-rpc/src/sync.rs b/zebra-rpc/src/sync.rs index fd323ef64bb..787b2e7c5a8 100644 --- a/zebra-rpc/src/sync.rs +++ b/zebra-rpc/src/sync.rs @@ -13,8 +13,8 @@ use zebra_chain::{ }; use zebra_node_services::rpc_client::RpcRequestClient; use zebra_state::{ - spawn_init_read_only, ChainTipBlock, ChainTipChange, ChainTipSender, CheckpointVerifiedBlock, - LatestChainTip, NonFinalizedState, ReadStateService, SemanticallyVerifiedBlock, ZebraDb, + spawn_init_read_only, ChainTipBlock, ChainTipChange, ChainTipSender, LatestChainTip, + NonFinalizedState, ReadStateService, SemanticallyVerifiedBlock, ZebraDb, MAX_BLOCK_REORG_HEIGHT, }; @@ -262,7 +262,7 @@ impl TrustedChainSync { tokio::task::spawn_blocking(move || { let (height, hash) = db.tip()?; db.block(height.into()) - .map(|block| CheckpointVerifiedBlock::with_hash(block, hash)) + .map(|block| SemanticallyVerifiedBlock::with_hash(block, hash)) .map(ChainTipBlock::from) }) .wait_for_panics() diff --git a/zebra-state/src/arbitrary.rs b/zebra-state/src/arbitrary.rs index 5c0b837566a..352ad550159 100644 --- a/zebra-state/src/arbitrary.rs +++ b/zebra-state/src/arbitrary.rs @@ -96,8 +96,12 @@ impl ContextuallyVerifiedBlock { .map(|outpoint| (outpoint, zero_utxo.clone())) .collect(); - ContextuallyVerifiedBlock::with_block_and_spent_utxos(block, zero_spent_utxos) - .expect("all UTXOs are provided with zero values") + ContextuallyVerifiedBlock::with_block_and_spent_utxos( + block, + zero_spent_utxos, + Default::default(), + ) + .expect("all UTXOs are provided with zero values") } /// Create a [`ContextuallyVerifiedBlock`] from a [`Block`] or [`SemanticallyVerifiedBlock`], @@ -125,6 +129,7 @@ impl ContextuallyVerifiedBlock { spent_outputs: new_outputs, transaction_hashes, chain_value_pool_change: ValueBalance::zero(), + issued_assets: Default::default(), } } } diff --git a/zebra-state/src/error.rs b/zebra-state/src/error.rs index cf495311efb..4a20f5c29d1 100644 --- a/zebra-state/src/error.rs +++ b/zebra-state/src/error.rs @@ -264,6 +264,12 @@ pub enum ValidateContextError { tx_index_in_block: Option, transaction_hash: transaction::Hash, }, + + #[error("burn amounts must be less than issued asset supply")] + InvalidBurn, + + #[error("must not issue finalized assets")] + InvalidIssuance, } /// Trait for creating the corresponding duplicate nullifier error from a nullifier. diff --git a/zebra-state/src/lib.rs b/zebra-state/src/lib.rs index e93a3b8f905..7cfc8304bdd 100644 --- a/zebra-state/src/lib.rs +++ b/zebra-state/src/lib.rs @@ -42,7 +42,8 @@ pub use error::{ ValidateContextError, }; pub use request::{ - CheckpointVerifiedBlock, HashOrHeight, ReadRequest, Request, SemanticallyVerifiedBlock, + CheckpointVerifiedBlock, HashOrHeight, IssuedAssetsOrChange, ReadRequest, Request, + SemanticallyVerifiedBlock, }; pub use response::{KnownBlock, MinedTx, ReadResponse, Response}; pub use service::{ diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 56be011d48e..cd71173caae 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -11,6 +11,7 @@ use zebra_chain::{ block::{self, Block}, history_tree::HistoryTree, orchard, + orchard_zsa::{AssetBase, IssuedAssets, IssuedAssetsChange}, parallel::tree::NoteCommitmentTrees, sapling, serialization::SerializationError, @@ -223,6 +224,10 @@ pub struct ContextuallyVerifiedBlock { /// The sum of the chain value pool changes of all transactions in this block. pub(crate) chain_value_pool_change: ValueBalance, + + /// A partial map of `issued_assets` with entries for asset states that were updated in + /// this block. + pub(crate) issued_assets: IssuedAssets, } /// Wraps note commitment trees and the history tree together. @@ -293,12 +298,33 @@ pub struct FinalizedBlock { pub(super) treestate: Treestate, /// This block's contribution to the deferred pool. pub(super) deferred_balance: Option>, + /// Updated asset states to be inserted into the finalized state, replacing the previous + /// asset states for those asset bases. + pub issued_assets: 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 +/// asset states for those asset bases. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum IssuedAssetsOrChange { + /// A map of updated issued assets. + Updated(IssuedAssets), + + /// A map of changes to apply to the issued assets map. + Change(IssuedAssetsChange), +} + +impl From for IssuedAssetsOrChange { + fn from(updated_issued_assets: IssuedAssets) -> Self { + Self::Updated(updated_issued_assets) + } } impl FinalizedBlock { /// Constructs [`FinalizedBlock`] from [`CheckpointVerifiedBlock`] and its [`Treestate`]. pub fn from_checkpoint_verified(block: CheckpointVerifiedBlock, treestate: Treestate) -> Self { - Self::from_semantically_verified(SemanticallyVerifiedBlock::from(block), treestate) + Self::from_semantically_verified(SemanticallyVerifiedBlock::from(block), treestate, None) } /// Constructs [`FinalizedBlock`] from [`ContextuallyVerifiedBlock`] and its [`Treestate`]. @@ -306,11 +332,20 @@ impl FinalizedBlock { block: ContextuallyVerifiedBlock, treestate: Treestate, ) -> Self { - Self::from_semantically_verified(SemanticallyVerifiedBlock::from(block), treestate) + let issued_assets = Some(block.issued_assets.clone()); + Self::from_semantically_verified( + SemanticallyVerifiedBlock::from(block), + treestate, + issued_assets, + ) } /// Constructs [`FinalizedBlock`] from [`SemanticallyVerifiedBlock`] and its [`Treestate`]. - fn from_semantically_verified(block: SemanticallyVerifiedBlock, treestate: Treestate) -> Self { + fn from_semantically_verified( + block: SemanticallyVerifiedBlock, + treestate: Treestate, + issued_assets: Option, + ) -> Self { Self { block: block.block, hash: block.hash, @@ -319,6 +354,7 @@ impl FinalizedBlock { transaction_hashes: block.transaction_hashes, treestate, deferred_balance: block.deferred_balance, + issued_assets, } } } @@ -384,6 +420,7 @@ impl ContextuallyVerifiedBlock { pub fn with_block_and_spent_utxos( semantically_verified: SemanticallyVerifiedBlock, mut spent_outputs: HashMap, + issued_assets: IssuedAssets, ) -> Result { let SemanticallyVerifiedBlock { block, @@ -411,6 +448,7 @@ impl ContextuallyVerifiedBlock { &utxos_from_ordered_utxos(spent_outputs), deferred_balance, )?, + issued_assets, }) } } @@ -427,6 +465,7 @@ impl CheckpointVerifiedBlock { block.deferred_balance = deferred_balance; block } + /// Creates a block that's ready to be committed to the finalized state, /// using a precalculated [`block::Hash`]. /// @@ -465,7 +504,7 @@ impl SemanticallyVerifiedBlock { impl From> for CheckpointVerifiedBlock { fn from(block: Arc) -> Self { - CheckpointVerifiedBlock(SemanticallyVerifiedBlock::from(block)) + Self(SemanticallyVerifiedBlock::from(block)) } } @@ -508,19 +547,6 @@ impl From for SemanticallyVerifiedBlock { } } -impl From for SemanticallyVerifiedBlock { - fn from(finalized: FinalizedBlock) -> Self { - Self { - block: finalized.block, - hash: finalized.hash, - height: finalized.height, - new_outputs: finalized.new_outputs, - transaction_hashes: finalized.transaction_hashes, - deferred_balance: finalized.deferred_balance, - } - } -} - impl From for SemanticallyVerifiedBlock { fn from(checkpoint_verified: CheckpointVerifiedBlock) -> Self { checkpoint_verified.0 @@ -1068,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 { @@ -1105,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/check.rs b/zebra-state/src/service/check.rs index ced63bfea16..d2eaeff4e5a 100644 --- a/zebra-state/src/service/check.rs +++ b/zebra-state/src/service/check.rs @@ -28,6 +28,7 @@ use crate::service::non_finalized_state::Chain; pub(crate) mod anchors; pub(crate) mod difficulty; +pub(crate) mod issuance; pub(crate) mod nullifier; pub(crate) mod utxo; diff --git a/zebra-state/src/service/check/issuance.rs b/zebra-state/src/service/check/issuance.rs new file mode 100644 index 00000000000..472ffd61fd8 --- /dev/null +++ b/zebra-state/src/service/check/issuance.rs @@ -0,0 +1,72 @@ +//! Checks for issuance and burn validity. + +use std::{collections::HashMap, sync::Arc}; + +use zebra_chain::orchard_zsa::{AssetBase, AssetState, IssuedAssets, IssuedAssetsChange}; + +use crate::{service::read, SemanticallyVerifiedBlock, ValidateContextError, ZebraDb}; + +use super::Chain; + +pub fn valid_burns_and_issuance( + finalized_state: &ZebraDb, + parent_chain: &Arc, + semantically_verified: &SemanticallyVerifiedBlock, +) -> Result { + let mut issued_assets = HashMap::new(); + + // 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)?; + + if asset_state.total_supply < burn.raw_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); + } + } + + // 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) + .unwrap_or_default(); + + let updated_asset_state = asset_state + .apply_change(change) + .ok_or(ValidateContextError::InvalidIssuance)?; + + // 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.rs b/zebra-state/src/service/finalized_state.rs index f8c9bade5c1..94328d9e51f 100644 --- a/zebra-state/src/service/finalized_state.rs +++ b/zebra-state/src/service/finalized_state.rs @@ -91,6 +91,7 @@ pub const STATE_COLUMN_FAMILIES_IN_CODE: &[&str] = &[ "orchard_anchors", "orchard_note_commitment_tree", "orchard_note_commitment_subtree", + "orchard_issued_assets", // Chain "history_tree", "tip_chain_value_pool", diff --git a/zebra-state/src/service/finalized_state/disk_format/shielded.rs b/zebra-state/src/service/finalized_state/disk_format/shielded.rs index bcd24d5c604..cb2844d4c08 100644 --- a/zebra-state/src/service/finalized_state/disk_format/shielded.rs +++ b/zebra-state/src/service/finalized_state/disk_format/shielded.rs @@ -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 FromDisk for NoteCommitmentSubtreeData { ) } } + +// 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), + } + } +} + +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() + } +} diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/column_family_names.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/column_family_names.snap index d37e037cac7..33f1c76717b 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/column_family_names.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/column_family_names.snap @@ -12,6 +12,7 @@ expression: cf_names "height_by_hash", "history_tree", "orchard_anchors", + "orchard_issued_assets", "orchard_note_commitment_subtree", "orchard_note_commitment_tree", "orchard_nullifiers", diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_0.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_0.snap index 3c333a9fc43..abd4ae001ec 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_0.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_0.snap @@ -5,6 +5,7 @@ expression: empty_column_families [ "balance_by_transparent_addr: no entries", "history_tree: no entries", + "orchard_issued_assets: no entries", "orchard_note_commitment_subtree: no entries", "orchard_nullifiers: no entries", "sapling_note_commitment_subtree: no entries", diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_1.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_1.snap index cb8ac5f6aed..8b114ddce4d 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_1.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_1.snap @@ -4,6 +4,7 @@ expression: empty_column_families --- [ "history_tree: no entries", + "orchard_issued_assets: no entries", "orchard_note_commitment_subtree: no entries", "orchard_nullifiers: no entries", "sapling_note_commitment_subtree: no entries", diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_2.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_2.snap index cb8ac5f6aed..8b114ddce4d 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_2.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_2.snap @@ -4,6 +4,7 @@ expression: empty_column_families --- [ "history_tree: no entries", + "orchard_issued_assets: no entries", "orchard_note_commitment_subtree: no entries", "orchard_nullifiers: no entries", "sapling_note_commitment_subtree: no entries", diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@no_blocks.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@no_blocks.snap index a2abce2083b..2d119139d26 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@no_blocks.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@no_blocks.snap @@ -11,6 +11,7 @@ expression: empty_column_families "height_by_hash: no entries", "history_tree: no entries", "orchard_anchors: no entries", + "orchard_issued_assets: no entries", "orchard_note_commitment_subtree: no entries", "orchard_note_commitment_tree: no entries", "orchard_nullifiers: no entries", diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_0.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_0.snap index 3c333a9fc43..abd4ae001ec 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_0.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_0.snap @@ -5,6 +5,7 @@ expression: empty_column_families [ "balance_by_transparent_addr: no entries", "history_tree: no entries", + "orchard_issued_assets: no entries", "orchard_note_commitment_subtree: no entries", "orchard_nullifiers: no entries", "sapling_note_commitment_subtree: no entries", diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_1.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_1.snap index cb8ac5f6aed..8b114ddce4d 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_1.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_1.snap @@ -4,6 +4,7 @@ expression: empty_column_families --- [ "history_tree: no entries", + "orchard_issued_assets: no entries", "orchard_note_commitment_subtree: no entries", "orchard_nullifiers: no entries", "sapling_note_commitment_subtree: no entries", diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_2.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_2.snap index cb8ac5f6aed..8b114ddce4d 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_2.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_2.snap @@ -4,6 +4,7 @@ expression: empty_column_families --- [ "history_tree: no entries", + "orchard_issued_assets: no entries", "orchard_note_commitment_subtree: no entries", "orchard_nullifiers: no entries", "sapling_note_commitment_subtree: no entries", diff --git a/zebra-state/src/service/finalized_state/zebra_db/block.rs b/zebra-state/src/service/finalized_state/zebra_db/block.rs index 4dc3a801ef3..6f0d2340b91 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/block.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/block.rs @@ -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 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 4bba75b1891..7e5664f80ea 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/shielded.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/shielded.rs @@ -19,7 +19,8 @@ use std::{ use zebra_chain::{ block::Height, - orchard, + orchard::{self}, + orchard_zsa::{AssetBase, AssetState, IssuedAssetsChange}, parallel::tree::NoteCommitmentTrees, sapling, sprout, subtree::{NoteCommitmentSubtreeData, NoteCommitmentSubtreeIndex}, @@ -33,14 +34,31 @@ use crate::{ disk_format::RawBytes, zebra_db::ZebraDb, }, - BoxError, + BoxError, 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 { + 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)?; + Ok(()) } @@ -480,6 +505,38 @@ 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, + finalized: &FinalizedBlock, + ) -> Result<(), BoxError> { + let mut batch = zebra_db.issued_assets_cf().with_batch_for_writing(self); + + 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(()) + } + /// Prepare a database batch containing the note commitment and history tree updates /// from `finalized.block`, and return it (without actually writing anything). /// diff --git a/zebra-state/src/service/non_finalized_state.rs b/zebra-state/src/service/non_finalized_state.rs index 08d64455024..1ca33cb43f4 100644 --- a/zebra-state/src/service/non_finalized_state.rs +++ b/zebra-state/src/service/non_finalized_state.rs @@ -325,6 +325,9 @@ impl NonFinalizedState { finalized_state, )?; + let issued_assets = + check::issuance::valid_burns_and_issuance(finalized_state, &new_chain, &prepared)?; + // Reads from disk check::anchors::block_sapling_orchard_anchors_refer_to_final_treestates( finalized_state, @@ -343,6 +346,8 @@ impl NonFinalizedState { let contextual = ContextuallyVerifiedBlock::with_block_and_spent_utxos( prepared.clone(), spent_utxos.clone(), + // TODO: Refactor this into repeated `With::with()` calls, see http_request_compatibility module. + issued_assets, ) .map_err(|value_balance_error| { ValidateContextError::CalculateBlockChainValueChange { diff --git a/zebra-state/src/service/non_finalized_state/chain.rs b/zebra-state/src/service/non_finalized_state/chain.rs index d0ce3eee904..30f838afbab 100644 --- a/zebra-state/src/service/non_finalized_state/chain.rs +++ b/zebra-state/src/service/non_finalized_state/chain.rs @@ -16,13 +16,16 @@ use zebra_chain::{ block::{self, Height}, history_tree::HistoryTree, orchard, + orchard_zsa::{AssetBase, AssetState, IssuedAssets, IssuedAssetsChange}, parallel::tree::NoteCommitmentTrees, parameters::Network, primitives::Groth16Proof, sapling, sprout, subtree::{NoteCommitmentSubtree, NoteCommitmentSubtreeData, NoteCommitmentSubtreeIndex}, - transaction::Transaction::*, - transaction::{self, Transaction}, + transaction::{ + self, + Transaction::{self, *}, + }, transparent, value_balance::ValueBalance, work::difficulty::PartialCumulativeWork, @@ -174,6 +177,11 @@ pub struct ChainInner { pub(crate) orchard_subtrees: BTreeMap>, + /// A partial map of `issued_assets` with entries for asset states that were updated in + /// this chain. + // TODO: Add reference to ZIP + pub(crate) issued_assets: HashMap, + // Nullifiers // /// The Sprout nullifiers revealed by `blocks`. @@ -237,6 +245,7 @@ impl Chain { orchard_anchors_by_height: Default::default(), orchard_trees_by_height: Default::default(), orchard_subtrees: Default::default(), + issued_assets: Default::default(), sprout_nullifiers: Default::default(), sapling_nullifiers: Default::default(), orchard_nullifiers: Default::default(), @@ -937,6 +946,47 @@ impl Chain { } } + /// Returns the Orchard issued asset state if one is present in + /// the chain for the provided asset base. + pub fn issued_asset(&self, asset_base: &AssetBase) -> Option { + self.issued_assets.get(asset_base).cloned() + } + + /// Remove the History tree index at `height`. + fn revert_issued_assets( + &mut self, + position: RevertPosition, + issued_assets: &IssuedAssets, + transactions: &[Arc], + ) { + if position == RevertPosition::Root { + trace!(?position, "removing unmodified issued assets"); + for (asset_base, &asset_state) in issued_assets.iter() { + if self + .issued_asset(asset_base) + .expect("issued assets for chain should include those in all blocks") + == asset_state + { + self.issued_assets.remove(asset_base); + } + } + } else { + trace!(?position, "reverting changes to issued assets"); + for issued_assets_change in IssuedAssetsChange::from_transactions(transactions) + .expect("blocks in chain state must be valid") + .iter() + .rev() + { + for (asset_base, change) in issued_assets_change.iter() { + self.issued_assets + .entry(asset_base) + .or_default() + .revert_change(change); + } + } + } + } + /// Adds the Orchard `tree` to the tree and anchor indexes at `height`. /// /// `height` can be either: @@ -1439,6 +1489,9 @@ impl Chain { self.add_history_tree(height, history_tree); + self.issued_assets + .extend(contextually_valid.issued_assets.clone()); + Ok(()) } @@ -1667,6 +1720,7 @@ impl UpdateWith for Chain { spent_outputs, transaction_hashes, chain_value_pool_change, + issued_assets, ) = ( contextually_valid.block.as_ref(), contextually_valid.hash, @@ -1675,6 +1729,7 @@ impl UpdateWith for Chain { &contextually_valid.spent_outputs, &contextually_valid.transaction_hashes, &contextually_valid.chain_value_pool_change, + &contextually_valid.issued_assets, ); // remove the blocks hash from `height_by_hash` @@ -1773,6 +1828,9 @@ impl UpdateWith for Chain { // TODO: move this to the history tree UpdateWith.revert...()? self.remove_history_tree(position, height); + // revert the issued assets map, if needed + self.revert_issued_assets(position, issued_assets, &block.transactions); + // revert the chain value pool balances, if needed self.revert_chain_with(chain_value_pool_change, position); } diff --git a/zebra-state/src/service/non_finalized_state/tests/prop.rs b/zebra-state/src/service/non_finalized_state/tests/prop.rs index 2a1adf65c20..16f3ee84f70 100644 --- a/zebra-state/src/service/non_finalized_state/tests/prop.rs +++ b/zebra-state/src/service/non_finalized_state/tests/prop.rs @@ -52,6 +52,7 @@ fn push_genesis_chain() -> Result<()> { ContextuallyVerifiedBlock::with_block_and_spent_utxos( block, only_chain.unspent_utxos(), + Default::default(), ) .map_err(|e| (e, chain_values.clone())) .expect("invalid block value pool change"); @@ -148,6 +149,7 @@ fn forked_equals_pushed_genesis() -> Result<()> { let block = ContextuallyVerifiedBlock::with_block_and_spent_utxos( block, partial_chain.unspent_utxos(), + Default::default() )?; partial_chain = partial_chain .push(block) @@ -167,7 +169,7 @@ fn forked_equals_pushed_genesis() -> Result<()> { for block in chain.iter().cloned() { let block = - ContextuallyVerifiedBlock::with_block_and_spent_utxos(block, full_chain.unspent_utxos())?; + ContextuallyVerifiedBlock::with_block_and_spent_utxos(block, full_chain.unspent_utxos(), Default::default())?; // Check some properties of the genesis block and don't push it to the chain. if block.height == block::Height(0) { @@ -210,7 +212,7 @@ fn forked_equals_pushed_genesis() -> Result<()> { // same original full chain. for block in chain.iter().skip(fork_at_count).cloned() { let block = - ContextuallyVerifiedBlock::with_block_and_spent_utxos(block, forked.unspent_utxos())?; + ContextuallyVerifiedBlock::with_block_and_spent_utxos(block, forked.unspent_utxos(), Default::default())?; forked = forked.push(block).expect("forked chain push is valid"); } 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)) +} diff --git a/zebra-test/src/vectors.rs b/zebra-test/src/vectors.rs index 7937b19ba7f..90694653899 100644 --- a/zebra-test/src/vectors.rs +++ b/zebra-test/src/vectors.rs @@ -6,12 +6,12 @@ use lazy_static::lazy_static; mod block; mod orchard_note_encryption; mod orchard_shielded_data; -mod zsa; +mod orchard_zsa; pub use block::*; pub use orchard_note_encryption::*; pub use orchard_shielded_data::*; -pub use zsa::*; +pub use orchard_zsa::*; /// A testnet transaction test vector /// diff --git a/zebra-test/src/vectors/zsa.rs b/zebra-test/src/vectors/orchard_zsa.rs similarity index 99% rename from zebra-test/src/vectors/zsa.rs rename to zebra-test/src/vectors/orchard_zsa.rs index f4caefb4afd..7a954c0529d 100644 --- a/zebra-test/src/vectors/zsa.rs +++ b/zebra-test/src/vectors/orchard_zsa.rs @@ -1,4 +1,4 @@ -//! ZSA test vectors +//! Orchard ZSA test vectors #![allow(missing_docs)] @@ -6,7 +6,7 @@ use hex::FromHex; use lazy_static::lazy_static; lazy_static! { -pub static ref ZSA_WORKFLOW_BLOCKS: [Vec; 3] = +pub static ref ORCHARD_ZSA_WORKFLOW_BLOCKS: [Vec; 3] = [ "0400000027e30134d620e9fe61f719938320bab63e7e72c91b5e23025676f90ed8119f02c71c7ffa660028b5f3bc0b0bedf9b76a829ce8f2ef82c2c69ab6948bc9fd00a80000000000000000000000000000000000000000000000000000000000000000f2fa494d3fa60c200202020202020202020202020202020202020202020202020202020202020202fd4005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020400008085202f89010000000000000000000000000000000000000000000000000000000000000000ffffffff025100ffffffff0140be4025000000001976a91475dd6d7f4bef95aa1ff1a711e5bfd853b4c6aaf888ac0000000001000000000000000000000000000006000080f8694a1277777777000000001c1d1c000000000002000adfbfe7961473dc7f8ffd411b3e2eeb005a37342e6081d5121f18f5648c8480adb28949796e09a38118152905839afc125618be1fdaf921d188488b607f2544e12a249ab310f17a9349bfe463c7de09d2b822ab0efa88b6d32f77d7c38793192b944aeec0ca94918390dbe44c50e706407692e348ed9b7cedd231941a673722ef1e7e74888672b2b2d08c97a9ac114b7039feffbeb8bbe197db4a0bca8d395cd40551c1d5d788acc2ad09eddda73a5948de2d9e2d82aa638dad6f5dc61042d6850b926d944f29f17e96eca84684252c97ce4382f2642e54208929a4b37954e8e386c677f2aee3e8f4f4aee9f76a87d868fa2210445c09b927b842485918c869a23be8213ae21937a8ca83406fab193cecfd3fcf3b1c698e8057a6c87c059dc6f4ccb30af8e608a7c04088cf3ca32ab20cd780da9443606b092c8b5d85c9a76433c0993e3eee385884ce1f3890abf95462c49bed01a3a5c09df98cb7082e9770bdee196f8b968003f5cc76d82bf575f01da3ed40e44b3b15721f4a9dd5ecd14fb71a42b24ccb7d7e6a3bc10b53ebcb7e0ec6ace91dbc19801eff0c76ec0c10602bca2cfce9f3e79536a25143d351ed2894b4eb4e549960f212f0787057ab1ac8b249c3d3ff8652cb3fb17d7656d50c5e6833b056feb26855332f60e7b8d1ebba32df63d8561fd7d209a1e5adb9853bb5b5d6a41bf1ec52d348023e945bc02e8d6ae8d5b6c7a9225991cca4aa0b41861f237b3bf545220799f152767c7fdcc693a989057e119c18a96007c69c8fe5751a4258ec3f0f99c1aea8dbbaf4df9953ccf6b42cf2f4265011ca89ec7b9ef2c6e9410886291054f50db6310b225ddf32f9db26416da6ca9ef6e3198db36ebab9802517aeaf628d41358fd141dda8fab32fcded20707abc3d00191e0b2690a1e2fa044814191323155fb21e3da8798e0bafef8c2da4c73f967504f51e716cf87117f90fd028df17f3ea26aebeda7f5b3192cd5f4855044e9a41bbfb817074ca1680a458338b191a9619dd337bd0335cb1896d79c79cce10e454b58fecb1cf10da9f53129bbf3cae2bde82007ed98505f16922b6ae53a3a709c2e01ff7e529925d6069807c06bb0c73abf8d463b2a944a97d150935cc76e1ae1f8f95159a928a5afbf76d54544a771fd4bc482ca522274b94c87b4f1c7cc3399709b5572c5133bc945cca63bad59b454ee301e3582f09c5c31f326a59705a2b534d8e9a79835bf767ea563b0aa74d3301c40a303f6fc04ba0f3807c5decb6743aabfed1a092f88975820c324e2229829462e4985e299c2415eccbdbb4ff26789b74e91db286e6a4af023e8a18e826e930d9d4ac8d92cd8a1098d0705852cda367ba067e723ace9ea8b9502e20e6519dc72b1cb477c4f3091ae4d20eb7401acac77d923eaf5de00ecbb61baa3aca9044f3e66262245aa9f3dce1d02a88e8c26b34e3c27b4e4e5f91cb633c9b6e098063d052dd6883d4c2b153c739ef78c5f375c640ff747adc1110de2f9d011118f3208bee2f3af9990d56ecddab1cfde0c053020b1116afdec7a3303fffe6f6880072482f95aa3115724814aa5fad017e3b7637f3dba509f1e371c9b87a275cfceb68aa5317dbac0e1959367d124935c76631b8aeb532d99c393374f214af2d6a3a5bf4071d97b6ad39b5b2ec03f1feb520ce467808eb2cedb3ec933c20322bcd4511b838de111f9faafb5d45ffd8edbb1fe8f0928d535ab9809b4cbf588af635419b10f7ad9f4418c766d88526215b74518cb6554e833ada2dea5e57776a09541d76ba545f8a727bbe7722912cf00da4a48a462a5b7b13c88941762462142f97e8da2b358435c9cb53d24b6443ea2e1bdaaf6ce58dbd0bcc598cf170a193e14e76ca8bde66ccc786bd330c6ce61db5f202b01c7faf185877e3614c1a1b4484cae6dbef080142f8c45e3e48485746fd3505bba099ae7b37b96e22b2cfe6a0dea5b017974126259d5055a28ad510b3b7116c27287fb7e635f1918d5a9ca2529b1741c9e86c59ddf11c3f70a56fac7c9607eb9bb36612494ed1ae819c092cfff73b7c9c5d3e8680dbe73f92b749c84363c374d80632fc488d0b7d35f25ecac1c151ad8427d7a4eacf24fa6937fd5c416776654bcfae92d999b51c49d76bd53a9d5600b40915acab5d31f0ea3f7a68adccbb72cb454164beb35819af0e9e06ecb40e96c9c2aa8018883301f65cfbaa7ea894737d49b44aa5d76c4b26bbb6de7126bf785fc2a8760ce1664150be0b6828659513561b52906e6a4782732749897a41ffce670736ce0baf5730fce9bcb50a44e1e9bba166f4812ecfdbc2ddd8483405cd2bc68ac179177e1713220348da35c7b2a30c9ad9670d99a53a3c4c4a611fcefe9e39024732d6996568f2fd8eca433a41664b070000000000000000ae2935f1dfd8a24aed7c70df7de3a668eb7a49b1319880dde2bbd9031ae5d82ffde01c599a33ae69b9dcc093a546efd4cdc2c8daa0479ccdc63123cbd0622fa54f8c15ce9a049727b659c998b2fc935ddfae5788c51772e00dd8fafb91e7b9e7b9d4a34efad77ea3f54eaf8bb5bfd4c5d5ec6761689042ec6b1639e79d2628e712669e32f33d058707141549b0a0fc31d9fe4a633871d1ca48096cd8272f735a0838bc1a440947547ce52183863bf080eba73bb36f5130d7dc8676be2e28d00714dc36ccc580d88f6d878357e7121a811a03eb12faddcb75c9c3703ccc4afdaa85101244e619f565a5635e6b8c856fda2edfc27b5c06a711730beb1c361a6a916fd713ba64385734b8563775d66adace055205c6cf9a6c90faca0629e7b93511d0e51e3405210bc3d3c590ead6671e57af44a9418a5d3c6369d5b6d294032f1592c601c2782f5e5fb7ef820f548a7e21661944982f5b04f8722ebf42456df6748a2f9ba2b816bfdfe1432f6c4911daec2b75802d43000403272e1de73bfd625b9742b8970133c0599a17cb7fd7984d6a3da82e845e179ea888019c6d86016cbef610a7a0e3409f0a2bac1181ce62a22fe3fdad2708225ec503077caf354dc5c12f6fad975509172383e2f87405fc7c387b1de333f435426fa3b8a524cea377f3c24690918a4ea2dbf4940ded498169b9b85adcd9d37175ac43897abea5d629775f4f9792d2ece6ff69dec38e38d0c1c9e40dd2967aa103a20a148290a9b89ea82e1bb5235bfd29d260862365933e19f81eb19be2c775707433d66c15f68e5bb8a578a925f20e9d1bc34132c5a214ade50ff48489b89cb674fd3a9c787d0ab539849aa19486e3d4081d4f361517f45fa35168e0432fbb69251a6a7e8f5d33b30564338f693e636d04203502588b4e9128744f49005a77e5de0f79e06053c01e82f4bc29f0bdaf3292c300030eb758fe2a7e98f41f0db618ecb99924e25084b0e69da78bb4918b365b8c613ff5e033d4994e176b5abe710fa552b3e5e21f59a33e4e0aad74c0504c2eeffcf213301b35d9b0bd3c7d140c849012b1fa7ee177e994366b9b278afd94f6bf9a65bbd1cfcf5f3525512e5b257f6a5cd61c43ff2c695cd9571d8d0e24bff92a5ace203d9a643b7c52d794b3a6a2f0cdc6c8c1527e51b32847935dd0c12b1f1aef49cc40d4318b5b067ab9d238e7dc4a8903d8ed224c15ed66b11043fb6ca6109587b6210027df615ef57125d696d0de758be6e4b1693e260589e441ebb020177a4bc7c577a7f2c7d415e00fe93cf1436bf13f738f0cb7d0448074f1436457dfdc03217b585d133dd44928779072129072c0cf0ad9ff3fdbc686f12d219313ceb77e01846f030d631c8014987081a1e3659239e009105143ff3fd3d999fb10e1a1b8f0adce31db881d5da746138462e5a1b45d47862fff760d3b1ae1de946257f2f0edd1bb911495fe1a24ec3a7cd5285e5bb25ac1d206d2f926f9dfd574758a6eb2ee5faed26e5a8e07dbeb11ba4dcfad69d93cc718fca7658b97384a243589699547d476887de967e325ba5e3b982b079eddf83998849579a849dbd3f2f487eaf9242512899756352f3680f334e0585bd43a3439bd62404297912e545e7c18e0c2e19714752b7525bebea1d83222648bc9457ff3fed4c91b1fabc6c88b5c3805dde0267f72a0abba1715fcbacce4253881625026ec2e240e8bb98c5316d28d361a91a1a572caa057492fec8d5e8d8b51f5890515186f7c97ba4a3810f40e9916567ea7a1980fd806d295c73a8b1243b538c373f531994507ae50889cddbe473a8dac128c98eeaa965cb0cdec2dce36fae175334b1dccd48839b7d28292a7c753cf23990d111e518a4260631d5a99a28f3bfd01db75d8271d08556abef553106fdc472ae97b5f4d2d3741a5104d06560d85f48d3e3acb040c3264d37f370f2acbff61ed733f655d8815983c7e78942131b41645f1ddcb24711ef39dede1317c4d7dd9b01f77066b9f3714b5f09dbb35e8341188e4384bad7238d9ef7d73e2055bb9e6706a90348ae9e5057780666f3642d7a18085e115e5ea3447fdd013a7d976d00e39edee09b271286c1a325161d3d6eac566cb86c5af3d287b5a56dcc48faec1d8342bf3c5436ccf03c0b47cb880aa7001e2c5464a406e24dfd9d021e62558e3cc9ae3228f03ddab021d5519fb426551e0a39ad08f68229662f16b79b7653b8f827a8527f1dc556a02f9e3c7d0f3872467a60340dbb0641d91d6956a33f5c905069ac39e67b40fc8d5dc617e00e89dd926bac628eb187ca1d0c41972b73b628f18159633c6d4697893bab32cb760a193b57034804a66381e62bebd6b729294bb14a113c5750a2bbdb57d40ec9e37ce7f3a486b920bf2972779b88d4ffd3136e2c10286682a4c413ed2991ce333060be5e348dde9eaded10e4cfc84ab1b157713936149239477a0b5a437574a45f24843ec4a525a71813c05c2524b92c893cb6aa0dda8df21d550371f5ac622038baef7071007a31cc486158ca1a8ec2a0c274ea26100fcfa5a3991b6f79384ae487975207d2b0b068df60c7bd63c014d13d2b5215ba7be1802b78742cee248c07cb00f3d5472dde9f85a1a9a323125a3cca08fe5c8d89f73c3fb600a4a3c7f28f12ccdac1e911c8d62242deb1ceb63eb40ffad0ae8845cc9efe69e9f5a7d2cb9910306e16b529d8e16e235e8e54eb84859d4346bfbfbefd453f9a4c8f4cc5c6c80a616433dbbcbb512651578d1d2736f513afa0fc68b401b53086d4a32d2a73100b59f8c07c06f43d17d2021fdc15410e22b5911b3572d5f7996703e97d24699da3fe7714ce74a1daa802609cc631db787abb71504d8c016cb7f5973c0d5f91899bbb100b97d4063ca590d15f176612d2e8779f89132428c6a17ce0dcab8ca081b9d891d3d0cd0bc755a193c5d5180d28d917ea7c5121c702e7c66a58b5499ba4fae3336a2040c986afd2f44d92047b338db4b6b3b6c176d88c641a6d9b4d4749654d55785002b3201ba3eb86562adf07f94b3e39bb3304a2d022a872ae74cbf27f0194c5c73037bca2d3daf1150aee2f81991aecca23660de5072568652037ce13944ec9d75f7cf424607e36233008df0a9707913985b837c631288ac62c253c9cc1586706b9e8238bb0d3d14fcfad900dff65772b36ca252d9f81450ec29d6be025262bc1104a1643c099b3bae8914c8c78cf39d09907d725a52f3ef3a9981c2dec6cccb88017805cf2163e8909eb0822c34d2b42ed08af78dbae9484e7e4faaf2a40b0762f23b491a2ccbdb5cdb4df184b2b70cd39b0fd39b8a50e4cc527f4c6169e79e9c1cca54900a1624e198a0214d8013c017a2dad0aea1521269505213c1c873cb5531b6dcaf1c5430d741514e49e7f3c0f7bea8c9e3ecddacc99e2a8e729f8a0e9c87687f11158aaa9a7159ca567598add54fdb1a58eb08da87154c59bb9214e9d63fc280ce2fe1300b12d4b805d2d992a5e5f74b04e6b41ef9e4f364aaf3f90aad6435d7662d5639882f9edf5dc7ae1e5623fb1cfd9578fbe00cf82353ecf865d9ea24b5d5050e6f7609205b2ef209c57df854ab27f2dbf047e69666ecb731f0b11e540edc105301dd9b915fb4fb1d96f4f8b99b9c42f55f99cedb22638167927766642f0c1f6c4038d4ebc8dfacf6a3ea59532d6275fa5947cc80f44650719be2802f83f62b86776c7a8ac0b92305c69583eb7b1457e21760890e8b9f42f0043af46d07f82f8aab3168ea992bb165dc7396aef85646148b9e9fa88735bcc4f2f94d70fe02200480795aed487d24810b4875284d8e51e25493075e17b7f9f319da50e339a61412cee460382cef9feefa131bb3038360535c5593039fe5fa3795bdff94b1d41e0538536a9e6de8e4a9228d65bdc5cf6868680f452599112cbb3750f9f167ed33017d61dc6b6b374d87384d3a81e74289bd5253ebd20edd58d54bd3711fed8b2273d5c39ab91cfa21b2d3a901891eff40eefd70b8d0d55c1c33a9bbbf2e0dfa2430c736a18addf449dbaa6ed37f04b5a921f945bca6bda7cc75fe47f4c8395918236dbd810406e684aec3eca46c8079dc76defdd90c746859df26c661e746260ec99f15b3bcef2d4eba263d6563f305d522b58f2a39d9f420625b2da43f7dab24c63ac0cd79078a56156ddb4a295057c02dfd02bb52511d08547ec1c0be7a7a1ffdeb550551cf0170e89d9ccf024e862eb9df3458bede7e0bc7060860bcefe43e526edc7ed295f331d5167705f7f32da9721abf972e7eb1235344776ac19bc23e6b916d3a5e54f6863dfe0f46b17800ca77c07f80f0fbe0b2a39fdd2e0107e53148182c577a60a52ce377947c1c44f9264db8fe29b5d9943ec70997fbd1759539f1c5c279f645b68a856d58571bd99d0589f444f239f194c9e73e1606f8affd027a78fec78b8ce11a3871e416307c4357e761b6836be85570d3f155e9d19db103d148cf9dd8b51faabd6157e5e80c9b78e19501489fb6fabc2c1b7de2d9f480006f0b5858eae39893f9ec8a36ed92f2d6e64a31a7c1b13dfa8540d3176e2d451b09237feca9752c8e14b48eee5dec0cc314a00cf41303c8af57c727140be157376f5182e5bd20ef43bfb73077f388b2152c79b40c7bd7360aa0da790677535a1e1ea76528a51b5ea8ceecf9babab979606945dc154ab3269d729996e6f7ed843e8207cd7893e5f8be32fecbcae63474a8f3d3e66f5ad3be91ebd42319d4d4e81377d3531f4bbb7279ba63403c9d827875d7c244a9e7a7c83818af42fee45603039becb40982e1ec43e71c919a409cebd605b865e99936dac09953b4be63ee592eb0f1bc6c8a0fb156bde6c4e05df97253dfa07ad950253f18e0bde6eb9baaad215c785a73750c6f30b36acff3e760abd513258e60d80770b4116cc7f925f34b286649676697b49fecdc8e99c6fe3311d34fcc8c4cc1f066ce680bbf9c9fc32722c858204e9f8201dab9bd6639830830e9a24830a2dfc02f767eea40019df8d41f2e0f63562cdbe55f71b136d52a61b271a24e5992a123f08babf356fd83468d20ecb634bb0ad02be4af5fa5163445cf5ae233804ea209f5c279c726db78f1c81974fb8cabc783e54ee537c9bc3c83370bba589e1389bb1e63ecf59250dcc2752fe0e1081cffb2e7f4c62d44e54a46480a809d383e81106a1b06165f419a8f3502cc7fef7c9067599af2f049fac6ee80b15122555362f7419ab7f3379cf9f27503c503eacc8e94bde23efcd0257fca4da1ac39ad5f580174c42860c91be20a8b1b95c2ccd2a51466da013a02d728d54c168eb50c064b30da49f272f08ca19099058805f91afce70776194f24a151fe36c2619df9fab6760554cbb58781514f131f1ec127a06d98e5ce4ba82fcf1165937ed258ddc9ace565827b6b8cc009b87b083119fc093a106d5f5c679e7a145b619e34f69ac0531a9d7e17ece8e335b66f14fa874dafa045603e127954d89dfcc0994581a48f54fec32d4228831dabe01d0d9f887f4604e975326e8cda35e2151a452be21a4f7117740e70bfb98cbbdaba32795fae8150ab9be24746faf5c8a9ab253cf34f2807e30a238a3ce2aa5de2691371c49b1475e62444947f632da3da60786d0f1f52a8ddb0f69bb293540830f10cf70b3d84609d16fb1c6285a4ca9ab615ea8b0aa6274317dc9c06fba50e001d00fb9db760fe6e4e751a720bb33cfa914fd5ccd5a5e5ee325805cabfbdbfdbe82a45aa53570a50fc22573e6bbf7fb641f16d01f44b9176f965b1ae610b0bf2a73fa1125b14bded8008d3d1617951e19f225d0698241746b651e003fafa16764506610fd92caf131e8c278fece483410fc3e2c6f2cc76d4a9b66028ff3aa83d4a074cce66ef035e8f2186d2ff9ed2615b0c451c8564b812f225feecf9cbbb2de6238dc4c8770a0e17feb7cb02217da98318414257c4dbf3022d8f1e5ad79fea78168c8f1771affdf4597994697e6cece2e6bfc7219d3018e3ac49549e37b8a57e0e69ef51c8944a3ed215f36c15a2883aceab247dac03ef5a82f235fea559e6b42cccd5eafc30066a3a3173bcf2f7ad34004071bd080e69c7ad3f514c62c928e63457afd2973142069ea68111c6820af10202db0396474cb2a78e1a7121ec04900d9f4ebbadf3d306273afaeaaaaf0d882dbd511146f009b748c2e093f02baac204a3b4ebd4bee5aedc3935775b9d01cab2723ce0c06ccfd5e2a8a2c8fc467c9a06ff3964e96e104890097d00a99811114179536be5457dc37864f4b5f848d27d28a6143b90bc2ae09b218e867fbd6791404ccb662fb779119b8cd2472d1f9e360ccc37f39f2019c79f365c813fd80faf189985f1704016f096acfc6bc0b674fb117ba7eab0f4138791416638ba365c546180b8d5662bfe157f3f63430198548216d7cec0ec8724ebde55883b2c384cbb67b2d7179362f9114dbbe561c8acb3d40ccde56ea66cd7c832b299a96f3a0e0aebb57e9246068d5fbdb126e6a149f7ef2214c35f30409f1b44de792cd741df0cf48f273f6dcafd69547fde219908a75b3b594f45a382ffda619f7e1378df37a8b2a25aad273329002ef931a95a0a7b670dba6dcfc08119783f60b84aba6ba878de6158e689ab051e5ed1743f6fe28a3c061198e7a49d08a68271205e4151c49264929d9ba38dcb2559f45658ce96b2c232cdf40e63bd8828794ef664543bc2f700a65d7c86f218a9b76ad0391906f4480f5563b434403e35eca1079d8c1f906a271ffa21d669a27883108ee78a4fceaa056c0bd5aa4496ec5f37b2dab8b19abc88c61ec5891759c6fb2263f534df3d7116d6874f42d8bc3a1ff9383a68ef3955295b5478c28308c79ab25ae9ca31544427a2cde901ea588ba872f37cedac395f6661ec659f1bcde925f6a82502b32fbb07d4356efda64e82c35356f9abaf5d3f0abbcbf0b0fcc2191501aeb7b59b21e00858b19492aaab25c62afc3b0cecd3d7746eb6cb1edb0cdc569602791a17911802c9f5ccca92717ac661cfd4d4dd8bcdec75492a64bdd2150c2235e7d87759e137b213cb3ab4e275a99e4ac77fda073e2870b6486ba384c44b4f59b382847a5d0a4f87198a996e639f51246014a0d9751db9f85bcbea056a7609332bb1e7ffee3baf262a346e45697d9c97c5ef099e109251368b5a807e6b69c1247e8430d2ac2261aab0ef2a0f695c22086b86fee0adb6bdc8a14af3d02ea0effac0f6f55e8203503b48deb8c8673b92c499284b935abd06352b391c253e35870f024bcbec4332f578a74d4ab0be09e73e3cbf5e1ed7e53eceeeff4ec26941dec578ce3a33f701bb540da65f810e7f4df368804cfcb6078c99d45e4f15ee1d1ad6831c3e6e01102e6ebeed1f86940de0759b256594c9f91041716bd57ea464e77cca292090f612bd7daf20e9b534bacf13ca7d940c90fa9c18b188fbcc17e282edb1156cf5c1351a9f118dcbc5cc720c5c6dad1ccfd04f1beab7817561e86442665b841c97150d10395fd842b54d025a221d81f05c820474e492341a0c6dff31f4a38ee089082f7bfb17b9d8c8355dc76bcefcf0c7692ece39649e85ddf7e395f1baf893eb960d8e1374e84a1d32fc1924ec5808c1255b34946db13ada6163b368754820d519197aceb746d33f556f9932aa775b5547d4b42ab6433e4adfea54bbd7d173e622229660e74ec486937fba081cbb26de3ce7f6f76e070cf54315f18b03675cee1c06fc765f145b7fe4fd12f897c83e21c299fb9614533e163564b9d3090cb00f253029a3a4042e2047cead0dcd42969685de183ded532773056cebbe0242e9bdbf079eb9c8b0a64df4833ad35fc40a317e99070683255e7087a0896b20e0b483410f9e4913bf36cd028555302162a6c6152803b31b8dede9717b80947efaa233f6324941e0714473c92da512fbec873e4b745505a5e691e2f1b6dc2e98d1cacf4c1a42dff6e360909bb82027b6ad070f34ea2d1bea39653da363b2dd14633f5d0f11cc1617ab8239e9832b162b2bc18d8703a39a21ac2ce9b1f23395225f6b34671d5e7679459e7f86391c80e2e3c350220f3f3cb40e575fd8afae3bbe1104246b092e405bb740e213734a5a171aeb6d82b185973a797cf3f17d77cee462e4f3032d053044aa6d8060928f6227bee4a2ad6f7cc6bf49df364cc75fdb9c9aefda07130967032ecb5a29b38bafd4e6755e2427585746460d696c9481db581ccd583311b68da1e80fc45b330e7fc6744105cf7f329effa8d05e5f04f891e6a45c0f620a1c516f22c796523a325d03aa141674b2d257074a20a7b14308310c73ccefa815f73715282cb467a763532504523a1b1fdcf2ed3af8381fa967e02294195a9d0eb43a1f5413be08d6e9ac6e95ebddb34f6962bb64dbde6e94bf734cca4cd1d70feb5b3525d1a4f8551facc79b5f00732cb252e9df686627a56b80b13fd033cf279cfc12ae321a0fa58da9df8da8e6f9f64214e40c22334f13bf1f6da122b4673deedbff3f98958b53af0f4b40158d79e63778123cc6dfd55f43f4bce42f318b0ac418dac56dd9436e78bb527c37dfc28180fed5c439f952931b29e271d83b633effe9809a6399282048357028ab1b540cc0510ee6b19a63643714857fe51c2b1b2963b7964cb602861e81eb52348453e9bc497c447e8a8eb73c79f3997ba17f35f32121ac7b0172845bd8caed56a79285e97d17aa467312c4d10b8bce1d18e416c383b128ded04fd1724a29cd8fe9377ead625ae91efff1a562e03d382e4b4621b28f717ac6fa928dac4a086aea4e122d59f28c961ace3dea0bfc79eb62a5702870bb86a8d82e6284b39f61d2c39b7d99eb65f319cce48af91a9028a48cae8c3c08134f7285c9e7161a570947fab3497f00476f9ede57415cf5889ec18501783af4c371a24560a3046a2683741e851ec1c34fe45c777ed5cb03dfb8ae6648a1224bfbc723f1a69a9edc5ef37147baa1a84b199be1dd645dcc0fba7ca9e8365309f3669b6d1d2e8a47e21d34d1405e6530e0d200dd9997ad72de1e70e660dcf53a6bc4bccd999214ef9206af79b44915e9956f8a019919290066728eb9ae5ccf073eefa4b9f771f584c03648cccfbc1823d118326d7488e2fdb2319df94a593ab0bb34c9970d038dddf2c174631f7b73eba3e6fdae9edee2ead25e57f4c498c32a567c546f089930cabc63db6421a25915714aeef8d9ccd320237cb0e4d302fe1c964c4aaa604714105a1228fe5ad6ca7f42fb2e07c7d6b0bae5f3b320f59e9821d0f66b702e0bef73c4f3d891454e90599f033a96da7df2faf22455f49e28b10ca126096573ceb1d4154791bd607ab67ddc372cdc3da2957e67ce2c599d50b90710895a934fe744c3cb75b1836eed5ac9a549c28930a6388a7c993c7d5a5aa302ee7bf08d177548ecd98c65152d6197286f52b57a3f918218fda1241e28c86201d6e3b6ca12d8e6756223bf9b19387c321db1a0ea2fdcb7a7705f7e8c81a998368a1cdb7788be5629a43704d8e91662b3e1a5ab205f85a27a139a5dd5e40cab92e6dcadb5be50ca3343905fd10ba97df8aa658634c914db6389809d9b18f59fbe371733e5ae1fb35f0f6230a2394119aca72cb11db8a0d0c82a0313562b97528fb50b99f21e3c4097366b763b0325a2f8875b32cd4beadb07925be74aa54aa89f9b52eb1394e1863899f04d7fb451fdb81fc4360a3320dc2a24b3b2c0fd463d9906b0797c3215595d59e5350da3a8cd519d51e76904a80d73a163b384fa68002516c7d7efb1f14aee9258b3aab9c5033b8d929430ef742cc88665799fb1207f2c8d333db1ac85d4c15235103d28b3769df98a763426546b21a8eb0f67872edc8c9d448b8c70d6f7af172d13c3aac5d4ae5bfc8ca9c891e501f2c473eac63cdc16a96b0f74cffb89211a411b0e6b4a0d794b5be83a7cdde651573a142789aaa6aaf76c7f6ba4851d1eedce7feb5f7a2c1179e351a6d97620395b96850238967e8264f581ba4ad4dc85933c874e30fa3adf74901f6ece0504879356a835eb019e12e5761f5555f63c91142c59cb32515de844c0284a31d5e148b694c53e3c69378a1c2880e893fce50f5ebb5b46b7ddc8753e7104f5effea9b0c36e3720469c3f20b8d97cd39c06cecf7881d20032be0f23ed939613cb0dc5ac81ece654aaf5ec36ba427cda4a0031328afc840ffda24b1829153682cbee0da142cfac74394c073def27b4b38f5cdc1c7b699d281d1fd41ac559410cba3330d16c74c8d035ef0210c8dd151a3850db594502d1d50c2959301c384da313611e361e71e937a5d1799d1a45398ce25b1111c86177152676d64393e6ed1f11821c1fb5dca4cddce3a3b1e28975d80dca762c79210222f6771d20ac64da695035d00dae321be393b17008e5f0037f4c1733e4a9f17ce275a85fb44ba59edf9e20403843b11863e4db333233314661bfaf853d6269b187bbb6c0eb0f510d4912645056813ad34cf3bfe5277c589a0314bae0aa802cff46b510c6c76938cb84e921f7b4cf4200da1a82942a807a2075c0f7dfebf768b54b2e308dc49488c4080d6c71c0bf8d773d5de3cd112c588a8ffe11d7a17534a6c7fef432c380ccf252a10d8cd1fa13a5cea6e546349923de83cceee44f2981984fef7144be4e72ea0c149458a7aa6648c9f658622c00be9d0074d3b0d498e475c8e4bfdc9ed4ce81d3aba532aabf7e18d5097ce37505c2dfe9df59a6bb30fe45d62c8a1f2b065ca8bf74f81bc3da5ea3bc5fe855ed0f0104574554480151238458b0d0dd6d493ec964a7462117237ea214ab8bb54b5a7d9e005f5606865f5c7c04adb7149725e02ae803000000000000c9cd432edea87319b8bdf5b400d17cb0d4743f2174c15037c7fd9e5cdce945862d09879b6ff8bdded4f70af68cd3e81dc71a4c671032da6cd9224a5c6c1a660aa1393872b9170453d05c1f40ee3bcb8f727b3e196cbb9c72e7f12ea97080f67e003c99764d0dda139b3165da5dc4bf9700c6a563fcd0543f549e7b19d4cc4caf777c3aac4386f3bb692fd45d7197df5894f1c9545709c9c2255a3b6ed950385ba5a7c9c5fe91bfc671695898f78518380e34231b3e36a49b641cb3e940beec0062", "040000007d24b5bfe1999a3b21189a3c4d4867784bc2105a0196aba2ba6fd1c9a63e22e1be3fd8ef559f3e7d94c5da9f3ffdb276804f413014d8bf07fe14907d6a37659320e538c79c0033eb2537c88a69d77f048cf4cc4fadd09c9bbb91b4d965ac8f2e0a104a4d0f0f0f200202020202020202020202020202020202020202020202020202020202020202fd4005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020400008085202f89010000000000000000000000000000000000000000000000000000000000000000ffffffff025200ffffffff0140be4025000000001976a91475dd6d7f4bef95aa1ff1a711e5bfd853b4c6aaf888ac0000000002000000000000000000000000000006000080f8694a1277777777000000001c1d1c000000000002ef753da29c8538cbe9669c722c10bec5663e07d101f0a6c3f1f86440a7b00dbe374e5118632c4075f9e84b6c62791de12f1ec0e70e7d415d61c6639d786b1a0c0289051e9e5ef26a5dcbceee48051ae1ee91d70e02022fcf954f3d1190186523ec4cb0ad65db85d28e247bc1daf3fa5b111983e5d328166df852374f3efa9430f7e7d5ef94c51c82437ac68d11f78c190ff7314fdfe4fe007c0be3aeae7bc1094c0be2db5c7d2b24faddb22ac70bfa8499783f312f0bec068f8c09483f7b7edb6e63753d60feb460e2ea1f683740eded3d994f602670d174d38dd95b2a151d0b1d5f2592bd522084eba11fe9f8fb1eac057b84bde9119816ff74790db723529e8713c8daf25996fad08f2a78ceff248ccc91a83402b94311946343866a6dace2226d246cb8226f2bf7555640d4891457a7f6bb6c85962a5e482ec760b6d6483a15f6b44108c2096492765fea12c37da638a7add8d0b74b1bbeb5784c3712349881b78d229a682f024cb21c0c3961704a71ebd54be06a17f44b1fb1926844f14c3a9eceb626fcacc77ddb138846bb40f28daaf7e431d5d09d6f2be928bd09b03f6ee2302cf572c781cda2167d7d8e9b1f4667c8b3f7621c0cf85aeb45462eebe743a33bebb34a9d118cb2d4a69d2038d591e3266e77e122f9fb889ab83325e5d2ab3bea0e85e10cfdd1508d3233ede0b9de84634972e6d3cbcf9325407c43fba5c9dd30a70aece3ac6ac3d5598fd2dd29907584b85398cf21879b4e9ca3c2066e65fad046e788e56fd4a9098b5a4b0fbbe12c0f7c0b5caffbbfa69e4289c9cca89ffb3158dabeb2952a6af2bac251010a3644c01918e0198b835da28e26f694ca21d897785240d0477cadd8bd03bad34639189525c02fea6172168722cf2ae9a6b51412b4f9b24495b9b2852cf045c1acc6d97dd0d6746dd116cf8bbce3258c862e1fb18a4e91d9118c5741a38d6a7aed613910b11cb881cb6d1437669ad853512778ced215ff5b460a47cbfd30e86f9eec227fc123262d73f45d71e66f17492af0457191e797ac9fe6149f4b3cd631dce8f9844bf16588f55003371165f0e48562a11799c33c5b4e4dd390b3943fddf0162c033f0751530acaf5ac2530f320c157c498452ac5012adbde2cc19339fe82e1ee6245dcea9f587a40f3b78600de1209ef9eb6a903d267a95742c856aab1829fc8974731e49b6e8f674eefa81b23026d0bcb1d770c31a60232798a8828fecba930b51b80ae6b98645be3c5b80b195828dfab3bf8763ae660dbe4f02ed5b52abb301b18f3ebf8f1f81b8feeeed620809673472aedf9d70ac86268b7a162d0f46c0ff6bf52bf5dd289a9f34c19632198cf15730427971369cebadb6e943a6d8dfa84f83f2f6451e9d155449f6ff1b41f538eb760edccc3697ef679a586c8295afff2cd8de2ddcaabecdd1c0b41e8db2790ca35e263372e2aadc2b579fcd47d74bcfa188dabae48a78eb8e32e403a3f4bbf86b8535c568a332e0b64de3b3ba0e75a2ce01deefb1b1faa6fc59cf602d1359180616258847d458d03990f158398018e63abf87086caacdeeabf6daf6965d184bf8dac33b1c5af2223168e023a2f5021874300012761f400e35e341e3b54683442a1bbf7907060b54181d27021ea69caefe8326414462f03c44eacac9f26c8a37a8eae78c76dbe19d33f6b198e8a2f4d77a2d50bdc9785518a1e210fea6451bc05e85bd106737ad37e9c96105db1b9bb09bd7cecc45960de0bd6d803913fa43935a8c17de7bf573089ec323b1aba8f6eaf0603b91e53c540305cefe8361dfe47b787257add20569bcb7aad355d93dfe9d28443da5662fd1030a8e251fef553877edd1e1559bba63ebcec258035548d037eb34276f4b256b22631489e8f7201c86537a53502b6f9b4ae7c2a7272459a4df0d203b7399ca1bae6b5260566332b955e342132535e527fa207f8ec9b0bcf9442b7794160497121720d2fd698e3eee28fa34de2321afe580958dd133b1dd2b36afa84dbd004c4a571afa48466d3b7915c84753186b5a3e7b724a8fb8e411f8732b963fe81bbaa48b247330eec8a0897ebd64a25032e8aa4c987ff8153bb447308bb3cf5ab699504746794711928457df6e10d689d81cd6a846123375f5c46ed603f14b0ac6c9729075873179c3bae9740c273d0ec9e1ce060285211c4e60fbd2801cae6c7337570601712fb81abfc25d9a43464541e13bc42b02f01f8ec78a7e5dd3e84fa9576891397106427a6ef262e11f24a55af39caf98130c69bb042475834753518d2f67f66c04d81c574eaf7d8b83bb029f037c4999159186e170752880638619096dd852f29994be72f3a6922a97610fa11085d4288214fe131d2243929d40dd5915a6789c77a499f43489ec2a0b7ad37e7b5070000000000000000dcdeed4e7121d043c65c8049da787baf0bb29c59b75e9608dc59b97fb001c92ffde01cdc9b9d97951e4247bb513ff71eff5413fda3522232fb7d7bc48429bf301c3bbf60c1e5346e6b2412ec70d7ec4091384f5a9cd1feddbf9b02fa15e591c1f86bb9ee9b65bccc11ed903646e771b8d252e63306498d3b325c7cf46dd9e4f8e1e385bd5631b11f427848e7fa3bb78f8f97d090ff15457ddb0030fa9a8332dd2ed68ec267909771b6ee3edd3370ba34516a4a7171680d2d912e2fa4c966942b231609e3a4f5860f35991d117445464ebf6049e31d0b0eacd23a11122c2cce82c6f72a160e131c588770bc6df3888503df8add4f1285f1417543f380479052a2bf8597f56b0d5587578c3b896ed7bf0e057b9a39ca8ee3b1abc2bb967bf5fd7064150871024113211114f203ce5f150e30ff55747a6ecd17a342b77fc7c41ba70e1a216112048b26bea002606abde584d49e7cd633b7a5391290e7808978100e2d5f955cda22772bf642aa0e3b4e8f30edc8c9e6dc1d4952160377205a83730dc58516ad5d5f1b121f397aa2d43f47ff7a09b602b9c61958a3317d2316b9c8db14e40a53e88f1744655a81b850ed7c9388c5388da56e36ed67ec15e15b52cac4bf47a088df916d2c70c9bb348640cb429cbed26810d8fe2218424a6c63f1dfb5bfd3277bc64dd12ae6c088d7f55c4804f6c8bcfd6332bf4d6edbce09574a9c64d3fdb1e41c4d17a403646efc749f7ca43eaf94b014dd269c833ac3e19bc7442ab86b815a9eaa9efa44d01e69c77f73f600c5a911f6e50b5713d079c855ddcc2ba6462077f5d170cda9bf0eb8e477095aad7cd5c081864b328a9cefe21b44b53ae46d38a66f6aeded011536310c3bc0dcf64d2dd0c9015c6c2f83d36d146f9bc9f5a627dca73c5e092953815715ce9ad107d5450b84d449e0e93bf16a4e03d8b15d8b938216cd6c0bc08ccbba0658391f8d8cccfcecf77a85778b9904105bce80cd8c15e38114a80fb6e6870fbc92af588a905da1de9cd58d7c288e7cf406ef916c27b1b4b61fac457fa6390d7a7b6c14256c7a87fd510806be27244e778173f993bf86ff5cbe48b85178c8ba1d529a974a00995ccaf42b64d4e714fb5df79980d79492002f7e220dceb7a14769cff9c1dba91247cb300eb8bfcfc3a85e360266fc4b9f328ba12a6098aeebc67e4fc9aae6762defa078251a0d9653cdbec24fbe31ffa3ef322e6ba7c7114d0f642b72c7ea2688505047248fb18b8321959dec8693a1ab349c16770aaf10889e46d8d8f508f3233a69c9a820b88fce5d5ed044b9cd7420f456459d3ae87a23cc72d3d9c770f94c82224d95426e10107ceb351676c1dea9f252470ec81a9825bf0c2b4a3342ffd702cdb306a351ef3bdb56539da5022b878e08549a6ec8d8773b44c19281da0e307614f72a30e46b73f8db627c4ae9f530c0ea6ea523a6b857f96acdf37b42808ffd31fccef667f92d7ae7ee853233308d0e6a61fb0f78f1d0ee35278788dc3f7585fff3688ce16d40da875b756b2cf4aa33875e01404fe7c74614f184a5eb458acd986abc580f0cf2517110b9f1239615194055de68c7925573faef91ec11706d27b7b672b42b323c32b25a796e795baa58a7dcf5e19a46f21b27bb14e2db080ea704f7a1d15c2ff114964a65bb7429a216ff96939999a743316b073d63cf87ec39d924f6e7658f325dd6a77c9921b2b21f49a22b1d96155d1dc9a206a9d521a5b3372c397556febe9495bbad48c1d9a50f0578bf5e0fdaf8d0276c1fbcd0eda0a5b72ced1fcddd7f6dc0df854aae139d42527db885aaed6998cfe1daef4865a39fccbf57673eae767a975e43f1b198185b1e37a7d1afe476cc35602f148cfee549147584f19255d6cb3e31def73cef31e3adff8184109cffcb6aba6e268367a2f1d803604aefe48404ec5b431c13dbb14374dc9e118736b43f342a3c93f57c707f58dab5f2359f88b48eb85c37d052105bdabb93a8e1f2866330f5548252ffbfd62b448fdeb777168701bdc6136a22bcc048e3679f6098c00ca7151267a4bb1c6561685f5f6fba0d1976a6b7999257a1e4d5155020b124f65e43dc06da593c7fdd96c6fd84afe493d2ade3624fd7672fc0fa7c77d97bea5be3c865655cc77440c7d28ea2cff6eb9bcf85780f2ff0e4215c8c18f63012aa4067fc1524e81c1b9d2e08975b0305c2a1add51a9471c9181835d923cf51b854cc659616fc1932e4997b2b3b737e661945abeb0d9b1fe3c113c2b2a8b371d8630927bcc23c21faff67fc6680ea0b3468b8a0279e3e160629bfccc7f1aac37b5aba4e275cb9cb8ada5c99361c70125a45c0536a9467343dbf1a22610ee2da7ab15fb8d3c5680cc447458f81523ee75668a3f75302693169b7a20349c35b77ef8e99cd3c8b852e4d1871972415de7b9bc9859697d7eab02e559f03cd57fae9e5d3d692e617a2cfbdc34eee3c9db4efdad6f1fb19a7c4907db5173f80ec204fe16919bcca832722c58273f8fb67f69e5d8b24c285aa1a74581f0f9d1fc11b42f578a1eafe7a7dc2c6f11065697d3207585344122314bcd914733132cd0bf5a661eb329dae384c0a85f559932a49d31facb17189716a38b20e3f0c0f30ab4686e79cfe9ad03c00fb0d726991869b6c89ea25da9450c3d6cc5bba1bb17c7a3c38962361890e3f7e24ce94253f63e12dcff3e2c3d045ad05f45ad9349575c7ccd0cd82fdf1e083c56dfed867382a4cf758decef9c05bea3138ca4507b7f638edbfe8f1512911ec00d4379fa996b4d64060078b95064cc81e92aff21e4b30d0b848a8d8d5ceab8f665e686881a79bb390d75f94c593be007bcd38d10de1e750df1a9256375c15e2bed4da66082248abb6d6660ec9ef6351125c245527c3b13e22d77ab516f2457d890f9dd5a6c8d0a21a4d626a5fcfc4bc3a427b5d3581830e070fc6b4c0ac8038bfa1aa52b12b0329410b6b8d0d78407817ec0cd708eaa5215921b14e113e8ceee38318fe47f3cf5a58c1ab86a6eb7734170e55b5002ab3a48cefe62fc6c897aeae4ef82eee968be5b59bf329cca3d03ad5dd38dc287eeda31de96cfb4ee94eca046b1d9e83632f1ce043b7c65782940b60bc0f9f35cde82d5623c83c6d3540139ba820af7bdada01c22a2531b1e3171e6befc5b8289868ccceedb49fe28a32bee69055d5e167eeaeb320832e0da67d6f536b7ea5226e86420f72ee68978a460d1a5f5d0f22e7b3fb59f68489f757581bb4fa107c14829bf5b93ee95f76a84c2f08514f73aa5c062585b57b02de19dd4039afc4480d8666cef6de93ea111a934295350433ecb4d7cd9957a0cc739439bd5308449d3ab744b76f429b3997611e9edfcc49f021f65645ac5524ddbc8fe9f49d8820633ea37d9e9dd5bfef0912fdc1ba80500df637ca3701122f543e99df4f528519c6233fe9cc94e5d2591124212b6a7711cb083018647b0e7200811aa58b82b5fbb10d347fb64ddf7ecc1e526d69bf7d0bf9ac4287ac42db1e4e1d9037ed3d9624cd19c590c460c87c71e7c5055cf0b78318761a5e5b8ea36978ac18275470e04d8e3da440cc7fda0b2fb7857bc2a0bda4843a60d21c3bfaaf7e32f16de155a161e01392be4ef0ab5df2bf0b18a0aaddcc364acf987c625c20fcb90b22e3a0bd6fdd161780a58517012cbfd7086a042f1e13b3f337ed2dfb4f66635d188287bfdadc3bab7a139c3ed8d784c2773836618e440f2f5ecabb712c9116a0d8536419ce663402b427556e899d12be13a588c66565c60fa3ee42f5b21cd3a8febc2b906eb91a778baf31aceeb53acb3556007cd7752fcda896c3a4a41cee337e5e63a5bd7d3d9be234295ccc93a1cd3b4c171f3a7210306901bcaabea6776909baa057ec840813de6ca414318b10d18787403f9ae1a57d671cecb824163683d2e8f3d40cd916a9e6d63aaf5f69dac13bf6cbcf9562a915febaf7d95e8fdc956018e42276703719e4f0d7c698051290d59531e034f884fe7794175006fa69b6b09897979881187a31d33c3728eb87e0562213ae81f502108314c35d590b02b4484caf58925d3f9620e89d5e4272be9fc2bcd587d337de2b815b64ebb3dc542dda0e64ff6d6037fff10941f565cb6814ac3058945d4fdd79c3f97819906551441b1914a4b6c4346a34d7d05315eeffa813cf95b83767317386bc21f1456ccf52cc983777764c02dd9c5ebf3940c19c8d4cd2c1e6366935211884e8aee011aac2fddcb0646cfb290d3f7ac0ea8fd1ec20b57f67a28a470673c4c202eb57409ca729c4ffadcb1828daed09ec1223d758548e477e7e06f8d9015df9a40b964438b2c59261a8527d0755b468601d381e60826627dad42f680f33a83027246eaa154ae2cf04ce7cfd0eae34735da1ecd1b408f4d41a9278115695b16a248cb697366105585a863b4629f6a6899d77dca911091f73e33a812f5baa98b3460edecd6cf2bda734810a7412943426e8d8e00e24afacc681379b92978ec8e049f34b22de5488e9ab25a7bd135ddcb766dcc95ee688268ef957b83130c6869bcf0bf43d606209c5071b00e32a20cd6da4bd4cc5492f435a62561348e3769093639c533570f22b9ecda5aa7c2773753211c68672101694fa491eaec2005a9cc439f773801afe423675efc54e302fdb5e09b895316e7ac8898343bcdd91ad238211e3318ddfe0d86c5ae1c6d83d791c52d9b35496b8ed9978a1c7b5dd00bc6c9697f39247238c2042258025121816660ef0c53a49995cd9b45ba1712d45695472b69d6757e56c572c0f290ad6225d35ca5e5f564ad2c3ac7fb0aa41ac346ec3036adaab5000b9c58d8861aed031b0e627a1e665c36dfceebc558f59df5b6235ee823de9d185d171a9237a20811d9b5e4efe508fb9907d25b6849dedff9e8d71fd0b2cff2f8ea8cc1a7e98b204295f267dae98a6476d99f9eb73499fc918e2db191b9decac79eef1b046e9958c32ff1c8bca28c125b2b2340f980219d5cc979eeabf2b2dadec09e43ea4bb67b136dd30cfcda9dbb611d899e5c3656d46bca5b43affc771c9eb1697b10936d6922c17baf10888d8fc10b4a891d4bfe5ead9136c4f79215c61d4796897fb39834f0440db29202211d82d2b16e69d9398fe33e22959a4310d274c67a4dc9ae6acf72abd13a1afb5fc319c3d5ae89933158c91ffc851e5e5fc854b102a047a0d30211afda1cb548d279fc894f1002c3721e3229519b560f0a71bb648149bc763b3401b9ad57704139a85d936bd0879a820f90be6ea7d78b6fe1679d336dcc2f776a3373e473ab5ee54f5e0a8df6114fd0e2d88ed6e7035d232eae1e4d785b417a43e06bdf7bfc3509bb4ad808d39ca785673436a1009dcce6cd055f10439de64652c31dcac2b65ec47264fec73581f8598f8c318bc0f8d0eb929e7c4d6e5f012238e01e19b1fd6b54feb463912e6557b65741ff8919434e44e04e41a5caadb74f3cc0254541c06faa480499326b7146499b55a2fced4dd416d5a31779c8de5dea938385217832cbc605788ff4fa96e24fdc3875b52769db18abdc76a687974925a52081043864ee8e39574d7f27fac30650eabb1fe70d5d7bab946b234d1b8ee449f08654ac868a22839924b9e7b94175e60c5b575263089b525f40d6f76570d2740095a43696a90f486332f45f4f6ee2d1d55d4b3f2311637a9e73f921a56ea0529104573b25b2c48307adcbe3b2232cc04933dae07c34c491099c57dd7e4b393fe34db10623bb35b74ce92f8b1892d24b9fd88eb7cfc3d1791b0bc3bddebcf42592093e6ffde9f40766ad3c21a67caed25b8b250946ec46773565fdcc6305803362274e612308b8b767423afffdb27239f3b9bcb8e70bf0ee68cc8a1af4e59f6972d7c90b182d862066d1b83ec044b259b398840cc736ece77dac61d07e4b338083a97146137dce132a2a8a0255d1c5aea80b5e0095b6181dbdf1fa23317c74c0e25cb86b08c4136065729777527f49f407256d36049e4c236201174b312e80090376a2113d62058b659f63de5b8695e52fd178c573ae0e14f205cb2fd929c9f09079930738deea7a2090288911af17e157d6c9ea4bc04f6264c81e4f3b5b087fd17a7c0b7eb631c1d1476ddce8be398fb7f576f3f01f98b823b7e8eca7cb3831c0ed3c036219c94ecf5a0c2d112f788d73a432394247fc0244de10e28d4577ecc084c521f0007f3a6215a549abb3091e59c6d6cd674c8e5775acd276c980033df3c2143be0589f65da81bfab1dff81e6783f814980bed3cf47e51dda6424ff43c2a82e966c7ec5b197a73cc580919db591cc522f87a561ca46753679b8d26110d1667bbcc24706633f219617f620958cfc35c1d70e034c0555bf76d9d854f8e5a0a973b88cd6096e5f3d2a72f3196f4bd42b32d6849fd37d10df37225b0cfe05c1005ff528c59352ad6e319f77da2ddf1c94b8b7e7c7a25c7cd15121540f31ec51725e71924c3616257756be8d404b53cb3091c6c3a11940c44cc8808426305550f35f9a4c5322222e661561bdecd8ace024f4c2fffeb3bc1bfa7f69454f6699d36dc42f35d78130f04b39ef392c5969a4d8de6d5271ee7a32af3f311386857fc0c21d053bae00a066ada63a28fd84b0fae439f1060a7b633ea07813863162195125e00d9c2aa7f25c11c981794cc97718f22e3cdbd21701d6f6f5fc25885be6330ac8b8266bd64a227535648a926a002345e224cb6bb4ec6767c1f1511c2c1753def927f97f9fd620c27eae292cca484472b6fe5d68aa58d3fbc772153043073308ec7c0a37c5355c0bdd43179025aa0e8d492353849fd7accf1a0acb7507f11281ab671d69ceaabbf539eae572f0015426f5e9a57e8c4d4d92216a93027572c11172cfb505e590e4d5899aa663a5ea32d5cc163ebef26c92908e15fe3c0705d06e32504c7901bebbbe7de2bb3f5ec28dd50299d941b24242ea0e151d747e129357f4fd6c98876dd0779fe40a7cd386f198150b43d5fced11d2906915937905f104a9ab5f0d05ddf227110e72a6c80a7878e559b5d8b1fd632e851633256254a22608358228feba5055a91dcfa6f702d55b8a463f0e0dd4af11872694e1b38ce25e8f8b4ec993d38223eba5dbc585fdd5ba53b179c20a278181fe3a0d13a7feb333941b59748b2b06b2c51d6c52d0a8bdcf833628cc30787d9d16c0991bb5a523a92da788563800133ac376a6feb09a8ce1de5c1ac1ce128272cdacb42b9592611e34bb0848a05f3c69e274b33481fd2b351ce7562413c2b2eee4cde66944b64103734d44e69b9187d0c98ee204f539c59b2374eef044e731b1bb3d06465f37d16634579e24f7ba512a29204e0e3fe7d89809914e41b7ad8d17e5ac11af5246b2192c833af76bad3906f95683240ea619360d9a0ad9e7e97777e2797180447e13067cdd4efb5ee7429fc5b97e80d3f9b8396b8d2df51ac47b434ee0109b0eaa22f854d4de4ada9dcc003a4a8e147ceed375d607922b59a50312813519571e2a526774b1241513f77d2b53d3d435f141c55eddad14021ccc542ac37aea8e24b9331fcceaa35dec147c114e181e9a6800f1748c3aa1b425d532da7ed19bf10ec5304900d6f685acba0fed0d7bde86d5d5dab0fe046efe66fe78b886d24c097fa252a7a9151e7ba2cf15798d1cdc07904c8f32c1853b8e79be4d344b6d8820005ff2e11ceaa7833e1de67fa3f64637bf20963bea0a981ee810562a0b8b2dd532e01140421a975d2e7f39c3988e99e5cc8116cd9462e2f405121dea5a03616d7fcad17ba0956b84671d94c524efaa13b6b0bcc15b98b6433619bbfc9736d5432fc5237b9a018b716c2e08e4eac57d2b7c33ffd813816a0b01f7d09a5afa082e1d10a327247e0f086f3360dd6b9b736903a518390b887a6dd2f5cfa65322e5fe6dd801ce6ff44388ecfcc02ab12c562f55484cbda45465049da5aba32131491f3bfd92a1f9e85fef13e622e06445252fe64b42680ba24d09d0cf28c9f517a24ac14322d57761c1d53491940ce65df7c1ce7b6f766409f66f6ab4a9b455df1de0316ea2281d48f8f2bfa74f001509e7d2222b7931453759b1cb0ef887335e62bf97aeb1fae49095be74c11f89ae74297e22a342f60ff851f43c1e695080527747f9326097ef0a6838a1f46daab1f109c4e921beb2ea1cb6a334dedc57e55e58469aea61a185933a8a4c5fcb5366487d36541c46f12d8b830ab05c415cdafeb875eb2031e154fdde2d07437c0297ba56e4416cab08544013b2728a42ac6daac513c436f3e07dc6aeee0957297b17c242e221c41187df1fe919602d84805486c0dfe8ba108bc4c5b95c300bf121307d465a3b69f99fece83b1ddba11e3a8ebde80cedaa52c7b5acc9d0417064415bfb6d8ff6ff46444a62619c9321171883977194fddaa0fb059c29139fea48955b1ceddae3ac8051923472d23f06773f49e6624a8f2ba23b36a6c574d85b40f701d705c983ad5bce01bb2d031617bf8b2769ffc90d2ef086be32b2e84cad752afdee010c8e26ee09ea95e963b57e93188d4af84a71a5224e383a0dde184ed343fc59f77620045b9d6cffdb1a89ef0d5cb7e853810f24523a211f2a2fc19a49729d02fbc51b9674783ed4c7c0ef070c6b47413681ee65934cbc65777f92d13f01879a5851ca57501b5cd016cce8e9902f4379997d5c75b3558161552ebe9eb325a3ee76fe87bfde3d2cb44fb9009bcf14ea07900a9e120367d6a0253fba1b57f388efab647bcbad82a4ef8b5a7158c3d0ddfd134e568c937375a9df0304080a5ae883ec3661b176c272c7dd87aa40f209c9fef9d07bd102818b55f3e7a8c0e06dad914acae44f9023a04395e12b1d41b7d425634c748713a709547376cff51e864654c3b24b19d294b99c5e61abbc2f270163995512a5d042d94e1df1b49bc9f2fc814d7409f97758487ea49b276ce60ba980b9e2df67e1a186a18ea60ac743f45714b80eeb90cc06d7158f6cf09c54a858fef698ffe7032c22de1618bfd90bbe8dd7db0aba29f53abf0aca67e1471bee1c871bf3595f423213645b9dd88a4153c7dee3845194b496abcc0104997fc89eebd4fc72c5ca73cb5789cbbec9cdd9cb0cf9117718041860fb2073fb3842c45cd1a8e44a295b3166e1ab0a5a18aaf22575454d4d3750aa838944d1f5caf671d9402cf331785d83c50c29b2e49d402ce4ba32516fe1b37c463359d7cb780d3561ac8c6ecf1626d33aa1b482f343086e740be27175fbe1cc6ff2f798b9cc96e2d88baa6d80b0e443374cdc7f11cda7def2a1875595f6980d0779641c9777632f7a5aebeb48f3e9d01c187695217de185ea6c99cd47b321c53c26a83cd2f749612ca143a2a3a594c2e80c1ec90d98564f2a69e579f9713c20af6a559f541514a7514b8fd88165be486e825c3de6899e97f9e39a1654e51ed9690cefd5e225602e68480e9f2346b3f8d61268dc5f6add54484b7f4bf8fe086110f74183a0bc2515056f9a0bad6f4dbbd848d80df7a24d5d92a6b6b2b16fabc0486028a13b67a2bbe17f9812ccf6692327e78a5dfeb56feb6dda38c9ff2360062cf7128f357ab4b395b26880fdf80dd889ab809da70b1f58f5c1005a3e054f9c40691c287163f445610bdf0f14e6cb73e0eac5ab2fa5a9db4a9774c135e92200c27a34aaa2f3b9e2d568d28ae69652f1a351636492a590f79df0d4f9d589ba0651aa572a6cf9070a873ce75a200506ca74339dafbf0da6add65d5b5594b5b49bb0b63b398b44ff5f996115ce0d1b6f220b7b7b20049f5fc9f26e4fd8b7668578c4dda8365a9e79db99378091b3572473747ca3b4104e7b7bb1bc22988df73cc1e6fdb242c78804b4334550788a3e6a50a17fd8a0985f2449eb8554bc28c989cc1d9755aac8b33482d2c70525e102b4518f7aed260cbc6d6e7317a22e92b776183ccd105ac9bfbed1bbf0da4be0c0203a390cb9b488ed0aaa098ba8d5ebb9659fbf19b8cbc1b4d78edae8f5f272d2bf0303fdedd0e3de90ec360fced0ebbc85d612a9c396ba66cde481b92efc7ac9a1381dea34d170791896ea030cfce7234a0a2d2301bc79043aa7dfed6f3ea7bbe28a344a274fd9b9670b70cd74379a6fa1849a26609f1ef892c0073b7db34f09b8631af1884552a234f7f37263e04c61418b602bddc8561c0e097c5767fdeddc5deabac6308e5ae1c41565baa4d0bb33718486f3c655245f26b26a12becdc95a31819afa5729cbf3127bca5596f9d409301a32e80a3b7ec270b9387e88e9e1422b9ac903aab06f29970b50673222a3460acc1ed2a6ed45d10d3b42887d803e880f39141ce63b8d3fcaf49d87a9ec7f9720808aeaf8eab0ab37ab690f97cbc125be08fd6cd41df957059878d19b106b39e06bf30cc9d3c85f25de650f4afd295dc11025f9a72a5e46422d88c01972e22bf024fe61e0dfb824df1bf44d1f77db0118127ae48e2a145d82bcb537cc4be81e7bbe0e1d3e19b56537e7bee8931d4fc38a03d1c387079b64590d1f775566cf0a16ca2bbad3409c29ca616d8f91040a3ef52bd0b7fb2dea0b65f0841a03fcf8d25a6d7a0904a74ca61835e3e0734e4addddd167cbe9d3ccc89a0965191fd70065b0fc48f450ebd036f9c15039393ebd191a982fff506847cb3de8eed4cb8074da6076302b1e7623cbf5bb1ac4d6af09fad853100eb1aeb302717031608283897f3edb7e5b7f5768a363eb2bfe951b6a9c470d9abfad6ebabd17dadc0ec021684726a3f7daceb4acfed42084e70c8e721034f481300",