diff --git a/.github/workflows/ci-basic.yml b/.github/workflows/ci-basic.yml index c2cde8686bf..226f1528046 100644 --- a/.github/workflows/ci-basic.yml +++ b/.github/workflows/ci-basic.yml @@ -16,9 +16,10 @@ jobs: ROCKSDB_LIB_DIR: /usr/lib # Use system-installed Snappy library for compression in RocksDB SNAPPY_LIB_DIR: /usr/lib/x86_64-linux-gnu + # FIXME: remove the following commented lines # Enable the `nu6` feature in `zcash_protocol` - RUSTFLAGS: '--cfg zcash_unstable="nu6"' - RUSTDOCFLAGS: '--cfg zcash_unstable="nu6"' + #RUSTFLAGS: '--cfg zcash_unstable="nu6"' + #RUSTDOCFLAGS: '--cfg zcash_unstable="nu6"' steps: - uses: actions/checkout@v4 diff --git a/Cargo.lock b/Cargo.lock index 6690a968028..08054b362ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5854,7 +5854,7 @@ dependencies = [ "which", "zcash_address", "zcash_encoding", - "zcash_keys 0.2.0 (git+https://github.com/QED-it/librustzcash?branch=zsa1)", + "zcash_keys", "zcash_note_encryption", "zcash_primitives", "zcash_protocol", @@ -5881,32 +5881,6 @@ dependencies = [ "primitive-types", ] -[[package]] -name = "zcash_keys" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "663489ffb4e51bc4436ff8796832612a9ff3c6516f1c620b5a840cb5dcd7b866" -dependencies = [ - "bech32", - "blake2b_simd", - "bls12_381", - "bs58", - "document-features", - "group", - "memuse", - "nonempty", - "rand_core 0.6.4", - "sapling-crypto", - "secrecy", - "subtle", - "tracing", - "zcash_address", - "zcash_encoding", - "zcash_primitives", - "zcash_protocol", - "zip32", -] - [[package]] name = "zcash_keys" version = "0.2.0" @@ -6060,6 +6034,7 @@ dependencies = [ "itertools 0.13.0", "jubjub", "lazy_static", + "nonempty", "num-integer", "orchard", "primitive-types", @@ -6293,7 +6268,7 @@ dependencies = [ "tracing-subscriber", "zcash_address", "zcash_client_backend", - "zcash_keys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zcash_keys", "zcash_note_encryption", "zcash_primitives", "zebra-chain", diff --git a/Cargo.toml b/Cargo.toml index dda73229b91..60ada1bd222 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -115,3 +115,4 @@ zcash_address = { version = "0.3.2", git = "https://github.com/QED-it/librustzca zcash_encoding = { version = "0.2.0", git = "https://github.com/QED-it/librustzcash", branch = "zsa1" } zcash_history = { version = "0.4.0", git = "https://github.com/QED-it/librustzcash", branch = "zsa1" } zcash_client_backend = { version = "0.12.1", git = "https://github.com/QED-it/librustzcash", branch = "zsa1" } +zcash_keys = { version = "0.2.0", git = "https://github.com/QED-it/librustzcash", branch = "zsa1" } diff --git a/zebra-chain/Cargo.toml b/zebra-chain/Cargo.toml index 0a866a9153c..37e013c0924 100644 --- a/zebra-chain/Cargo.toml +++ b/zebra-chain/Cargo.toml @@ -16,6 +16,7 @@ categories = ["asynchronous", "cryptography::cryptocurrencies", "encoding"] [features] default = [] +#default = ["tx-v6"] # Production features that activate extra functionality @@ -60,6 +61,11 @@ proptest-impl = [ bench = ["zebra-test"] +# Support for transaction version 6 +tx-v6 = [ + "nonempty" +] + [dependencies] # Cryptography @@ -104,6 +110,9 @@ sapling-crypto.workspace = true zcash_protocol.workspace = true zcash_address.workspace = true +# Used for orchard serialization +nonempty = { version = "0.7", optional = true } + # Time chrono = { version = "0.4.38", default-features = false, features = ["clock", "std", "serde"] } humantime = "2.1.0" @@ -180,5 +189,5 @@ name = "redpallas" harness = false # FIXME: remove this and all zcash_unstable usage in the code after updating librustzcash -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(zcash_unstable, values("nu6"))'] } +#[lints.rust] +#unexpected_cfgs = { level = "warn", check-cfg = ['cfg(zcash_unstable, values("nu6"))'] } diff --git a/zebra-chain/src/lib.rs b/zebra-chain/src/lib.rs index 460d3a850f0..dd6fa1a1abf 100644 --- a/zebra-chain/src/lib.rs +++ b/zebra-chain/src/lib.rs @@ -41,6 +41,9 @@ pub mod transparent; pub mod value_balance; pub mod work; +#[cfg(feature = "tx-v6")] +pub mod orchard_zsa; + #[cfg(any(test, feature = "proptest-impl"))] pub use block::LedgerState; diff --git a/zebra-chain/src/orchard.rs b/zebra-chain/src/orchard.rs index be96644c8c9..3141b6a1154 100644 --- a/zebra-chain/src/orchard.rs +++ b/zebra-chain/src/orchard.rs @@ -6,6 +6,7 @@ mod action; mod address; mod commitment; mod note; +mod orchard_flavor_ext; mod sinsemilla; #[cfg(any(test, feature = "proptest-impl"))] @@ -22,4 +23,13 @@ pub use address::Address; pub use commitment::{CommitmentRandomness, NoteCommitment, ValueCommitment}; pub use keys::Diversifier; pub use note::{EncryptedNote, Note, Nullifier, WrappedNoteKey}; +pub use orchard_flavor_ext::{OrchardFlavorExt, OrchardVanilla}; pub use shielded_data::{AuthorizedAction, Flags, ShieldedData}; + +pub(crate) use shielded_data::ActionCommon; + +#[cfg(feature = "tx-v6")] +pub use orchard_flavor_ext::OrchardZSA; + +#[cfg(feature = "tx-v6")] +pub(crate) use crate::orchard_zsa::issuance::IssueData; diff --git a/zebra-chain/src/orchard/action.rs b/zebra-chain/src/orchard/action.rs index ae7690def7a..4f256408f11 100644 --- a/zebra-chain/src/orchard/action.rs +++ b/zebra-chain/src/orchard/action.rs @@ -11,6 +11,7 @@ use super::{ commitment::{self, ValueCommitment}, keys, note::{self, Nullifier}, + OrchardFlavorExt, }; /// An Action description, as described in the [Zcash specification §7.3][actiondesc]. @@ -21,7 +22,7 @@ use super::{ /// /// [actiondesc]: https://zips.z.cash/protocol/nu5.pdf#actiondesc #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct Action { +pub struct Action { /// A value commitment to net value of the input note minus the output note pub cv: commitment::ValueCommitment, /// The nullifier of the input note being spent. @@ -35,14 +36,14 @@ pub struct Action { /// encrypted private key in `out_ciphertext`. pub ephemeral_key: keys::EphemeralPublicKey, /// A ciphertext component for the encrypted output note. - pub enc_ciphertext: note::EncryptedNote, + pub enc_ciphertext: V::EncryptedNote, /// A ciphertext component that allows the holder of a full viewing key to /// recover the recipient diversified transmission key and the ephemeral /// private key (and therefore the entire note plaintext). pub out_ciphertext: note::WrappedNoteKey, } -impl ZcashSerialize for Action { +impl ZcashSerialize for Action { fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { self.cv.zcash_serialize(&mut writer)?; writer.write_all(&<[u8; 32]>::from(self.nullifier)[..])?; @@ -55,7 +56,7 @@ impl ZcashSerialize for Action { } } -impl ZcashDeserialize for Action { +impl ZcashDeserialize for Action { fn zcash_deserialize(mut reader: R) -> Result { // # Consensus // @@ -93,7 +94,8 @@ impl ZcashDeserialize for Action { // https://zips.z.cash/protocol/protocol.pdf#concretesym but fixed to // 580 bytes in https://zips.z.cash/protocol/protocol.pdf#outputencodingandconsensus // See [`note::EncryptedNote::zcash_deserialize`]. - enc_ciphertext: note::EncryptedNote::zcash_deserialize(&mut reader)?, + // FIXME: don't mention about 580 here as this should work for OrchardZSA too? + enc_ciphertext: V::EncryptedNote::zcash_deserialize(&mut reader)?, // Type is `Sym.C`, i.e. `𝔹^Y^{\[N\]}`, i.e. arbitrary-sized byte arrays // https://zips.z.cash/protocol/protocol.pdf#concretesym but fixed to // 80 bytes in https://zips.z.cash/protocol/protocol.pdf#outputencodingandconsensus diff --git a/zebra-chain/src/orchard/arbitrary.rs b/zebra-chain/src/orchard/arbitrary.rs index 7a6544606f8..54572085f11 100644 --- a/zebra-chain/src/orchard/arbitrary.rs +++ b/zebra-chain/src/orchard/arbitrary.rs @@ -10,17 +10,18 @@ use reddsa::{orchard::SpendAuth, Signature, SigningKey, VerificationKey, Verific use proptest::{array, collection::vec, prelude::*}; use super::{ - keys::*, note, tree, Action, AuthorizedAction, Flags, NoteCommitment, ValueCommitment, + keys::*, note, tree, Action, AuthorizedAction, Flags, NoteCommitment, OrchardFlavorExt, + OrchardVanilla, ValueCommitment, }; -impl Arbitrary for Action { +impl Arbitrary for Action { type Parameters = (); fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { ( any::(), any::(), - any::(), + any::>(), any::(), ) .prop_map(|(nullifier, rk, enc_ciphertext, out_ciphertext)| Self { @@ -54,11 +55,11 @@ impl Arbitrary for note::Nullifier { type Strategy = BoxedStrategy; } -impl Arbitrary for AuthorizedAction { +impl Arbitrary for AuthorizedAction { type Parameters = (); fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - (any::(), any::()) + (any::>(), any::()) .prop_map(|(action, spend_auth_sig)| Self { action, spend_auth_sig: spend_auth_sig.0, diff --git a/zebra-chain/src/orchard/note/arbitrary.rs b/zebra-chain/src/orchard/note/arbitrary.rs index e9365de80c1..7968877d9bd 100644 --- a/zebra-chain/src/orchard/note/arbitrary.rs +++ b/zebra-chain/src/orchard/note/arbitrary.rs @@ -2,13 +2,13 @@ use proptest::{collection::vec, prelude::*}; use super::*; -impl Arbitrary for EncryptedNote { +impl Arbitrary for EncryptedNote { type Parameters = (); fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - (vec(any::(), 580)) + (vec(any::(), N)) .prop_map(|v| { - let mut bytes = [0; 580]; + let mut bytes = [0; N]; bytes.copy_from_slice(v.as_slice()); Self(bytes) }) diff --git a/zebra-chain/src/orchard/note/ciphertexts.rs b/zebra-chain/src/orchard/note/ciphertexts.rs index b27ffbc53a1..68f92dc667d 100644 --- a/zebra-chain/src/orchard/note/ciphertexts.rs +++ b/zebra-chain/src/orchard/note/ciphertexts.rs @@ -1,7 +1,5 @@ //! Encrypted parts of Orchard notes. -// FIXME: make it a generic and add support for OrchardZSA (encrypted tote size ofr it is not 580!) - use std::{fmt, io}; use serde_big_array::BigArray; @@ -12,20 +10,20 @@ use crate::serialization::{SerializationError, ZcashDeserialize, ZcashSerialize} /// /// Corresponds to the Orchard 'encCiphertext's #[derive(Deserialize, Serialize)] -pub struct EncryptedNote(#[serde(with = "BigArray")] pub(crate) [u8; 580]); +pub struct EncryptedNote(#[serde(with = "BigArray")] pub(crate) [u8; N]); // These impls all only exist because of array length restrictions. // TODO: use const generics https://github.com/ZcashFoundation/zebra/issues/2042 -impl Copy for EncryptedNote {} +impl Copy for EncryptedNote {} -impl Clone for EncryptedNote { +impl Clone for EncryptedNote { fn clone(&self) -> Self { *self } } -impl fmt::Debug for EncryptedNote { +impl fmt::Debug for EncryptedNote { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_tuple("EncryptedNote") .field(&hex::encode(&self.0[..])) @@ -33,36 +31,36 @@ impl fmt::Debug for EncryptedNote { } } -impl Eq for EncryptedNote {} +impl Eq for EncryptedNote {} -impl From<[u8; 580]> for EncryptedNote { - fn from(bytes: [u8; 580]) -> Self { +impl From<[u8; N]> for EncryptedNote { + fn from(bytes: [u8; N]) -> Self { EncryptedNote(bytes) } } -impl From for [u8; 580] { - fn from(enc_ciphertext: EncryptedNote) -> Self { +impl From> for [u8; N] { + fn from(enc_ciphertext: EncryptedNote) -> Self { enc_ciphertext.0 } } -impl PartialEq for EncryptedNote { +impl PartialEq for EncryptedNote { fn eq(&self, other: &Self) -> bool { self.0[..] == other.0[..] } } -impl ZcashSerialize for EncryptedNote { +impl ZcashSerialize for EncryptedNote { fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { writer.write_all(&self.0[..])?; Ok(()) } } -impl ZcashDeserialize for EncryptedNote { +impl ZcashDeserialize for EncryptedNote { fn zcash_deserialize(mut reader: R) -> Result { - let mut bytes = [0; 580]; + let mut bytes = [0; N]; reader.read_exact(&mut bytes[..])?; Ok(Self(bytes)) } @@ -127,13 +125,16 @@ impl ZcashDeserialize for WrappedNoteKey { } } +#[cfg(test)] +use crate::orchard::OrchardFlavorExt; + #[cfg(test)] use proptest::prelude::*; #[cfg(test)] proptest! { #[test] - fn encrypted_ciphertext_roundtrip(ec in any::()) { + fn encrypted_ciphertext_roundtrip(ec in any::>()) { let _init_guard = zebra_test::init(); let mut data = Vec::new(); diff --git a/zebra-chain/src/orchard/orchard_flavor_ext.rs b/zebra-chain/src/orchard/orchard_flavor_ext.rs new file mode 100644 index 00000000000..f8ce25d0ed5 --- /dev/null +++ b/zebra-chain/src/orchard/orchard_flavor_ext.rs @@ -0,0 +1,84 @@ +//! This module defines traits and structures for supporting the Orchard Shielded Protocol +//! for `V5` and `V6` versions of the transaction. +use std::{fmt::Debug, io}; + +use serde::{de::DeserializeOwned, Serialize}; + +#[cfg(any(test, feature = "proptest-impl"))] +use proptest_derive::Arbitrary; + +use orchard::{note_encryption::OrchardDomainCommon, orchard_flavor}; + +use crate::serialization::{SerializationError, ZcashDeserialize, ZcashSerialize}; + +use super::note; + +#[cfg(feature = "tx-v6")] +use crate::orchard_zsa::burn::BurnItem; + +/// A trait representing compile-time settings of Orchard Shielded Protocol used in +/// the transactions `V5` and `V6`. +pub trait OrchardFlavorExt: Clone + Debug { + /// A type representing an encrypted note for this protocol version. + /// A type representing an encrypted note for this protocol version. + type EncryptedNote: Clone + + Debug + + PartialEq + + Eq + + DeserializeOwned + + Serialize + + ZcashDeserialize + + ZcashSerialize; + + /// FIXME: add doc + type Flavor: orchard_flavor::OrchardFlavor; + + /// The size of the encrypted note for this protocol version. + const ENCRYPTED_NOTE_SIZE: usize = Self::Flavor::ENC_CIPHERTEXT_SIZE; + + /// A type representing a burn field for this protocol version. + type BurnType: Clone + Debug + Default + ZcashDeserialize + ZcashSerialize; +} + +/// A structure representing a tag for Orchard protocol variant used for the transaction version `V5`. +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))] +pub struct OrchardVanilla; + +/// A structure representing a tag for Orchard protocol variant used for the transaction version `V6` +/// (which ZSA features support). +#[cfg(feature = "tx-v6")] +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))] +pub struct OrchardZSA; + +/// A special marker type indicating the absence of a burn field in Orchard ShieldedData for `V5` transactions. +/// Useful for unifying ShieldedData serialization and deserialization implementations across various +/// Orchard protocol variants (i.e. various transaction versions). +#[derive(Default, Clone, Debug, PartialEq, Eq, Serialize)] +pub struct NoBurn; + +impl ZcashSerialize for NoBurn { + fn zcash_serialize(&self, mut _writer: W) -> Result<(), io::Error> { + Ok(()) + } +} + +impl ZcashDeserialize for NoBurn { + fn zcash_deserialize(mut _reader: R) -> Result { + Ok(Self) + } +} + +impl OrchardFlavorExt for OrchardVanilla { + type Flavor = orchard_flavor::OrchardVanilla; + type EncryptedNote = note::EncryptedNote<{ Self::ENCRYPTED_NOTE_SIZE }>; + type BurnType = NoBurn; +} + +#[cfg(feature = "tx-v6")] +impl OrchardFlavorExt for OrchardZSA { + type Flavor = orchard_flavor::OrchardZSA; + type EncryptedNote = note::EncryptedNote<{ Self::ENCRYPTED_NOTE_SIZE }>; + type BurnType = Vec; +} diff --git a/zebra-chain/src/orchard/shielded_data.rs b/zebra-chain/src/orchard/shielded_data.rs index 5347919cd01..baf98da8c42 100644 --- a/zebra-chain/src/orchard/shielded_data.rs +++ b/zebra-chain/src/orchard/shielded_data.rs @@ -20,9 +20,17 @@ use crate::{ }, }; +use super::OrchardFlavorExt; + +#[cfg(not(feature = "tx-v6"))] +use super::OrchardVanilla; + +#[cfg(feature = "tx-v6")] +use super::OrchardZSA; + /// A bundle of [`Action`] descriptions and signature data. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] -pub struct ShieldedData { +pub struct ShieldedData { /// The orchard flags for this transaction. /// Denoted as `flagsOrchard` in the spec. pub flags: Flags, @@ -37,13 +45,18 @@ pub struct ShieldedData { pub proof: Halo2Proof, /// The Orchard Actions, in the order they appear in the transaction. /// Denoted as `vActionsOrchard` and `vSpendAuthSigsOrchard` in the spec. - pub actions: AtLeastOne, + pub actions: AtLeastOne>, /// A signature on the transaction `sighash`. /// Denoted as `bindingSigOrchard` in the spec. pub binding_sig: Signature, + + #[cfg(feature = "tx-v6")] + /// Assets intended for burning + /// Denoted as `vAssetBurn` in the spec (ZIP 230). + pub burn: V::BurnType, } -impl fmt::Display for ShieldedData { +impl fmt::Display for ShieldedData { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut fmter = f.debug_struct("orchard::ShieldedData"); @@ -59,13 +72,18 @@ impl fmt::Display for ShieldedData { } } -impl ShieldedData { +impl ShieldedData { /// Iterate over the [`Action`]s for the [`AuthorizedAction`]s in this /// transaction, in the order they appear in it. - pub fn actions(&self) -> impl Iterator { + pub fn actions(&self) -> impl Iterator> { self.actions.actions() } + /// FIXME: add a doc comment + pub fn action_commons(&self) -> impl Iterator + '_ { + self.actions.actions().map(|action| action.into()) + } + /// Collect the [`Nullifier`]s for this transaction. pub fn nullifiers(&self) -> impl Iterator { self.actions().map(|action| &action.nullifier) @@ -119,9 +137,9 @@ impl ShieldedData { } } -impl AtLeastOne { +impl AtLeastOne> { /// Iterate over the [`Action`]s of each [`AuthorizedAction`]. - pub fn actions(&self) -> impl Iterator { + pub fn actions(&self) -> impl Iterator> { self.iter() .map(|authorized_action| &authorized_action.action) } @@ -131,23 +149,64 @@ impl AtLeastOne { /// /// Every authorized Orchard `Action` must have a corresponding `SpendAuth` signature. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] -pub struct AuthorizedAction { +pub struct AuthorizedAction { /// The action description of this Action. - pub action: Action, + pub action: Action, /// The spend signature. pub spend_auth_sig: Signature, } -impl AuthorizedAction { +impl AuthorizedAction { + /// The size of a single Action + /// + /// Actions are 5 * 32 + ENCRYPTED_NOTE_SIZE + 80 bytes so the total size of each Action is 820 bytes. + /// [7.5 Action Description Encoding and Consensus][ps] + /// + /// [ps]: + pub const ACTION_SIZE: u64 = 5 * 32 + (V::ENCRYPTED_NOTE_SIZE as u64) + 80; + + /// The size of a single `Signature`. + /// + /// Each Signature is 64 bytes. + /// [7.1 Transaction Encoding and Consensus][ps] + /// + /// [ps]: + pub const SPEND_AUTH_SIG_SIZE: u64 = 64; + + /// The size of a single AuthorizedAction + /// + /// Each serialized `Action` has a corresponding `Signature`. + pub const AUTHORIZED_ACTION_SIZE: u64 = Self::ACTION_SIZE + Self::SPEND_AUTH_SIG_SIZE; + + /// The maximum number of actions in the transaction. + // Since a serialized Vec uses at least one byte for its length, + // and the signature is required, + // a valid max allocation can never exceed this size + pub const ACTION_MAX_ALLOCATION: u64 = (MAX_BLOCK_BYTES - 1) / Self::AUTHORIZED_ACTION_SIZE; + + // To be but we ensure ACTION_MAX_ALLOCATION is less than 2^16 on compile time + // (this is a workaround, as static_assertions::const_assert! doesn't work for generics, + // see TrustedPreallocate for Action) + const _ACTION_MAX_ALLOCATION_OK: u64 = (1 << 16) - Self::ACTION_MAX_ALLOCATION; + /* FIXME: remove this + const ACTION_MAX_ALLOCATION_OK: () = assert!( + Self::ACTION_MAX_ALLOCATION < 1, //(1 << 16), + "must be less than 2^16" + ); + */ + /// Split out the action and the signature for V5 transaction /// serialization. - pub fn into_parts(self) -> (Action, Signature) { + pub fn into_parts(self) -> (Action, Signature) { (self.action, self.spend_auth_sig) } // Combine the action and the spend auth sig from V5 transaction /// deserialization. - pub fn from_parts(action: Action, spend_auth_sig: Signature) -> AuthorizedAction { + pub fn from_parts( + action: Action, + spend_auth_sig: Signature, + ) -> AuthorizedAction { AuthorizedAction { action, spend_auth_sig, @@ -155,38 +214,48 @@ impl AuthorizedAction { } } -/// The size of a single Action -/// -/// Actions are 5 * 32 + 580 + 80 bytes so the total size of each Action is 820 bytes. -/// [7.5 Action Description Encoding and Consensus][ps] -/// -/// [ps]: -pub const ACTION_SIZE: u64 = 5 * 32 + 580 + 80; +// TODO: FIXME: Consider moving it to transaction.rs as it's not used here. Or move its usage here from transaction.rs. +/// A struct that contains values of several fields of an `Action` struct. +/// Those fields are used in other parts of the code that call the `orchard_actions()` method of the `Transaction`. +/// The goal of using `ActionCommon` is that it's not a generic, unlike `Action`, so it can be returned from Transaction methods +/// (the fields of `ActionCommon` do not depend on the generic parameter `Version` of `Action`). +pub struct ActionCommon { + /// A reference to the value commitment to the net value of the input note minus the output note. + pub cv: super::commitment::ValueCommitment, + /// A reference to the nullifier of the input note being spent. + pub nullifier: super::note::Nullifier, + /// A reference to the randomized validating key for `spendAuthSig`. + pub rk: reddsa::VerificationKeyBytes, + /// A reference to the x-coordinate of the note commitment for the output note. + pub cm_x: pallas::Base, +} -/// The size of a single `Signature`. -/// -/// Each Signature is 64 bytes. -/// [7.1 Transaction Encoding and Consensus][ps] -/// -/// [ps]: -pub const SPEND_AUTH_SIG_SIZE: u64 = 64; +impl From<&Action> for ActionCommon { + fn from(action: &Action) -> Self { + Self { + cv: action.cv, + nullifier: action.nullifier, + rk: action.rk, + cm_x: action.cm_x, + } + } +} -/// The size of a single AuthorizedAction -/// -/// Each serialized `Action` has a corresponding `Signature`. -pub const AUTHORIZED_ACTION_SIZE: u64 = ACTION_SIZE + SPEND_AUTH_SIG_SIZE; +/* +struct AssertBlockSizeLimit; + +impl AssertBlockSizeLimit { + const OK: () = assert!(N < (1 << 16), "must be less than 2^16"); +} +*/ /// The maximum number of orchard actions in a valid Zcash on-chain transaction V5. /// /// If a transaction contains more actions than can fit in maximally large block, it might be /// valid on the network and in the mempool, but it can never be mined into a block. So /// rejecting these large edge-case transactions can never break consensus. -impl TrustedPreallocate for Action { +impl TrustedPreallocate for Action { fn max_allocation() -> u64 { - // Since a serialized Vec uses at least one byte for its length, - // and the signature is required, - // a valid max allocation can never exceed this size - const MAX: u64 = (MAX_BLOCK_BYTES - 1) / AUTHORIZED_ACTION_SIZE; // # Consensus // // > [NU5 onward] nSpendsSapling, nOutputsSapling, and nActionsOrchard MUST all be less than 2^16. @@ -196,15 +265,28 @@ impl TrustedPreallocate for Action { // This acts as nActionsOrchard and is therefore subject to the rule. // The maximum value is actually smaller due to the block size limit, // but we ensure the 2^16 limit with a static assertion. - static_assertions::const_assert!(MAX < (1 << 16)); - MAX + // + // TODO: FIXME: find a better way to use static check (see https://github.com/nvzqz/static-assertions/issues/40, + // https://users.rust-lang.org/t/how-do-i-static-assert-a-property-of-a-generic-u32-parameter/76307)? + // The following expression doesn't work for generics, so a workaround with _ACTION_MAX_ALLOCATION_OK in + // AuthorizedAction impl is used instead: + // static_assertions::const_assert!(AuthorizedAction::::ACTION_MAX_ALLOCATION < (1 << 16)); + AuthorizedAction::::ACTION_MAX_ALLOCATION } } impl TrustedPreallocate for Signature { fn max_allocation() -> u64 { // Each signature must have a corresponding action. - Action::max_allocation() + #[cfg(not(feature = "tx-v6"))] + let result = Action::::max_allocation(); + + // TODO: FIXME: Check this: V6 is used as it provides the max size of the action. + // So it's used even for V5 - is this correct? + #[cfg(feature = "tx-v6")] + let result = Action::::max_allocation(); + + result } } diff --git a/zebra-chain/src/orchard/tests/preallocate.rs b/zebra-chain/src/orchard/tests/preallocate.rs index 79f6a16e7d9..6b1fadfce29 100644 --- a/zebra-chain/src/orchard/tests/preallocate.rs +++ b/zebra-chain/src/orchard/tests/preallocate.rs @@ -4,10 +4,7 @@ use reddsa::{orchard::SpendAuth, Signature}; use crate::{ block::MAX_BLOCK_BYTES, - orchard::{ - shielded_data::{ACTION_SIZE, AUTHORIZED_ACTION_SIZE}, - Action, AuthorizedAction, - }, + orchard::{Action, AuthorizedAction, OrchardVanilla}, serialization::{arbitrary::max_allocation_is_big_enough, TrustedPreallocate, ZcashSerialize}, }; @@ -17,16 +14,16 @@ proptest! { /// Confirm that each `AuthorizedAction` takes exactly AUTHORIZED_ACTION_SIZE /// bytes when serialized. #[test] - fn authorized_action_size_is_small_enough(authorized_action in ::arbitrary_with(())) { + fn authorized_action_size_is_small_enough(authorized_action in >::arbitrary_with(())) { let (action, spend_auth_sig) = authorized_action.into_parts(); let mut serialized_len = action.zcash_serialize_to_vec().expect("Serialization to vec must succeed").len(); serialized_len += spend_auth_sig.zcash_serialize_to_vec().expect("Serialization to vec must succeed").len(); - prop_assert!(serialized_len as u64 == AUTHORIZED_ACTION_SIZE) + prop_assert!(serialized_len as u64 == AuthorizedAction::::AUTHORIZED_ACTION_SIZE) } /// Verify trusted preallocation for `AuthorizedAction` and its split fields #[test] - fn authorized_action_max_allocation_is_big_enough(authorized_action in ::arbitrary_with(())) { + fn authorized_action_max_allocation_is_big_enough(authorized_action in >::arbitrary_with(())) { let (action, spend_auth_sig) = authorized_action.into_parts(); let ( @@ -37,12 +34,14 @@ proptest! { ) = max_allocation_is_big_enough(action); // Calculate the actual size of all required Action fields - prop_assert!((smallest_disallowed_serialized_len as u64)/ACTION_SIZE*AUTHORIZED_ACTION_SIZE >= MAX_BLOCK_BYTES); - prop_assert!((largest_allowed_serialized_len as u64)/ACTION_SIZE*AUTHORIZED_ACTION_SIZE <= MAX_BLOCK_BYTES); + prop_assert!((smallest_disallowed_serialized_len as u64)/AuthorizedAction::::ACTION_SIZE* + AuthorizedAction::::AUTHORIZED_ACTION_SIZE >= MAX_BLOCK_BYTES); + prop_assert!((largest_allowed_serialized_len as u64)/AuthorizedAction::::ACTION_SIZE* + AuthorizedAction::::AUTHORIZED_ACTION_SIZE <= MAX_BLOCK_BYTES); // Check the serialization limits for `Action` - prop_assert!(((smallest_disallowed_vec_len - 1) as u64) == Action::max_allocation()); - prop_assert!((largest_allowed_vec_len as u64) == Action::max_allocation()); + prop_assert!(((smallest_disallowed_vec_len - 1) as u64) == Action::::max_allocation()); + prop_assert!((largest_allowed_vec_len as u64) == Action::::max_allocation()); prop_assert!((largest_allowed_serialized_len as u64) <= MAX_BLOCK_BYTES); let ( diff --git a/zebra-chain/src/orchard_zsa.rs b/zebra-chain/src/orchard_zsa.rs new file mode 100644 index 00000000000..9806850daf9 --- /dev/null +++ b/zebra-chain/src/orchard_zsa.rs @@ -0,0 +1,7 @@ +//! Orchard ZSA related functionality. + +pub mod burn; +pub mod issuance; +pub mod serialize; + +pub use burn::BurnItem; diff --git a/zebra-chain/src/orchard_zsa/burn.rs b/zebra-chain/src/orchard_zsa/burn.rs new file mode 100644 index 00000000000..04c878feb49 --- /dev/null +++ b/zebra-chain/src/orchard_zsa/burn.rs @@ -0,0 +1,80 @@ +//! Orchard ZSA burn related functionality. + +use std::io; + +use crate::{ + amount::Amount, + block::MAX_BLOCK_BYTES, + serialization::{SerializationError, TrustedPreallocate, ZcashDeserialize, ZcashSerialize}, +}; + +use orchard::note::AssetBase; + +use super::serialize::ASSET_BASE_SIZE; + +// Sizes of the serialized values for types in bytes (used for TrustedPreallocate impls) +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; + +/// Represents an Orchard ZSA burn item. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BurnItem(AssetBase, Amount); + +impl ZcashSerialize for BurnItem { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + let BurnItem(asset_base, amount) = self; + + asset_base.zcash_serialize(&mut writer)?; + amount.zcash_serialize(&mut writer)?; + + Ok(()) + } +} + +impl ZcashDeserialize for BurnItem { + fn zcash_deserialize(mut reader: R) -> Result { + Ok(Self( + AssetBase::zcash_deserialize(&mut reader)?, + Amount::zcash_deserialize(&mut reader)?, + )) + } +} + +impl TrustedPreallocate for BurnItem { + fn max_allocation() -> u64 { + // FIXME: is this a correct calculation way? + // The longest Vec we receive from an honest peer must fit inside a valid block. + // Since encoding the length of the vec takes at least one byte, we use MAX_BLOCK_BYTES - 1 + (MAX_BLOCK_BYTES - 1) / BURN_ITEM_SIZE + } +} + +#[cfg(any(test, feature = "proptest-impl"))] +impl serde::Serialize for BurnItem { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + // FIXME: return custom error with a meaningful description? + (self.0.to_bytes(), &self.1).serialize(serializer) + } +} + +#[cfg(any(test, feature = "proptest-impl"))] +impl<'de> serde::Deserialize<'de> for BurnItem { + fn deserialize(deserializer: D) -> Result + where + 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)?; + // 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, + )) + } +} diff --git a/zebra-chain/src/orchard_zsa/issuance.rs b/zebra-chain/src/orchard_zsa/issuance.rs new file mode 100644 index 00000000000..9d31276072c --- /dev/null +++ b/zebra-chain/src/orchard_zsa/issuance.rs @@ -0,0 +1,260 @@ +//! Orchard ZSA issuance related functionality. + +use std::{fmt::Debug, io}; + +use crate::{ + block::MAX_BLOCK_BYTES, + serialization::{ + zcash_serialize_bytes, zcash_serialize_empty_list, ReadZcashExt, SerializationError, + TrustedPreallocate, ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize, + }, +}; + +use nonempty::NonEmpty; + +// FIXME: needed for "as_bool" only - consider to implement as_bool locally +use bitvec::macros::internal::funty::Fundamental; + +use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; + +use orchard::{ + issuance::{IssueAction, IssueBundle, Signed}, + keys::IssuanceValidatingKey, + note::{RandomSeed, Rho}, + primitives::redpallas::{SigType, Signature, SpendAuth}, + value::NoteValue, + Address, Note, +}; + +use super::serialize::ASSET_BASE_SIZE; + +/// Wrapper for `IssueBundle` used in the context of Transaction V6. This allows the implementation of +/// a Serde serializer for unit tests within this crate. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct IssueData(IssueBundle); + +// Sizes of the serialized values for types in bytes (used for TrustedPreallocate impls) +// FIXME: are those values correct (43, 32 etc.)? +//const ISSUANCE_VALIDATING_KEY_SIZE: u64 = 32; +const ADDRESS_SIZE: u64 = 43; +const NULLIFIER_SIZE: u64 = 32; +const NOTE_VALUE_SIZE: u64 = 4; +const RANDOM_SEED_SIZE: u64 = 32; +// FIXME: is this a correct way to calculate (simple sum of sizes of components)? +const NOTE_SIZE: u64 = + ADDRESS_SIZE + NOTE_VALUE_SIZE + ASSET_BASE_SIZE + NULLIFIER_SIZE + RANDOM_SEED_SIZE; + +// FIXME: duplicates ZcashSerialize for reddsa::Signature in transaction/serialize.rs +// (as Signature from oechard_zsa is formally a different type) +impl ZcashSerialize for Signature { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + writer.write_all(&<[u8; 64]>::from(self))?; + Ok(()) + } +} + +// FIXME: duplicates ZcashDeserialize for reddsa::Signature in transaction/serialize.rs +// (as Signature from oechard_zsa is formally a different type) +impl ZcashDeserialize for Signature { + fn zcash_deserialize(mut reader: R) -> Result { + Ok(reader.read_64_bytes()?.into()) + } +} + +impl ZcashDeserialize for Signed { + fn zcash_deserialize(mut reader: R) -> Result { + let signature = Signature::::zcash_deserialize(&mut reader)?; + Ok(Signed::from_data((&signature).into())) + } +} + +impl ZcashSerialize for IssuanceValidatingKey { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + writer.write_all(&self.to_bytes()) + } +} + +impl ZcashDeserialize for IssuanceValidatingKey { + fn zcash_deserialize(mut reader: R) -> Result { + IssuanceValidatingKey::from_bytes(&reader.read_32_bytes()?) + .ok_or_else(|| SerializationError::Parse("Invalid orchard_zsa IssuanceValidatingKey!")) + } +} + +impl ZcashSerialize for Address { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + writer.write_all(&self.to_raw_address_bytes()) + } +} + +impl ZcashDeserialize for Address { + fn zcash_deserialize(mut reader: R) -> Result { + let mut bytes = [0u8; ADDRESS_SIZE as usize]; + reader.read_exact(&mut bytes)?; + Option::from(Address::from_raw_address_bytes(&bytes)) + .ok_or_else(|| SerializationError::Parse("Invalid orchard_zsa Address!")) + } +} + +impl ZcashSerialize for Rho { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + writer.write_all(&self.to_bytes()) + } +} + +impl ZcashDeserialize for Rho { + fn zcash_deserialize(mut reader: R) -> Result { + Option::from(Rho::from_bytes(&reader.read_32_bytes()?)) + .ok_or_else(|| SerializationError::Parse("Invalid orchard_zsa Rho!")) + } +} + +impl ZcashSerialize for RandomSeed { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + writer.write_all(self.as_bytes()) + } +} + +// RandomSeed::zcash_deserialize can't be implemented and used as it requires Nullifier parameter. +// That's why we need to have this function. +fn zcash_deserialize_random_seed( + mut reader: R, + rho: &Rho, +) -> Result { + Option::from(RandomSeed::from_bytes(reader.read_32_bytes()?, rho)) + .ok_or_else(|| SerializationError::Parse("Invalid orchard_zsa RandomSeed!")) +} + +impl ZcashSerialize for NoteValue { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + // FIXME: use Amount serializer/deserializer? + writer.write_u64::(self.inner())?; + Ok(()) + } +} + +impl ZcashDeserialize for NoteValue { + fn zcash_deserialize(mut reader: R) -> Result { + Ok(NoteValue::from_raw(reader.read_u64::()?)) + } +} + +impl ZcashSerialize for Note { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + self.recipient().zcash_serialize(&mut writer)?; + self.value().zcash_serialize(&mut writer)?; + self.asset().zcash_serialize(&mut writer)?; + self.rho().zcash_serialize(&mut writer)?; + self.rseed().zcash_serialize(&mut writer)?; + + Ok(()) + } +} + +impl ZcashDeserialize for Note { + fn zcash_deserialize(mut reader: R) -> Result { + let recipient = (&mut reader).zcash_deserialize_into()?; + let value = (&mut reader).zcash_deserialize_into()?; + let asset = (&mut reader).zcash_deserialize_into()?; + let rho = (&mut reader).zcash_deserialize_into()?; + let rseed = zcash_deserialize_random_seed(&mut reader, &rho)?; + + Option::from(Note::from_parts(recipient, value, asset, rho, rseed)) + .ok_or_else(|| SerializationError::Parse("Invalid orchard_zsa Note components!")) + } +} + +impl TrustedPreallocate for Note { + fn max_allocation() -> u64 { + // FIXME: is this a correct calculation way? + // The longest Vec we receive from an honest peer must fit inside a valid block. + // Since encoding the length of the vec takes at least one byte, we use MAX_BLOCK_BYTES - 1 + (MAX_BLOCK_BYTES - 1) / NOTE_SIZE + } +} + +impl ZcashSerialize for IssueAction { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + writer.write_u8(self.is_finalized().as_u8())?; + self.notes().to_vec().zcash_serialize(&mut writer)?; + zcash_serialize_bytes(&self.asset_desc().to_vec(), &mut writer)?; + Ok(()) + } +} + +impl ZcashDeserialize for IssueAction { + fn zcash_deserialize(mut reader: R) -> Result { + let finalize = reader.read_u8()?.as_bool(); + let notes = (&mut reader).zcash_deserialize_into()?; + let asset_descr = (&mut reader).zcash_deserialize_into()?; + Ok(IssueAction::from_parts(asset_descr, notes, finalize)) + } +} + +impl TrustedPreallocate for IssueAction { + fn max_allocation() -> u64 { + // FIXME: impl correct calculation + 10 + } +} + +impl ZcashSerialize for IssueBundle { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + // FIXME: try to avoid transforming to Vec (consider implementation of ZcashSerialize for IntoIter generic, + // or use AtLeastOne). + // This is how does it work in librustzcash: + // Vector::write_nonempty(&mut writer, bundle.actions(), |w, action| write_action(action, w))?; + let actions: Vec<_> = self.actions().clone().into(); + + actions.zcash_serialize(&mut writer)?; + self.ik().zcash_serialize(&mut writer)?; + writer.write_all(&<[u8; 64]>::from(self.authorization().signature()))?; + Ok(()) + } +} + +impl ZcashSerialize for Option { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + match self { + None => { + // Denoted as `&Option` in the spec (ZIP 230). + zcash_serialize_empty_list(writer)?; + } + Some(issue_data) => { + issue_data.0.zcash_serialize(&mut writer)?; + } + } + Ok(()) + } +} + +// FIXME: We can't split IssueData out of Option deserialization, +// because the counts are read along with the arrays. +impl ZcashDeserialize for Option { + fn zcash_deserialize(mut reader: R) -> Result { + let actions: Vec<_> = (&mut reader).zcash_deserialize_into()?; + + if actions.is_empty() { + Ok(None) + } else { + let ik = (&mut reader).zcash_deserialize_into()?; + let authorization = (&mut reader).zcash_deserialize_into()?; + + Ok(Some(IssueData(IssueBundle::from_parts( + ik, + NonEmpty::from_vec(actions).ok_or_else(|| { + SerializationError::Parse("Invalid orchard_zsa IssueData - no actions!") + })?, + authorization, + )))) + } + } +} + +#[cfg(any(test, feature = "proptest-impl"))] +impl serde::Serialize for IssueData { + fn serialize(&self, serializer: S) -> Result { + // TODO: FIXME: implement Serde serialization here + "(IssueData)".serialize(serializer) + } +} diff --git a/zebra-chain/src/orchard_zsa/serialize.rs b/zebra-chain/src/orchard_zsa/serialize.rs new file mode 100644 index 00000000000..6afc51e1887 --- /dev/null +++ b/zebra-chain/src/orchard_zsa/serialize.rs @@ -0,0 +1,23 @@ +//! Serialization implementation for selected types from the 'orchard_zsa' crate used in this module. + +use std::io; + +use crate::serialization::{ReadZcashExt, SerializationError, ZcashDeserialize, ZcashSerialize}; + +use orchard::note::AssetBase; + +// The size of the serialized AssetBase in bytes (used for TrustedPreallocate impls) +pub(crate) const ASSET_BASE_SIZE: u64 = 32; + +impl ZcashSerialize for AssetBase { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + writer.write_all(&self.to_bytes()) + } +} + +impl ZcashDeserialize for AssetBase { + fn zcash_deserialize(mut reader: R) -> Result { + Option::from(AssetBase::from_bytes(&reader.read_32_bytes()?)) + .ok_or_else(|| SerializationError::Parse("Invalid orchard_zsa AssetBase!")) + } +} diff --git a/zebra-chain/src/parameters/network_upgrade.rs b/zebra-chain/src/parameters/network_upgrade.rs index 3d35f0526d2..a1cc0258687 100644 --- a/zebra-chain/src/parameters/network_upgrade.rs +++ b/zebra-chain/src/parameters/network_upgrade.rs @@ -530,11 +530,11 @@ impl From for NetworkUpgrade { zcash_protocol::consensus::NetworkUpgrade::Heartwood => Self::Heartwood, zcash_protocol::consensus::NetworkUpgrade::Canopy => Self::Canopy, zcash_protocol::consensus::NetworkUpgrade::Nu5 => Self::Nu5, - // FIXME: remove cfg - #[cfg(zcash_unstable = "nu6")] + // FIXME: remove this cfg + //#[cfg(zcash_unstable = "nu6")] zcash_protocol::consensus::NetworkUpgrade::Nu6 => Self::Nu6, - // FIXME: remove cfg and process Nu7 properly (uses Self::Nu6 for now) - #[cfg(zcash_unstable = "nu6")] + // FIXME: remove this cfg and process Nu7 properly (uses Self::Nu6 for now) + //#[cfg(zcash_unstable = "nu6")] zcash_protocol::consensus::NetworkUpgrade::Nu7 => Self::Nu6, } } diff --git a/zebra-chain/src/parameters/transaction.rs b/zebra-chain/src/parameters/transaction.rs index 77b88abca44..d7391bdf184 100644 --- a/zebra-chain/src/parameters/transaction.rs +++ b/zebra-chain/src/parameters/transaction.rs @@ -17,4 +17,5 @@ pub const TX_V5_VERSION_GROUP_ID: u32 = 0x26A7_270A; /// Orchard transactions must use transaction version 5 and this version /// group ID. // FIXME: use a proper value! +#[cfg(feature = "tx-v6")] pub const TX_V6_VERSION_GROUP_ID: u32 = 0x26A7_270B; diff --git a/zebra-chain/src/primitives/zcash_primitives.rs b/zebra-chain/src/primitives/zcash_primitives.rs index 4cf445854d2..0d10f8546af 100644 --- a/zebra-chain/src/primitives/zcash_primitives.rs +++ b/zebra-chain/src/primitives/zcash_primitives.rs @@ -10,7 +10,7 @@ use crate::{ amount::{Amount, NonNegative}, parameters::{ConsensusBranchId, Network}, serialization::ZcashSerialize, - transaction::{AuthDigest, HashType, SigHash, Transaction}, + transaction::{tx_v5_and_v6, AuthDigest, HashType, SigHash, Transaction}, transparent::{self, Script}, }; @@ -138,7 +138,8 @@ impl zp_tx::components::orchard::MapAuth for IdentityMap { @@ -158,11 +159,13 @@ impl<'a> zp_tx::Authorization for PrecomputedAuth<'a> { type OrchardAuth = orchard::bundle::Authorized; // FIXME: is this correct? - #[cfg(zcash_unstable = "nu6")] + // FIXME: remove this cfg + //#[cfg(zcash_unstable = "nu6")] type OrchardZsaAuth = orchard::bundle::Authorized; // FIXME: is this correct? - #[cfg(zcash_unstable = "nu6")] + // FIXME: remove this cfg + //#[cfg(zcash_unstable = "nu6")] type IssueAuth = orchard::issuance::Signed; } @@ -180,10 +183,7 @@ impl TryFrom<&Transaction> for zp_tx::Transaction { #[allow(clippy::unwrap_in_result)] fn try_from(trans: &Transaction) -> Result { let network_upgrade = match trans { - Transaction::V5 { - network_upgrade, .. - } - | Transaction::V6 { + tx_v5_and_v6! { network_upgrade, .. } => network_upgrade, Transaction::V1 { .. } diff --git a/zebra-chain/src/transaction.rs b/zebra-chain/src/transaction.rs index ada329140f1..e60f13b57a1 100644 --- a/zebra-chain/src/transaction.rs +++ b/zebra-chain/src/transaction.rs @@ -53,6 +53,74 @@ use crate::{ value_balance::{ValueBalance, ValueBalanceError}, }; +// FIXME: doc this +// Move down +macro_rules! orchard_shielded_data_iter { + ($self:expr, $mapper:expr) => { + match $self { + // No Orchard shielded data + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { .. } => Box::new(std::iter::empty()), + + Transaction::V5 { + orchard_shielded_data, + .. + } => Box::new(orchard_shielded_data.into_iter().flat_map($mapper)), + + #[cfg(feature = "tx-v6")] + Transaction::V6 { + orchard_shielded_data, + .. + } => Box::new(orchard_shielded_data.into_iter().flat_map($mapper)), + } + }; +} + +// FIXME: doc this +// Move down +macro_rules! orchard_shielded_data_field { + ($self:expr, $field:ident) => { + match $self { + // No Orchard shielded data + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { .. } => None, + + Transaction::V5 { + orchard_shielded_data, + .. + } => orchard_shielded_data.as_ref().map(|data| data.$field), + + #[cfg(feature = "tx-v6")] + Transaction::V6 { + orchard_shielded_data, + .. + } => orchard_shielded_data.as_ref().map(|data| data.$field), + } + }; +} + +// FIXME: +// Define the macro for including the V6 pattern +#[cfg(feature = "tx-v6")] +macro_rules! tx_v5_and_v6 { + { $($fields:tt)* } => { + Transaction::V5 { $($fields)* } | Transaction::V6 { $($fields)* } + }; +} + +#[cfg(not(feature = "tx-v6"))] +macro_rules! tx_v5_and_v6 { + { $($fields:tt)* } => { + Transaction::V5 { $($fields)* } + }; +} + +pub(crate) use tx_v5_and_v6; + /// A Zcash transaction. /// /// A transaction is an encoded data structure that facilitates the transfer of @@ -140,11 +208,12 @@ pub enum Transaction { /// The sapling shielded data for this transaction, if any. sapling_shielded_data: Option>, /// The orchard data for this transaction, if any. - orchard_shielded_data: Option, + orchard_shielded_data: Option>, }, // FIXME: implement V6 properly (now it's just a coipy of V5) /// A `version = 6` transaction , which supports Orchard ZSA, Orchard Vanille, Sapling and /// transparent, but not Sprout. + #[cfg(feature = "tx-v6")] V6 { /// The Network Upgrade for this transaction. /// @@ -161,8 +230,12 @@ pub enum Transaction { outputs: Vec, /// The sapling shielded data for this transaction, if any. sapling_shielded_data: Option>, - /// The orchard data for this transaction, if any. - orchard_shielded_data: Option, + /// The ZSA orchard shielded data for this transaction, if any. + #[cfg(feature = "tx-v6")] + orchard_shielded_data: Option>, + /// The ZSA issuance data for this transaction, if any. + #[cfg(feature = "tx-v6")] + orchard_zsa_issue_data: Option, }, } @@ -269,7 +342,7 @@ impl Transaction { | Transaction::V2 { .. } | Transaction::V3 { .. } | Transaction::V4 { .. } => None, - Transaction::V5 { .. } | Transaction::V6 { .. } => Some(AuthDigest::from(self)), + tx_v5_and_v6! { .. } => Some(AuthDigest::from(self)), } } @@ -342,10 +415,7 @@ impl Transaction { pub fn is_overwintered(&self) -> bool { match self { Transaction::V1 { .. } | Transaction::V2 { .. } => false, - Transaction::V3 { .. } - | Transaction::V4 { .. } - | Transaction::V5 { .. } - | Transaction::V6 { .. } => true, + Transaction::V3 { .. } | Transaction::V4 { .. } | tx_v5_and_v6! { .. } => true, } } @@ -357,6 +427,7 @@ impl Transaction { Transaction::V3 { .. } => 3, Transaction::V4 { .. } => 4, Transaction::V5 { .. } => 5, + #[cfg(feature = "tx-v6")] Transaction::V6 { .. } => 6, } } @@ -368,8 +439,7 @@ impl Transaction { | Transaction::V2 { lock_time, .. } | Transaction::V3 { lock_time, .. } | Transaction::V4 { lock_time, .. } - | Transaction::V5 { lock_time, .. } - | Transaction::V6 { lock_time, .. } => *lock_time, + | tx_v5_and_v6! { lock_time, .. } => *lock_time, }; // `zcashd` checks that the block height is greater than the lock height. @@ -416,8 +486,7 @@ impl Transaction { | Transaction::V2 { lock_time, .. } | Transaction::V3 { lock_time, .. } | Transaction::V4 { lock_time, .. } - | Transaction::V5 { lock_time, .. } - | Transaction::V6 { lock_time, .. } => *lock_time, + | tx_v5_and_v6! { lock_time, .. } => *lock_time, }; let mut lock_time_bytes = Vec::new(); lock_time @@ -447,8 +516,7 @@ impl Transaction { Transaction::V1 { .. } | Transaction::V2 { .. } => None, Transaction::V3 { expiry_height, .. } | Transaction::V4 { expiry_height, .. } - | Transaction::V5 { expiry_height, .. } - | Transaction::V6 { expiry_height, .. } => match expiry_height { + | tx_v5_and_v6! { expiry_height, .. } => match expiry_height { // Consensus rule: // > No limit: To set no limit on transactions (so that they do not expire), nExpiryHeight should be set to 0. // https://zips.z.cash/zip-0203#specification @@ -477,11 +545,7 @@ impl Transaction { ref mut expiry_height, .. } - | Transaction::V5 { - ref mut expiry_height, - .. - } - | Transaction::V6 { + | tx_v5_and_v6! { ref mut expiry_height, .. } => expiry_height, @@ -498,10 +562,7 @@ impl Transaction { | Transaction::V2 { .. } | Transaction::V3 { .. } | Transaction::V4 { .. } => None, - Transaction::V5 { - network_upgrade, .. - } - | Transaction::V6 { + tx_v5_and_v6! { network_upgrade, .. } => Some(*network_upgrade), } @@ -516,8 +577,7 @@ impl Transaction { Transaction::V2 { ref inputs, .. } => inputs, Transaction::V3 { ref inputs, .. } => inputs, Transaction::V4 { ref inputs, .. } => inputs, - Transaction::V5 { ref inputs, .. } => inputs, - Transaction::V6 { ref inputs, .. } => inputs, + tx_v5_and_v6! { ref inputs, .. } => inputs, } } @@ -529,8 +589,7 @@ impl Transaction { Transaction::V2 { ref mut inputs, .. } => inputs, Transaction::V3 { ref mut inputs, .. } => inputs, Transaction::V4 { ref mut inputs, .. } => inputs, - Transaction::V5 { ref mut inputs, .. } => inputs, - Transaction::V6 { ref mut inputs, .. } => inputs, + tx_v5_and_v6! { ref mut inputs, .. } => inputs, } } @@ -548,8 +607,7 @@ impl Transaction { Transaction::V2 { ref outputs, .. } => outputs, Transaction::V3 { ref outputs, .. } => outputs, Transaction::V4 { ref outputs, .. } => outputs, - Transaction::V5 { ref outputs, .. } => outputs, - Transaction::V6 { ref outputs, .. } => outputs, + tx_v5_and_v6! { ref outputs, .. } => outputs, } } @@ -569,10 +627,7 @@ impl Transaction { Transaction::V4 { ref mut outputs, .. } => outputs, - Transaction::V5 { - ref mut outputs, .. - } => outputs, - Transaction::V6 { + tx_v5_and_v6! { ref mut outputs, .. } => outputs, } @@ -622,8 +677,7 @@ impl Transaction { joinsplit_data: None, .. } - | Transaction::V5 { .. } - | Transaction::V6 { .. } => Box::new(std::iter::empty()), + | tx_v5_and_v6! { .. } => Box::new(std::iter::empty()), } } @@ -658,8 +712,7 @@ impl Transaction { joinsplit_data: None, .. } - | Transaction::V5 { .. } - | Transaction::V6 { .. } => 0, + | tx_v5_and_v6! { .. } => 0, } } @@ -698,8 +751,7 @@ impl Transaction { joinsplit_data: None, .. } - | Transaction::V5 { .. } - | Transaction::V6 { .. } => Box::new(std::iter::empty()), + | tx_v5_and_v6! { .. } => Box::new(std::iter::empty()), } } @@ -735,8 +787,7 @@ impl Transaction { joinsplit_data: None, .. } - | Transaction::V5 { .. } - | Transaction::V6 { .. } => None, + | tx_v5_and_v6! { .. } => None, } } @@ -744,7 +795,7 @@ impl Transaction { pub fn has_sprout_joinsplit_data(&self) -> bool { match self { // No JoinSplits - Transaction::V1 { .. } | Transaction::V5 { .. } | Transaction::V6 { .. } => false, + Transaction::V1 { .. } | tx_v5_and_v6! { .. } => false, // JoinSplits-on-BCTV14 Transaction::V2 { joinsplit_data, .. } | Transaction::V3 { joinsplit_data, .. } => { @@ -791,8 +842,7 @@ impl Transaction { .. } | Transaction::V1 { .. } - | Transaction::V5 { .. } - | Transaction::V6 { .. } => Box::new(std::iter::empty()), + | tx_v5_and_v6! { .. } => Box::new(std::iter::empty()), } } @@ -809,12 +859,7 @@ impl Transaction { .. } => Box::new(sapling_shielded_data.anchors()), - Transaction::V5 { - sapling_shielded_data: Some(sapling_shielded_data), - .. - } => Box::new(sapling_shielded_data.anchors()), - - Transaction::V6 { + tx_v5_and_v6! { sapling_shielded_data: Some(sapling_shielded_data), .. } => Box::new(sapling_shielded_data.anchors()), @@ -827,11 +872,7 @@ impl Transaction { sapling_shielded_data: None, .. } - | Transaction::V5 { - sapling_shielded_data: None, - .. - } - | Transaction::V6 { + | tx_v5_and_v6! { sapling_shielded_data: None, .. } => Box::new(std::iter::empty()), @@ -856,11 +897,7 @@ impl Transaction { sapling_shielded_data: Some(sapling_shielded_data), .. } => Box::new(sapling_shielded_data.spends_per_anchor()), - Transaction::V5 { - sapling_shielded_data: Some(sapling_shielded_data), - .. - } => Box::new(sapling_shielded_data.spends_per_anchor()), - Transaction::V6 { + tx_v5_and_v6! { sapling_shielded_data: Some(sapling_shielded_data), .. } => Box::new(sapling_shielded_data.spends_per_anchor()), @@ -873,11 +910,7 @@ impl Transaction { sapling_shielded_data: None, .. } - | Transaction::V5 { - sapling_shielded_data: None, - .. - } - | Transaction::V6 { + | tx_v5_and_v6! { sapling_shielded_data: None, .. } => Box::new(std::iter::empty()), @@ -892,11 +925,7 @@ impl Transaction { sapling_shielded_data: Some(sapling_shielded_data), .. } => Box::new(sapling_shielded_data.outputs()), - Transaction::V5 { - sapling_shielded_data: Some(sapling_shielded_data), - .. - } => Box::new(sapling_shielded_data.outputs()), - Transaction::V6 { + tx_v5_and_v6! { sapling_shielded_data: Some(sapling_shielded_data), .. } => Box::new(sapling_shielded_data.outputs()), @@ -909,11 +938,7 @@ impl Transaction { sapling_shielded_data: None, .. } - | Transaction::V5 { - sapling_shielded_data: None, - .. - } - | Transaction::V6 { + | tx_v5_and_v6! { sapling_shielded_data: None, .. } => Box::new(std::iter::empty()), @@ -930,11 +955,7 @@ impl Transaction { sapling_shielded_data: Some(sapling_shielded_data), .. } => Box::new(sapling_shielded_data.nullifiers()), - Transaction::V5 { - sapling_shielded_data: Some(sapling_shielded_data), - .. - } => Box::new(sapling_shielded_data.nullifiers()), - Transaction::V6 { + tx_v5_and_v6! { sapling_shielded_data: Some(sapling_shielded_data), .. } => Box::new(sapling_shielded_data.nullifiers()), @@ -947,11 +968,7 @@ impl Transaction { sapling_shielded_data: None, .. } - | Transaction::V5 { - sapling_shielded_data: None, - .. - } - | Transaction::V6 { + | tx_v5_and_v6! { sapling_shielded_data: None, .. } => Box::new(std::iter::empty()), @@ -968,11 +985,7 @@ impl Transaction { sapling_shielded_data: Some(sapling_shielded_data), .. } => Box::new(sapling_shielded_data.note_commitments()), - Transaction::V5 { - sapling_shielded_data: Some(sapling_shielded_data), - .. - } => Box::new(sapling_shielded_data.note_commitments()), - Transaction::V6 { + tx_v5_and_v6! { sapling_shielded_data: Some(sapling_shielded_data), .. } => Box::new(sapling_shielded_data.note_commitments()), @@ -985,11 +998,7 @@ impl Transaction { sapling_shielded_data: None, .. } - | Transaction::V5 { - sapling_shielded_data: None, - .. - } - | Transaction::V6 { + | tx_v5_and_v6! { sapling_shielded_data: None, .. } => Box::new(std::iter::empty()), @@ -1004,11 +1013,7 @@ impl Transaction { sapling_shielded_data, .. } => sapling_shielded_data.is_some(), - Transaction::V5 { - sapling_shielded_data, - .. - } => sapling_shielded_data.is_some(), - Transaction::V6 { + tx_v5_and_v6! { sapling_shielded_data, .. } => sapling_shielded_data.is_some(), @@ -1017,96 +1022,40 @@ impl Transaction { // orchard - /// Access the [`orchard::ShieldedData`] in this transaction, - /// regardless of version. - pub fn orchard_shielded_data(&self) -> Option<&orchard::ShieldedData> { - match self { - // Maybe Orchard shielded data - Transaction::V5 { - orchard_shielded_data, - .. - } => orchard_shielded_data.as_ref(), - - // FIXME: Support V6/OrchardZSA propetly. - Transaction::V6 { - orchard_shielded_data, - .. - } => orchard_shielded_data.as_ref(), - - // No Orchard shielded data - Transaction::V1 { .. } - | Transaction::V2 { .. } - | Transaction::V3 { .. } - | Transaction::V4 { .. } => None, - } - } - - /// Modify the [`orchard::ShieldedData`] in this transaction, - /// regardless of version. - #[cfg(any(test, feature = "proptest-impl"))] - pub fn orchard_shielded_data_mut(&mut self) -> Option<&mut orchard::ShieldedData> { - match self { - Transaction::V5 { - orchard_shielded_data: Some(orchard_shielded_data), - .. - } => Some(orchard_shielded_data), - - // FIXME: Support V6/OrchardZSA propetly. - Transaction::V6 { - orchard_shielded_data: Some(orchard_shielded_data), - .. - } => Some(orchard_shielded_data), - - Transaction::V1 { .. } - | Transaction::V2 { .. } - | Transaction::V3 { .. } - | Transaction::V4 { .. } - | Transaction::V5 { - orchard_shielded_data: None, - .. - } - | Transaction::V6 { - orchard_shielded_data: None, - .. - } => None, - } - } - /// Iterate over the [`orchard::Action`]s in this transaction, if there are any, /// regardless of version. - pub fn orchard_actions(&self) -> impl Iterator { - self.orchard_shielded_data() - .into_iter() - .flat_map(orchard::ShieldedData::actions) + pub fn orchard_actions(&self) -> Box + '_> { + orchard_shielded_data_iter!(self, orchard::ShieldedData::action_commons) } /// Access the [`orchard::Nullifier`]s in this transaction, if there are any, /// regardless of version. - pub fn orchard_nullifiers(&self) -> impl Iterator { - self.orchard_shielded_data() - .into_iter() - .flat_map(orchard::ShieldedData::nullifiers) + pub fn orchard_nullifiers(&self) -> Box + '_> { + orchard_shielded_data_iter!(self, orchard::ShieldedData::nullifiers) } /// Access the note commitments in this transaction, if there are any, /// regardless of version. - pub fn orchard_note_commitments(&self) -> impl Iterator { - self.orchard_shielded_data() - .into_iter() - .flat_map(orchard::ShieldedData::note_commitments) + pub fn orchard_note_commitments(&self) -> Box + '_> { + orchard_shielded_data_iter!(self, orchard::ShieldedData::note_commitments) } /// Access the [`orchard::Flags`] in this transaction, if there is any, /// regardless of version. pub fn orchard_flags(&self) -> Option { - self.orchard_shielded_data() - .map(|orchard_shielded_data| orchard_shielded_data.flags) + // FIXME: remove this line with_shielded_data!(self, |data: impl orchard::ShieldedDataCommon| data.flags) + orchard_shielded_data_field!(self, flags) + } + + /// FIXME: add doc + pub fn orchard_shared_anchor(&self) -> Option { + orchard_shielded_data_field!(self, shared_anchor) } /// Return if the transaction has any Orchard shielded data, /// regardless of version. pub fn has_orchard_shielded_data(&self) -> bool { - self.orchard_shielded_data().is_some() + self.orchard_flags().is_some() } // value balances @@ -1194,8 +1143,7 @@ impl Transaction { joinsplit_data: None, .. } - | Transaction::V5 { .. } - | Transaction::V6 { .. } => Box::new(std::iter::empty()), + | tx_v5_and_v6! { .. } => Box::new(std::iter::empty()), } } @@ -1244,8 +1192,7 @@ impl Transaction { joinsplit_data: None, .. } - | Transaction::V5 { .. } - | Transaction::V6 { .. } => Box::new(std::iter::empty()), + | tx_v5_and_v6! { .. } => Box::new(std::iter::empty()), } } @@ -1292,8 +1239,7 @@ impl Transaction { joinsplit_data: None, .. } - | Transaction::V5 { .. } - | Transaction::V6 { .. } => Box::new(std::iter::empty()), + | tx_v5_and_v6! { .. } => Box::new(std::iter::empty()), } } @@ -1342,8 +1288,7 @@ impl Transaction { joinsplit_data: None, .. } - | Transaction::V5 { .. } - | Transaction::V6 { .. } => Box::new(std::iter::empty()), + | tx_v5_and_v6! { .. } => Box::new(std::iter::empty()), } } @@ -1384,8 +1329,7 @@ impl Transaction { joinsplit_data: None, .. } - | Transaction::V5 { .. } - | Transaction::V6 { .. } => Box::new(iter::empty()), + | tx_v5_and_v6! { .. } => Box::new(iter::empty()), }; joinsplit_value_balances.map(ValueBalance::from_sprout_amount) @@ -1423,11 +1367,7 @@ impl Transaction { sapling_shielded_data: Some(sapling_shielded_data), .. } => sapling_shielded_data.value_balance, - Transaction::V5 { - sapling_shielded_data: Some(sapling_shielded_data), - .. - } => sapling_shielded_data.value_balance, - Transaction::V6 { + tx_v5_and_v6! { sapling_shielded_data: Some(sapling_shielded_data), .. } => sapling_shielded_data.value_balance, @@ -1439,11 +1379,7 @@ impl Transaction { sapling_shielded_data: None, .. } - | Transaction::V5 { - sapling_shielded_data: None, - .. - } - | Transaction::V6 { + | tx_v5_and_v6! { sapling_shielded_data: None, .. } => Amount::zero(), @@ -1463,11 +1399,7 @@ impl Transaction { sapling_shielded_data: Some(sapling_shielded_data), .. } => Some(&mut sapling_shielded_data.value_balance), - Transaction::V5 { - sapling_shielded_data: Some(sapling_shielded_data), - .. - } => Some(&mut sapling_shielded_data.value_balance), - Transaction::V6 { + tx_v5_and_v6! { sapling_shielded_data: Some(sapling_shielded_data), .. } => Some(&mut sapling_shielded_data.value_balance), @@ -1478,11 +1410,7 @@ impl Transaction { sapling_shielded_data: None, .. } - | Transaction::V5 { - sapling_shielded_data: None, - .. - } - | Transaction::V6 { + | tx_v5_and_v6! { sapling_shielded_data: None, .. } => None, @@ -1501,10 +1429,8 @@ impl Transaction { /// /// pub fn orchard_value_balance(&self) -> ValueBalance { - let orchard_value_balance = self - .orchard_shielded_data() - .map(|shielded_data| shielded_data.value_balance) - .unwrap_or_else(Amount::zero); + let orchard_value_balance = + orchard_shielded_data_field!(self, value_balance).unwrap_or_else(Amount::zero); ValueBalance::from_orchard_amount(orchard_value_balance) } @@ -1515,8 +1441,33 @@ impl Transaction { /// See `orchard_value_balance` for details. #[cfg(any(test, feature = "proptest-impl"))] pub fn orchard_value_balance_mut(&mut self) -> Option<&mut Amount> { - self.orchard_shielded_data_mut() - .map(|shielded_data| &mut shielded_data.value_balance) + match self { + Transaction::V5 { + orchard_shielded_data: Some(orchard_shielded_data), + .. + } => Some(&mut orchard_shielded_data.value_balance), + + #[cfg(feature = "tx-v6")] + Transaction::V6 { + orchard_shielded_data: Some(orchard_shielded_data), + .. + } => Some(&mut orchard_shielded_data.value_balance), + + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { .. } + | Transaction::V5 { + orchard_shielded_data: None, + .. + } => None, + + #[cfg(feature = "tx-v6")] + Transaction::V6 { + orchard_shielded_data: None, + .. + } => None, + } } /// Returns the value balances for this transaction using the provided transparent outputs. diff --git a/zebra-chain/src/transaction/arbitrary.rs b/zebra-chain/src/transaction/arbitrary.rs index 73aac10a23d..ffd169f651f 100644 --- a/zebra-chain/src/transaction/arbitrary.rs +++ b/zebra-chain/src/transaction/arbitrary.rs @@ -141,7 +141,7 @@ impl Transaction { transparent::Input::vec_strategy(&ledger_state, MAX_ARBITRARY_ITEMS), vec(any::(), 0..MAX_ARBITRARY_ITEMS), option::of(any::>()), - option::of(any::()), + option::of(any::>()), ) .prop_map( move |( @@ -697,7 +697,7 @@ impl Arbitrary for sapling::TransferData { type Strategy = BoxedStrategy; } -impl Arbitrary for orchard::ShieldedData { +impl Arbitrary for orchard::ShieldedData { type Parameters = (); fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { @@ -707,7 +707,7 @@ impl Arbitrary for orchard::ShieldedData { any::(), any::(), vec( - any::(), + any::>(), 1..MAX_ARBITRARY_ITEMS, ), any::(), @@ -722,6 +722,8 @@ impl Arbitrary for orchard::ShieldedData { .try_into() .expect("arbitrary vector size range produces at least one action"), binding_sig: binding_sig.0, + #[cfg(feature = "tx-v6")] + burn: Default::default(), }, ) .boxed() @@ -918,12 +920,13 @@ pub fn transaction_to_fake_v5( orchard_shielded_data: None, }, v5 @ V5 { .. } => v5.clone(), + #[cfg(feature = "tx-v6")] V6 { inputs, outputs, lock_time, sapling_shielded_data, - orchard_shielded_data, + orchard_shielded_data: _, .. } => V5 { network_upgrade: block_nu, @@ -932,7 +935,10 @@ pub fn transaction_to_fake_v5( lock_time: *lock_time, expiry_height: height, sapling_shielded_data: sapling_shielded_data.clone(), - orchard_shielded_data: orchard_shielded_data.clone(), + // FIXME: is it possible to convert V6 shielded data to V5? + // FIXME: add another function for V6, like transaction_to_fake_v6? + //orchard_shielded_data: orchard_shielded_data.clone(), + orchard_shielded_data: None, }, } } @@ -1036,6 +1042,7 @@ pub fn transactions_from_blocks<'a>( }) } +// FIXME: make it a generic to support V6? /// Modify a V5 transaction to insert fake Orchard shielded data. /// /// Creates a fake instance of [`orchard::ShieldedData`] with one fake action. Note that both the @@ -1050,7 +1057,7 @@ pub fn transactions_from_blocks<'a>( /// Panics if the transaction to be modified is not V5. pub fn insert_fake_orchard_shielded_data( transaction: &mut Transaction, -) -> &mut orchard::ShieldedData { +) -> &mut orchard::ShieldedData { // Create a dummy action let mut runner = TestRunner::default(); let dummy_action = orchard::Action::arbitrary() @@ -1065,13 +1072,15 @@ pub fn insert_fake_orchard_shielded_data( }; // Place the dummy action inside the Orchard shielded data - let dummy_shielded_data = orchard::ShieldedData { + let dummy_shielded_data = orchard::ShieldedData:: { flags: orchard::Flags::empty(), value_balance: Amount::try_from(0).expect("invalid transaction amount"), shared_anchor: orchard::tree::Root::default(), proof: Halo2Proof(vec![]), actions: at_least_one![dummy_authorized_action], binding_sig: Signature::from([0u8; 64]), + #[cfg(feature = "tx-v6")] + burn: Default::default(), }; // Replace the shielded data in the transaction diff --git a/zebra-chain/src/transaction/serialize.rs b/zebra-chain/src/transaction/serialize.rs index 61b3cb2d579..42743881bd5 100644 --- a/zebra-chain/src/transaction/serialize.rs +++ b/zebra-chain/src/transaction/serialize.rs @@ -11,10 +11,8 @@ use reddsa::{orchard::Binding, orchard::SpendAuth, Signature}; use crate::{ amount, block::MAX_BLOCK_BYTES, - parameters::{ - OVERWINTER_VERSION_GROUP_ID, SAPLING_VERSION_GROUP_ID, TX_V5_VERSION_GROUP_ID, - TX_V6_VERSION_GROUP_ID, - }, + orchard::OrchardFlavorExt, + parameters::{OVERWINTER_VERSION_GROUP_ID, SAPLING_VERSION_GROUP_ID, TX_V5_VERSION_GROUP_ID}, primitives::{Halo2Proof, ZkSnarkProof}, serialization::{ zcash_deserialize_external_count, zcash_serialize_empty_list, @@ -23,6 +21,9 @@ use crate::{ }, }; +#[cfg(feature = "tx-v6")] +use crate::parameters::TX_V6_VERSION_GROUP_ID; + use super::*; use crate::sapling; @@ -326,7 +327,7 @@ impl ZcashDeserialize for Option> { } } -impl ZcashSerialize for Option { +impl ZcashSerialize for Option> { fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { match self { None => { @@ -342,14 +343,15 @@ impl ZcashSerialize for Option { orchard_shielded_data.zcash_serialize(&mut writer)?; } } + Ok(()) } } -impl ZcashSerialize for orchard::ShieldedData { +impl ZcashSerialize for orchard::ShieldedData { fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { // Split the AuthorizedAction - let (actions, sigs): (Vec, Vec>) = self + let (actions, sigs): (Vec>, Vec>) = self .actions .iter() .cloned() @@ -377,16 +379,20 @@ impl ZcashSerialize for orchard::ShieldedData { // Denoted as `bindingSigOrchard` in the spec. self.binding_sig.zcash_serialize(&mut writer)?; + #[cfg(feature = "tx-v6")] + // Denoted as `vAssetBurn` in the spec (ZIP 230). + self.burn.zcash_serialize(&mut writer)?; + Ok(()) } } // we can't split ShieldedData out of Option deserialization, // because the counts are read along with the arrays. -impl ZcashDeserialize for Option { +impl ZcashDeserialize for Option> { fn zcash_deserialize(mut reader: R) -> Result { // Denoted as `nActionsOrchard` and `vActionsOrchard` in the spec. - let actions: Vec = (&mut reader).zcash_deserialize_into()?; + let actions: Vec> = (&mut reader).zcash_deserialize_into()?; // "The fields flagsOrchard, valueBalanceOrchard, anchorOrchard, sizeProofsOrchard, // proofsOrchard , and bindingSigOrchard are present if and only if nActionsOrchard > 0." @@ -432,7 +438,7 @@ impl ZcashDeserialize for Option { let binding_sig: Signature = (&mut reader).zcash_deserialize_into()?; // Create the AuthorizedAction from deserialized parts - let authorized_actions: Vec = actions + let authorized_actions: Vec> = actions .into_iter() .zip(sigs) .map(|(action, spend_auth_sig)| { @@ -440,11 +446,17 @@ impl ZcashDeserialize for Option { }) .collect(); - let actions: AtLeastOne = authorized_actions.try_into()?; + let actions: AtLeastOne> = authorized_actions.try_into()?; + + // TODO: FIXME: add a proper comment + #[cfg(feature = "tx-v6")] + let burn = (&mut reader).zcash_deserialize_into()?; - Ok(Some(orchard::ShieldedData { + Ok(Some(orchard::ShieldedData:: { flags, value_balance, + #[cfg(feature = "tx-v6")] + burn, shared_anchor, proof, actions, @@ -676,7 +688,7 @@ impl ZcashSerialize for Transaction { orchard_shielded_data.zcash_serialize(&mut writer)?; } - // FIXME: implement a proper serialization for V6 + #[cfg(feature = "tx-v6")] Transaction::V6 { network_upgrade, lock_time, @@ -685,9 +697,11 @@ impl ZcashSerialize for Transaction { outputs, sapling_shielded_data, orchard_shielded_data, + orchard_zsa_issue_data, } => { + // FIXME: fix spec or use another link as the current version of the PDF + // doesn't contain V6 description. // Transaction V6 spec: - // FIXME: specify a proper ref // https://zips.z.cash/protocol/protocol.pdf#txnencoding // Denoted as `nVersionGroupId` in the spec. @@ -722,6 +736,9 @@ impl ZcashSerialize for Transaction { // `flagsOrchard`,`valueBalanceOrchard`, `anchorOrchard`, `sizeProofsOrchard`, // `proofsOrchard`, `vSpendAuthSigsOrchard`, and `bindingSigOrchard`. orchard_shielded_data.zcash_serialize(&mut writer)?; + + // TODO: FIXME: add ref to spec + orchard_zsa_issue_data.zcash_serialize(&mut writer)?; } } Ok(()) @@ -980,9 +997,11 @@ impl ZcashDeserialize for Transaction { }) } // FIXME: implement a proper deserialization for V6 + #[cfg(feature = "tx-v6")] (6, true) => { + // FIXME: fix spec or use another link as the current version of the PDF + // doesn't contain V6 description. // Transaction V6 spec: - // FIXME: specify a proper ref // https://zips.z.cash/protocol/protocol.pdf#txnencoding // Denoted as `nVersionGroupId` in the spec. @@ -1023,6 +1042,9 @@ impl ZcashDeserialize for Transaction { // `proofsOrchard`, `vSpendAuthSigsOrchard`, and `bindingSigOrchard`. let orchard_shielded_data = (&mut limited_reader).zcash_deserialize_into()?; + // TODO: FIXME: add ref to spec + let orchard_zsa_issue_data = (&mut limited_reader).zcash_deserialize_into()?; + Ok(Transaction::V6 { network_upgrade, lock_time, @@ -1031,6 +1053,7 @@ impl ZcashDeserialize for Transaction { outputs, sapling_shielded_data, orchard_shielded_data, + orchard_zsa_issue_data, }) } (_, _) => Err(SerializationError::Parse("bad tx header")), @@ -1080,6 +1103,11 @@ pub const MIN_TRANSPARENT_TX_V4_SIZE: u64 = MIN_TRANSPARENT_TX_SIZE + 4; /// v5 transactions also have an expiry height and a consensus branch ID. 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. +//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 /// /// `tx` messages contain a single transaction, and `block` messages are limited to the maximum diff --git a/zebra-chain/src/transaction/tests/vectors.rs b/zebra-chain/src/transaction/tests/vectors.rs index f20bfaef455..9e0af8adf63 100644 --- a/zebra-chain/src/transaction/tests/vectors.rs +++ b/zebra-chain/src/transaction/tests/vectors.rs @@ -988,11 +988,7 @@ fn binding_signatures_for_network(network: Network) { .expect("must pass verification"); } } - Transaction::V5 { - sapling_shielded_data, - .. - } - | Transaction::V6 { + tx_v5_and_v6! { sapling_shielded_data, .. } => { diff --git a/zebra-chain/src/transaction/txid.rs b/zebra-chain/src/transaction/txid.rs index eb05fd6edb3..08172459ade 100644 --- a/zebra-chain/src/transaction/txid.rs +++ b/zebra-chain/src/transaction/txid.rs @@ -2,7 +2,7 @@ //! from the transaction. use std::io; -use super::{Hash, Transaction}; +use super::{tx_v5_and_v6, Hash, Transaction}; use crate::serialization::{sha256d, ZcashSerialize}; /// A Transaction ID builder. It computes the transaction ID by hashing @@ -28,7 +28,7 @@ impl<'a> TxIdBuilder<'a> { | Transaction::V2 { .. } | Transaction::V3 { .. } | Transaction::V4 { .. } => self.txid_v1_to_v4(), - Transaction::V5 { .. } | Transaction::V6 { .. } => self.txid_v5_v6(), + tx_v5_and_v6! { .. } => self.txid_v5_v6(), } } diff --git a/zebra-chain/src/transaction/unmined.rs b/zebra-chain/src/transaction/unmined.rs index 554e8c3f0e1..ce327ae9f76 100644 --- a/zebra-chain/src/transaction/unmined.rs +++ b/zebra-chain/src/transaction/unmined.rs @@ -21,7 +21,7 @@ use crate::{ amount::{Amount, NonNegative}, serialization::ZcashSerialize, transaction::{ - AuthDigest, Hash, + tx_v5_and_v6, AuthDigest, Hash, Transaction::{self, *}, WtxId, }, @@ -140,7 +140,7 @@ impl From<&Transaction> for UnminedTxId { fn from(transaction: &Transaction) -> Self { match transaction { V1 { .. } | V2 { .. } | V3 { .. } | V4 { .. } => Legacy(transaction.into()), - V5 { .. } | V6 { .. } => Witnessed(transaction.into()), + tx_v5_and_v6! { .. } => Witnessed(transaction.into()), } } } diff --git a/zebra-consensus/Cargo.toml b/zebra-consensus/Cargo.toml index 2dd58ed562d..f85b10d9370 100644 --- a/zebra-consensus/Cargo.toml +++ b/zebra-consensus/Cargo.toml @@ -16,6 +16,7 @@ categories = ["asynchronous", "cryptography::cryptocurrencies"] [features] default = [] +#default = ["tx-v6"] # Production features that activate extra dependencies, or extra features in dependencies @@ -34,6 +35,12 @@ getblocktemplate-rpcs = [ # Test-only features proptest-impl = ["proptest", "proptest-derive", "zebra-chain/proptest-impl", "zebra-state/proptest-impl"] +# Support for transaction version 6 +tx-v6 = [ + "zebra-state/tx-v6", + "zebra-chain/tx-v6" +] + [dependencies] blake2b_simd = "1.0.2" bellman = "0.14.0" diff --git a/zebra-consensus/src/primitives/halo2.rs b/zebra-consensus/src/primitives/halo2.rs index 447d9bbd449..ab88f6cbde4 100644 --- a/zebra-consensus/src/primitives/halo2.rs +++ b/zebra-consensus/src/primitives/halo2.rs @@ -10,7 +10,7 @@ use std::{ use futures::{future::BoxFuture, FutureExt}; use once_cell::sync::Lazy; -use orchard::{circuit::VerifyingKey, orchard_flavor::OrchardVanilla}; +use orchard::circuit::VerifyingKey; use rand::{thread_rng, CryptoRng, RngCore}; use thiserror::Error; @@ -19,6 +19,8 @@ use tower::{util::ServiceFn, Service}; use tower_batch_control::{Batch, BatchControl}; use tower_fallback::Fallback; +use zebra_chain::orchard::{OrchardFlavorExt, OrchardVanilla}; + use crate::BoxError; use super::{spawn_fifo, spawn_fifo_and_convert}; @@ -76,7 +78,7 @@ pub type ItemVerifyingKey = VerifyingKey; lazy_static::lazy_static! { /// The halo2 proof verifying key. // FIXME: support OrchardZSA? - pub static ref VERIFYING_KEY: ItemVerifyingKey = ItemVerifyingKey::build::(); + pub static ref VERIFYING_KEY: ItemVerifyingKey = ItemVerifyingKey::build::<::Flavor>(); } // === TEMPORARY BATCH HALO2 SUBSTITUTE === @@ -131,8 +133,8 @@ impl BatchVerifier { // === END TEMPORARY BATCH HALO2 SUBSTITUTE === -impl From<&zebra_chain::orchard::ShieldedData> for Item { - fn from(shielded_data: &zebra_chain::orchard::ShieldedData) -> Item { +impl From<&zebra_chain::orchard::ShieldedData> for Item { + fn from(shielded_data: &zebra_chain::orchard::ShieldedData) -> Item { use orchard::{circuit, note, primitives::redpallas, tree, value}; let anchor = tree::Anchor::from_bytes(shielded_data.shared_anchor.into()).unwrap(); diff --git a/zebra-consensus/src/primitives/halo2/tests.rs b/zebra-consensus/src/primitives/halo2/tests.rs index 9b5c367e640..8af73f182e1 100644 --- a/zebra-consensus/src/primitives/halo2/tests.rs +++ b/zebra-consensus/src/primitives/halo2/tests.rs @@ -12,14 +12,13 @@ use orchard::{ circuit::ProvingKey, keys::{FullViewingKey, Scope, SpendingKey}, note::AssetBase, - orchard_flavor::OrchardVanilla, value::NoteValue, Anchor, Bundle, }; use rand::rngs::OsRng; use zebra_chain::{ - orchard::ShieldedData, + orchard::{OrchardFlavorExt, OrchardVanilla, ShieldedData}, serialization::{ZcashDeserializeInto, ZcashSerialize}, }; @@ -28,7 +27,7 @@ use crate::primitives::halo2::*; // FIXME: add support for OrchardZSA (see OrchardVanilla and AssetBase::native() usage below) #[allow(dead_code, clippy::print_stdout)] fn generate_test_vectors() { - let proving_key = ProvingKey::build::(); + let proving_key = ProvingKey::build::<::Flavor>(); let rng = OsRng; @@ -41,7 +40,7 @@ fn generate_test_vectors() { let anchor_bytes = [0; 32]; let note_value = 10; - let shielded_data: Vec = (1..=4) + let shielded_data: Vec> = (1..=4) .map(|num_recipients| { let mut builder = Builder::new( BundleType::Transactional { @@ -63,7 +62,8 @@ fn generate_test_vectors() { .unwrap(); } - let bundle: Bundle<_, i64, OrchardVanilla> = builder.build(rng).unwrap().unwrap().0; + let bundle: Bundle<_, i64, ::Flavor> = + builder.build(rng).unwrap().unwrap().0; let bundle = bundle .create_proof(&proving_key, rng) @@ -71,7 +71,7 @@ fn generate_test_vectors() { .apply_signatures(rng, [0; 32], &[]) .unwrap(); - zebra_chain::orchard::ShieldedData { + zebra_chain::orchard::ShieldedData:: { flags, value_balance: note_value.try_into().unwrap(), shared_anchor: anchor_bytes.try_into().unwrap(), @@ -82,7 +82,7 @@ fn generate_test_vectors() { .actions() .iter() .map(|a| { - let action = zebra_chain::orchard::Action { + let action = zebra_chain::orchard::Action:: { cv: a.cv_net().to_bytes().try_into().unwrap(), nullifier: a.nullifier().to_bytes().try_into().unwrap(), rk: <[u8; 32]>::from(a.rk()).into(), @@ -91,7 +91,7 @@ fn generate_test_vectors() { // FIXME: support OrchardZSA too, 580 works for OrchardVanilla only! // FIXME: consider more "type safe" way to do the following conversion // (now it goes through &[u8]) - enc_ciphertext: <[u8; 580]>::try_from( + enc_ciphertext: <[u8; OrchardVanilla::ENCRYPTED_NOTE_SIZE]>::try_from( a.encrypted_note().enc_ciphertext.as_ref(), ) .unwrap() @@ -107,6 +107,9 @@ fn generate_test_vectors() { .try_into() .unwrap(), binding_sig: <[u8; 64]>::from(bundle.authorization().binding_signature()).into(), + // FIXME: use a proper value when implementing V6 + #[cfg(feature = "tx-v6")] + burn: Default::default(), } }) .collect(); @@ -121,7 +124,7 @@ fn generate_test_vectors() { async fn verify_orchard_halo2_proofs( verifier: &mut V, - shielded_data: Vec, + shielded_data: Vec>, ) -> Result<(), V::Error> where V: tower::Service, @@ -154,9 +157,10 @@ async fn verify_generated_halo2_proofs() { .clone() .iter() .map(|bytes| { - let maybe_shielded_data: Option = bytes - .zcash_deserialize_into() - .expect("a valid orchard::ShieldedData instance"); + let maybe_shielded_data: Option> = + bytes + .zcash_deserialize_into() + .expect("a valid orchard::ShieldedData instance"); maybe_shielded_data.unwrap() }) .collect(); @@ -183,7 +187,7 @@ async fn verify_generated_halo2_proofs() { async fn verify_invalid_orchard_halo2_proofs( verifier: &mut V, - shielded_data: Vec, + shielded_data: Vec>, ) -> Result<(), V::Error> where V: tower::Service, @@ -221,9 +225,10 @@ async fn correctly_err_on_invalid_halo2_proofs() { .clone() .iter() .map(|bytes| { - let maybe_shielded_data: Option = bytes - .zcash_deserialize_into() - .expect("a valid orchard::ShieldedData instance"); + let maybe_shielded_data: Option> = + bytes + .zcash_deserialize_into() + .expect("a valid orchard::ShieldedData instance"); maybe_shielded_data.unwrap() }) .collect(); diff --git a/zebra-consensus/src/transaction.rs b/zebra-consensus/src/transaction.rs index 5c21ccd494c..e91f576f2a5 100644 --- a/zebra-consensus/src/transaction.rs +++ b/zebra-consensus/src/transaction.rs @@ -406,12 +406,7 @@ where orchard_shielded_data, .. } - // FIXME: implement proper V6 verification - | Transaction::V6 { - sapling_shielded_data, - orchard_shielded_data, - .. - }=> Self::verify_v5_transaction( + => Self::verify_v5_transaction( &req, &network, script_verifier, @@ -419,6 +414,14 @@ where sapling_shielded_data, orchard_shielded_data, )?, + // FIXME: implement proper V6 verification + #[cfg(feature = "tx-v6")] + Transaction::V6 { + .. + } => { + tracing::debug!(?tx, "V6 transaction verification is not supported for now"); + return Err(TransactionError::WrongVersion); + } }; if let Some(unmined_tx) = req.mempool_transaction() { @@ -722,7 +725,7 @@ where script_verifier: script::Verifier, cached_ffi_transaction: Arc, sapling_shielded_data: &Option>, - orchard_shielded_data: &Option, + orchard_shielded_data: &Option>, ) -> Result { let transaction = request.transaction(); let upgrade = request.upgrade(network); @@ -1019,7 +1022,7 @@ where /// Verifies a transaction's Orchard shielded data. fn verify_orchard_shielded_data( - orchard_shielded_data: &Option, + orchard_shielded_data: &Option>, shielded_sighash: &SigHash, ) -> Result { let mut async_checks = AsyncChecks::new(); diff --git a/zebra-consensus/src/transaction/check.rs b/zebra-consensus/src/transaction/check.rs index 66e3d0be595..133c7e80470 100644 --- a/zebra-consensus/src/transaction/check.rs +++ b/zebra-consensus/src/transaction/check.rs @@ -172,8 +172,8 @@ pub fn coinbase_tx_no_prevout_joinsplit_spend(tx: &Transaction) -> Result<(), Tr return Err(TransactionError::CoinbaseHasSpend); } - if let Some(orchard_shielded_data) = tx.orchard_shielded_data() { - if orchard_shielded_data.flags.contains(Flags::ENABLE_SPENDS) { + if let Some(orchard_flags) = tx.orchard_flags() { + if orchard_flags.contains(Flags::ENABLE_SPENDS) { return Err(TransactionError::CoinbaseHasEnableSpendsOrchard); } } diff --git a/zebra-consensus/src/transaction/tests.rs b/zebra-consensus/src/transaction/tests.rs index 0a4c21bb039..5494537edb8 100644 --- a/zebra-consensus/src/transaction/tests.rs +++ b/zebra-consensus/src/transaction/tests.rs @@ -12,7 +12,7 @@ use tower::{service_fn, ServiceExt}; use zebra_chain::{ amount::{Amount, NonNegative}, block::{self, Block, Height}, - orchard::AuthorizedAction, + orchard::{AuthorizedAction, OrchardVanilla}, parameters::{Network, NetworkUpgrade}, primitives::{ed25519, x25519, Groth16Proof}, sapling, @@ -2830,9 +2830,9 @@ fn coinbase_outputs_are_decryptable_for_historical_blocks_for_network( /// Given an Orchard action as a base, fill fields related to note encryption /// from the given test vector and returned the modified action. fn fill_action_with_note_encryption_test_vector( - action: &zebra_chain::orchard::Action, + action: &zebra_chain::orchard::Action, v: &zebra_test::vectors::TestVector, -) -> zebra_chain::orchard::Action { +) -> zebra_chain::orchard::Action { let mut action = action.clone(); action.cv = v.cv_net.try_into().expect("test vector must be valid"); action.cm_x = pallas::Base::from_repr(v.cmx).unwrap(); diff --git a/zebra-rpc/qa/rpc-tests/test_framework/mininode.py b/zebra-rpc/qa/rpc-tests/test_framework/mininode.py index d56fb8bf79c..2fbe6faca89 100755 --- a/zebra-rpc/qa/rpc-tests/test_framework/mininode.py +++ b/zebra-rpc/qa/rpc-tests/test_framework/mininode.py @@ -425,6 +425,7 @@ def __repr__(self): return "RedPallasSignature(%s)" % bytes_to_hex_str(self.data) +# FIXME: add support of OrchardZSA class OrchardAction(object): def __init__(self): self.cv = None @@ -441,7 +442,7 @@ def deserialize(self, f): self.rk = deser_uint256(f) self.cmx = deser_uint256(f) self.ephemeralKey = deser_uint256(f) - self.encCiphertext = f.read(580) + self.encCiphertext = f.read(580) # FIXME: works for OrchardVanilla only self.outCiphertext = f.read(80) def serialize(self): diff --git a/zebra-state/Cargo.toml b/zebra-state/Cargo.toml index 6bdfdaaeb66..af73168ae39 100644 --- a/zebra-state/Cargo.toml +++ b/zebra-state/Cargo.toml @@ -15,6 +15,8 @@ keywords = ["zebra", "zcash"] categories = ["asynchronous", "caching", "cryptography::cryptocurrencies"] [features] +default = [] +#default = ["tx-v6"] # Production features that activate extra dependencies, or extra features in dependencies @@ -45,6 +47,11 @@ elasticsearch = [ "zebra-chain/elasticsearch", ] +# Support for transaction version 6 +tx-v6 = [ + "zebra-chain/tx-v6" +] + [dependencies] bincode = "1.3.3" chrono = { version = "0.4.38", default-features = false, features = ["clock", "std"] } diff --git a/zebra-state/src/service/check/anchors.rs b/zebra-state/src/service/check/anchors.rs index 5f6ee293e34..b4a53c8f176 100644 --- a/zebra-state/src/service/check/anchors.rs +++ b/zebra-state/src/service/check/anchors.rs @@ -88,25 +88,21 @@ fn sapling_orchard_anchors_refer_to_final_treestates( // > earlier block’s final Orchard treestate. // // - if let Some(orchard_shielded_data) = transaction.orchard_shielded_data() { + if let Some(shared_anchor) = transaction.orchard_shared_anchor() { tracing::debug!( - ?orchard_shielded_data.shared_anchor, + ?shared_anchor, ?tx_index_in_block, ?height, "observed orchard anchor", ); if !parent_chain - .map(|chain| { - chain - .orchard_anchors - .contains(&orchard_shielded_data.shared_anchor) - }) + .map(|chain| chain.orchard_anchors.contains(&shared_anchor)) .unwrap_or(false) - && !finalized_state.contains_orchard_anchor(&orchard_shielded_data.shared_anchor) + && !finalized_state.contains_orchard_anchor(&shared_anchor) { return Err(ValidateContextError::UnknownOrchardAnchor { - anchor: orchard_shielded_data.shared_anchor, + anchor: shared_anchor, height, tx_index_in_block, transaction_hash, @@ -114,7 +110,7 @@ fn sapling_orchard_anchors_refer_to_final_treestates( } tracing::debug!( - ?orchard_shielded_data.shared_anchor, + ?shared_anchor, ?tx_index_in_block, ?height, "validated orchard anchor", diff --git a/zebra-state/src/service/check/tests/nullifier.rs b/zebra-state/src/service/check/tests/nullifier.rs index 0392f1c8e79..8a8b17e4fd0 100644 --- a/zebra-state/src/service/check/tests/nullifier.rs +++ b/zebra-state/src/service/check/tests/nullifier.rs @@ -700,8 +700,8 @@ proptest! { /// (And that the test infrastructure generally works.) #[test] fn accept_distinct_arbitrary_orchard_nullifiers_in_one_block( - authorized_action in TypeNameToDebug::::arbitrary(), - orchard_shielded_data in TypeNameToDebug::::arbitrary(), + authorized_action in TypeNameToDebug::>::arbitrary(), + orchard_shielded_data in TypeNameToDebug::>::arbitrary(), use_finalized_state in any::(), ) { let _init_guard = zebra_test::init(); @@ -759,9 +759,9 @@ proptest! { /// if they come from different AuthorizedActions in the same orchard::ShieldedData/Transaction. #[test] fn reject_duplicate_orchard_nullifiers_in_transaction( - authorized_action1 in TypeNameToDebug::::arbitrary(), - mut authorized_action2 in TypeNameToDebug::::arbitrary(), - orchard_shielded_data in TypeNameToDebug::::arbitrary(), + authorized_action1 in TypeNameToDebug::>::arbitrary(), + mut authorized_action2 in TypeNameToDebug::>::arbitrary(), + orchard_shielded_data in TypeNameToDebug::>::arbitrary(), ) { let _init_guard = zebra_test::init(); @@ -812,10 +812,10 @@ proptest! { /// if they come from different transactions in the same block. #[test] fn reject_duplicate_orchard_nullifiers_in_block( - authorized_action1 in TypeNameToDebug::::arbitrary(), - mut authorized_action2 in TypeNameToDebug::::arbitrary(), - orchard_shielded_data1 in TypeNameToDebug::::arbitrary(), - orchard_shielded_data2 in TypeNameToDebug::::arbitrary(), + authorized_action1 in TypeNameToDebug::>::arbitrary(), + mut authorized_action2 in TypeNameToDebug::>::arbitrary(), + orchard_shielded_data1 in TypeNameToDebug::>::arbitrary(), + orchard_shielded_data2 in TypeNameToDebug::>::arbitrary(), ) { let _init_guard = zebra_test::init(); @@ -872,10 +872,10 @@ proptest! { /// if they come from different blocks in the same chain. #[test] fn reject_duplicate_orchard_nullifiers_in_chain( - authorized_action1 in TypeNameToDebug::::arbitrary(), - mut authorized_action2 in TypeNameToDebug::::arbitrary(), - orchard_shielded_data1 in TypeNameToDebug::::arbitrary(), - orchard_shielded_data2 in TypeNameToDebug::::arbitrary(), + authorized_action1 in TypeNameToDebug::>::arbitrary(), + mut authorized_action2 in TypeNameToDebug::>::arbitrary(), + orchard_shielded_data1 in TypeNameToDebug::>::arbitrary(), + orchard_shielded_data2 in TypeNameToDebug::>::arbitrary(), duplicate_in_finalized_state in any::(), ) { let _init_guard = zebra_test::init(); @@ -1126,8 +1126,8 @@ fn transaction_v4_with_sapling_shielded_data( /// /// If there are no `AuthorizedAction`s in `authorized_actions`. fn transaction_v5_with_orchard_shielded_data( - orchard_shielded_data: impl Into>, - authorized_actions: impl IntoIterator, + orchard_shielded_data: impl Into>>, + authorized_actions: impl IntoIterator>, ) -> Transaction { let mut orchard_shielded_data = orchard_shielded_data.into(); let authorized_actions: Vec<_> = authorized_actions.into_iter().collect(); diff --git a/zebra-state/src/service/finalized_state/zebra_db/arbitrary.rs b/zebra-state/src/service/finalized_state/zebra_db/arbitrary.rs index bbe0e026d8c..39bf082d43d 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/arbitrary.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/arbitrary.rs @@ -66,8 +66,8 @@ impl ZebraDb { } // Orchard - if let Some(orchard_shielded_data) = transaction.orchard_shielded_data() { - batch.zs_insert(&orchard_anchors, orchard_shielded_data.shared_anchor, ()); + if let Some(shared_anchor) = transaction.orchard_shared_anchor() { + batch.zs_insert(&orchard_anchors, shared_anchor, ()); } } diff --git a/zebra-state/src/service/non_finalized_state/chain.rs b/zebra-state/src/service/non_finalized_state/chain.rs index dce9c783ec8..d0ce3eee904 100644 --- a/zebra-state/src/service/non_finalized_state/chain.rs +++ b/zebra-state/src/service/non_finalized_state/chain.rs @@ -1492,21 +1492,23 @@ impl Chain { .zip(transaction_hashes.iter().cloned()) .enumerate() { - let ( - inputs, - outputs, - joinsplit_data, - sapling_shielded_data_per_spend_anchor, - sapling_shielded_data_shared_anchor, - orchard_shielded_data, - ) = match transaction.deref() { + let transaction_data = match transaction.deref() { V4 { inputs, outputs, joinsplit_data, sapling_shielded_data, .. - } => (inputs, outputs, joinsplit_data, sapling_shielded_data, &None, &None), + } => ( + inputs, + outputs, + joinsplit_data, + sapling_shielded_data, + &None, + &None, + #[cfg(feature ="tx-v6")] + &None + ), V5 { inputs, outputs, @@ -1520,7 +1522,10 @@ impl Chain { &None, sapling_shielded_data, orchard_shielded_data, + #[cfg(feature ="tx-v6")] + &None, ), + #[cfg(feature ="tx-v6")] V6 { inputs, outputs, @@ -1533,6 +1538,7 @@ impl Chain { &None, &None, sapling_shielded_data, + &None, orchard_shielded_data, ), V1 { .. } | V2 { .. } | V3 { .. } => unreachable!( @@ -1540,6 +1546,27 @@ impl Chain { ), }; + #[cfg(not(feature = "tx-v6"))] + let ( + inputs, + outputs, + joinsplit_data, + sapling_shielded_data_per_spend_anchor, + sapling_shielded_data_shared_anchor, + orchard_shielded_data_vanilla, + ) = transaction_data; + + #[cfg(feature = "tx-v6")] + let ( + inputs, + outputs, + joinsplit_data, + sapling_shielded_data_per_spend_anchor, + sapling_shielded_data_shared_anchor, + orchard_shielded_data_vanilla, + orchard_shielded_data_zsa, + ) = transaction_data; + // add key `transaction.hash` and value `(height, tx_index)` to `tx_loc_by_hash` let transaction_location = TransactionLocation::from_usize(height, transaction_index); let prior_pair = self @@ -1559,7 +1586,9 @@ impl Chain { self.update_chain_tip_with(joinsplit_data)?; self.update_chain_tip_with(sapling_shielded_data_per_spend_anchor)?; self.update_chain_tip_with(sapling_shielded_data_shared_anchor)?; - self.update_chain_tip_with(orchard_shielded_data)?; + self.update_chain_tip_with(orchard_shielded_data_vanilla)?; + #[cfg(feature = "tx-v6")] + self.update_chain_tip_with(orchard_shielded_data_zsa)?; } // update the chain value pool balances @@ -1696,11 +1725,12 @@ impl UpdateWith for Chain { sapling_shielded_data, orchard_shielded_data, ), + #[cfg(feature = "tx-v6")] V6 { inputs, outputs, sapling_shielded_data, - orchard_shielded_data, + orchard_shielded_data: _, .. } => ( inputs, @@ -1708,7 +1738,8 @@ impl UpdateWith for Chain { &None, &None, sapling_shielded_data, - orchard_shielded_data, + // FIXME: support V6 shielded data? + &None, //orchard_shielded_data, ), V1 { .. } | V2 { .. } | V3 { .. } => unreachable!( "older transaction versions only exist in finalized blocks, because of the mandatory canopy checkpoint", @@ -2045,11 +2076,11 @@ where } } -impl UpdateWith> for Chain { +impl UpdateWith>> for Chain { #[instrument(skip(self, orchard_shielded_data))] fn update_chain_tip_with( &mut self, - orchard_shielded_data: &Option, + orchard_shielded_data: &Option>, ) -> Result<(), ValidateContextError> { if let Some(orchard_shielded_data) = orchard_shielded_data { // We do note commitment tree updates in parallel rayon threads. @@ -2070,7 +2101,7 @@ impl UpdateWith> for Chain { #[instrument(skip(self, orchard_shielded_data))] fn revert_chain_with( &mut self, - orchard_shielded_data: &Option, + orchard_shielded_data: &Option>, _position: RevertPosition, ) { if let Some(orchard_shielded_data) = orchard_shielded_data { diff --git a/zebra-state/src/tests.rs b/zebra-state/src/tests.rs index 6b955480572..d01a871f142 100644 --- a/zebra-state/src/tests.rs +++ b/zebra-state/src/tests.rs @@ -34,6 +34,7 @@ impl FakeChainHelper for Arc { Transaction::V3 { inputs, .. } => &mut inputs[0], Transaction::V4 { inputs, .. } => &mut inputs[0], Transaction::V5 { inputs, .. } => &mut inputs[0], + #[cfg(feature = "tx-v6")] Transaction::V6 { inputs, .. } => &mut inputs[0], }; diff --git a/zebra-test/src/vectors/orchard_note_encryption.rs b/zebra-test/src/vectors/orchard_note_encryption.rs index ff52b661b53..84b576df5c3 100644 --- a/zebra-test/src/vectors/orchard_note_encryption.rs +++ b/zebra-test/src/vectors/orchard_note_encryption.rs @@ -1,6 +1,7 @@ //! Contains test vectors for Orchard note encryptions. use lazy_static::lazy_static; +// FIXME: add tests for OrchardZSA #[allow(missing_docs)] pub struct TestVector { pub incoming_viewing_key: [u8; 64], @@ -18,7 +19,7 @@ pub struct TestVector { pub shared_secret: [u8; 32], pub k_enc: [u8; 32], pub p_enc: [u8; 564], - pub c_enc: [u8; 580], + pub c_enc: [u8; 580], // FIXME: works for OrchardVanilla only! pub ock: [u8; 32], pub op: [u8; 64], pub c_out: [u8; 80], diff --git a/zebrad/Cargo.toml b/zebrad/Cargo.toml index 2bea0392f9d..fc1c8d4a889 100644 --- a/zebrad/Cargo.toml +++ b/zebrad/Cargo.toml @@ -52,7 +52,8 @@ features = [ [features] # In release builds, don't compile debug logging code, to improve performance. -default = ["release_max_level_info", "progress-bar", "getblocktemplate-rpcs"] +#default = ["release_max_level_info", "progress-bar", "getblocktemplate-rpcs"] +default = ["release_max_level_info", "progress-bar", "getblocktemplate-rpcs", "tx-v6"] # Default features for official ZF binary release builds default-release-binaries = ["default", "sentry"] @@ -156,6 +157,13 @@ test_sync_to_mandatory_checkpoint_testnet = [] test_sync_past_mandatory_checkpoint_mainnet = [] test_sync_past_mandatory_checkpoint_testnet = [] +# Support for transaction version 6 +tx-v6 = [ + "zebra-consensus/tx-v6", + "zebra-state/tx-v6", + "zebra-chain/tx-v6" +] + [dependencies] zebra-chain = { path = "../zebra-chain", version = "1.0.0-beta.41" } zebra-consensus = { path = "../zebra-consensus", version = "1.0.0-beta.41" } diff --git a/zebrad/src/components/mempool/storage/tests/prop.rs b/zebrad/src/components/mempool/storage/tests/prop.rs index 98abdb2b8e8..d3ef954063d 100644 --- a/zebrad/src/components/mempool/storage/tests/prop.rs +++ b/zebrad/src/components/mempool/storage/tests/prop.rs @@ -568,7 +568,11 @@ impl SpendConflictTestInput { } // No JoinSplits - Transaction::V1 { .. } | Transaction::V5 { .. } | Transaction::V6 { .. } => {} + Transaction::V1 { .. } | Transaction::V5 { .. } => {} + + // No JoinSplits + #[cfg(feature = "tx-v6")] + Transaction::V6 { .. } => {} } } } @@ -635,8 +639,12 @@ impl SpendConflictTestInput { Transaction::V5 { sapling_shielded_data, .. + } => { + Self::remove_sapling_transfers_with_conflicts(sapling_shielded_data, &conflicts) } - | Transaction::V6 { + + #[cfg(feature = "tx-v6")] + Transaction::V6 { sapling_shielded_data, .. } => { @@ -712,12 +720,15 @@ impl SpendConflictTestInput { Transaction::V5 { orchard_shielded_data, .. - } - | Transaction::V6 { - orchard_shielded_data, - .. } => Self::remove_orchard_actions_with_conflicts(orchard_shielded_data, &conflicts), + // FIXME: implement for V6 + #[cfg(feature = "tx-v6")] + Transaction::V6 { + orchard_shielded_data: _, + .. + } => {} + // No Spends Transaction::V1 { .. } | Transaction::V2 { .. } @@ -732,7 +743,7 @@ impl SpendConflictTestInput { /// /// This may clear the entire shielded data. fn remove_orchard_actions_with_conflicts( - maybe_shielded_data: &mut Option, + maybe_shielded_data: &mut Option>, conflicts: &HashSet, ) { if let Some(shielded_data) = maybe_shielded_data.take() { @@ -789,10 +800,11 @@ struct SaplingSpendConflict { fallback_shielded_data: DisplayToDebug>, } +// FIXME: make it a generic to support V6 /// A conflict caused by revealing the same Orchard nullifier. #[derive(Arbitrary, Clone, Debug)] struct OrchardSpendConflict { - new_shielded_data: DisplayToDebug, + new_shielded_data: DisplayToDebug>, } impl SpendConflictForTransactionV4 { @@ -938,7 +950,10 @@ impl OrchardSpendConflict { /// the new action is inserted in the transaction. /// /// The transaction will then conflict with any other transaction with the same new nullifier. - pub fn apply_to(self, orchard_shielded_data: &mut Option) { + pub fn apply_to( + self, + orchard_shielded_data: &mut Option>, + ) { if let Some(shielded_data) = orchard_shielded_data.as_mut() { shielded_data.actions.first_mut().action.nullifier = self.new_shielded_data.actions.first().action.nullifier;