From 8883543a85db643fb153d3860bc5c1074cc928ae Mon Sep 17 00:00:00 2001 From: Alfredo Garcia Date: Wed, 3 Mar 2021 18:56:41 -0300 Subject: [PATCH] Add transaction version 5 stubs (#1824) * add transaction V5 stub * add v5_strategy * deduplicate version group ids * Update comment for V5 transactions * Add V5 transactions to non_finalized_state Currently these are all `unimplemented!(...)` * Fix struct matches * Apply trivial panic message changes * add zcash_deserialize for V5 * make all tx versions explicit in sprout and sapling nullifier functions * match exhaustively in sprout and sapling nullifier functions * fix matches in zebra-consensus * fix NU5 strategy * We're still deciding if v5 transactions support Sprout Co-authored-by: teor --- zebra-chain/src/parameters.rs | 2 + zebra-chain/src/parameters/transaction.rs | 13 +++++ zebra-chain/src/transaction.rs | 53 +++++++++++++++++-- zebra-chain/src/transaction/arbitrary.rs | 28 ++++++++-- zebra-chain/src/transaction/serialize.rs | 41 ++++++++++++-- zebra-chain/src/transaction/sighash.rs | 45 ++++++++++++---- zebra-consensus/src/transaction.rs | 3 ++ zebra-consensus/src/transaction/check.rs | 7 +++ .../src/service/non_finalized_state/chain.rs | 12 +++-- zebra-state/src/tests.rs | 1 + 10 files changed, 181 insertions(+), 24 deletions(-) create mode 100644 zebra-chain/src/parameters/transaction.rs diff --git a/zebra-chain/src/parameters.rs b/zebra-chain/src/parameters.rs index 0fa86428..ff369e79 100644 --- a/zebra-chain/src/parameters.rs +++ b/zebra-chain/src/parameters.rs @@ -15,10 +15,12 @@ mod genesis; mod network; mod network_upgrade; +mod transaction; pub use genesis::*; pub use network::Network; pub use network_upgrade::*; +pub use transaction::*; #[cfg(test)] mod tests; diff --git a/zebra-chain/src/parameters/transaction.rs b/zebra-chain/src/parameters/transaction.rs new file mode 100644 index 00000000..bab59e79 --- /dev/null +++ b/zebra-chain/src/parameters/transaction.rs @@ -0,0 +1,13 @@ +//! Transaction consensus and utility parameters. + +/// The version group ID for Overwinter transactions. +pub const OVERWINTER_VERSION_GROUP_ID: u32 = 0x03C4_8270; + +/// The version group ID for Sapling transactions. +pub const SAPLING_VERSION_GROUP_ID: u32 = 0x892F_2085; + +/// The version group ID for version 5 transactions. +/// +/// Orchard transactions must use transaction version 5 and this version +/// group ID. Sapling transactions can use v4 or v5 transactions. +pub const TX_V5_VERSION_GROUP_ID: u32 = 0x26A7_270A; diff --git a/zebra-chain/src/transaction.rs b/zebra-chain/src/transaction.rs index 1d8401f6..4f2c1285 100644 --- a/zebra-chain/src/transaction.rs +++ b/zebra-chain/src/transaction.rs @@ -42,7 +42,7 @@ use crate::{ /// internally by different enum variants. Because we checkpoint on Sapling /// activation, we do not validate any pre-Sapling transaction types. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -// XXX consider boxing the Optional fields of V4 txs +// XXX consider boxing the Optional fields of V4 and V5 txs #[allow(clippy::large_enum_variant)] pub enum Transaction { /// A fully transparent transaction (`version = 1`). @@ -99,6 +99,22 @@ pub enum Transaction { /// The shielded data for this transaction, if any. shielded_data: Option, }, + /// A `version = 5` transaction, which supports `Sapling` and `Orchard`. + // TODO: does this transaction type support `Sprout`? + // Check for ZIP-225 updates after the decision on 2021-03-05. + V5 { + /// The earliest time or block height that this transaction can be added to the + /// chain. + lock_time: LockTime, + /// The latest block height that this transaction can be added to the chain. + expiry_height: block::Height, + /// The transparent inputs to the transaction. + inputs: Vec, + /// The transparent outputs from the transaction. + outputs: Vec, + /// The rest of the transaction as bytes + rest: Vec, + }, } impl Transaction { @@ -114,6 +130,7 @@ impl Transaction { Transaction::V2 { ref inputs, .. } => inputs, Transaction::V3 { ref inputs, .. } => inputs, Transaction::V4 { ref inputs, .. } => inputs, + Transaction::V5 { ref inputs, .. } => inputs, } } @@ -124,6 +141,7 @@ impl Transaction { Transaction::V2 { ref outputs, .. } => outputs, Transaction::V3 { ref outputs, .. } => outputs, Transaction::V4 { ref outputs, .. } => outputs, + Transaction::V5 { ref outputs, .. } => outputs, } } @@ -134,6 +152,7 @@ impl Transaction { Transaction::V2 { lock_time, .. } => *lock_time, Transaction::V3 { lock_time, .. } => *lock_time, Transaction::V4 { lock_time, .. } => *lock_time, + Transaction::V5 { lock_time, .. } => *lock_time, } } @@ -144,6 +163,7 @@ impl Transaction { Transaction::V2 { .. } => None, Transaction::V3 { expiry_height, .. } => Some(*expiry_height), Transaction::V4 { expiry_height, .. } => Some(*expiry_height), + Transaction::V5 { expiry_height, .. } => Some(*expiry_height), } } @@ -174,8 +194,26 @@ impl Transaction { .joinsplits() .flat_map(|joinsplit| joinsplit.nullifiers.iter()), ), + // Maybe JoinSplits, maybe not, we're still deciding + Transaction::V5 { .. } => { + unimplemented!( + "v5 transaction format as specified in ZIP-225 after decision on 2021-03-12" + ) + } // No JoinSplits - _ => Box::new(std::iter::empty()), + Transaction::V1 { .. } + | Transaction::V2 { + joinsplit_data: None, + .. + } + | Transaction::V3 { + joinsplit_data: None, + .. + } + | Transaction::V4 { + joinsplit_data: None, + .. + } => Box::new(std::iter::empty()), } } @@ -189,8 +227,17 @@ impl Transaction { shielded_data: Some(shielded_data), .. } => Box::new(shielded_data.nullifiers()), + Transaction::V5 { .. } => { + unimplemented!("v5 transaction format as specified in ZIP-225") + } // No JoinSplits - _ => Box::new(std::iter::empty()), + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { + shielded_data: None, + .. + } => Box::new(std::iter::empty()), } } diff --git a/zebra-chain/src/transaction/arbitrary.rs b/zebra-chain/src/transaction/arbitrary.rs index c34da137..80950803 100644 --- a/zebra-chain/src/transaction/arbitrary.rs +++ b/zebra-chain/src/transaction/arbitrary.rs @@ -104,6 +104,26 @@ impl Transaction { ) .boxed() } + /// Generate a proptest strategy for V5 Transactions + pub fn v5_strategy(ledger_state: LedgerState) -> BoxedStrategy { + ( + any::(), + any::(), + transparent::Input::vec_strategy(ledger_state, 10), + vec(any::(), 0..10), + any::>(), + ) + .prop_map( + |(lock_time, expiry_height, inputs, outputs, rest)| Transaction::V5 { + lock_time, + expiry_height, + inputs, + outputs, + rest, + }, + ) + .boxed() + } /// Proptest Strategy for creating a Vector of transactions where the first /// transaction is always the only coinbase transaction @@ -236,9 +256,11 @@ impl Arbitrary for Transaction { NetworkUpgrade::Blossom | NetworkUpgrade::Heartwood | NetworkUpgrade::Canopy => { Self::v4_strategy(ledger_state) } - NetworkUpgrade::NU5 => { - unimplemented!("NU5 upgrade can use v4 or v5 transactions, as specified in ZIP-225") - } + NetworkUpgrade::NU5 => prop_oneof![ + Self::v4_strategy(ledger_state), + Self::v5_strategy(ledger_state) + ] + .boxed(), } } diff --git a/zebra-chain/src/transaction/serialize.rs b/zebra-chain/src/transaction/serialize.rs index dc9c28fa..742da3ba 100644 --- a/zebra-chain/src/transaction/serialize.rs +++ b/zebra-chain/src/transaction/serialize.rs @@ -6,6 +6,7 @@ use std::{io, sync::Arc}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use crate::{ + parameters::{OVERWINTER_VERSION_GROUP_ID, SAPLING_VERSION_GROUP_ID, TX_V5_VERSION_GROUP_ID}, primitives::ZkSnarkProof, serialization::{ ReadZcashExt, SerializationError, WriteZcashExt, ZcashDeserialize, ZcashDeserializeInto, @@ -16,9 +17,6 @@ use crate::{ use super::*; -const OVERWINTER_VERSION_GROUP_ID: u32 = 0x03C4_8270; -const SAPLING_VERSION_GROUP_ID: u32 = 0x892F_2085; - impl ZcashDeserialize for jubjub::Fq { fn zcash_deserialize(mut reader: R) -> Result { let possible_scalar = jubjub::Fq::from_bytes(&reader.read_32_bytes()?); @@ -182,6 +180,23 @@ impl ZcashSerialize for Transaction { None => {} } } + Transaction::V5 { + lock_time, + expiry_height, + inputs, + outputs, + rest, + } => { + // Write version 5 and set the fOverwintered bit. + writer.write_u32::(5 | (1 << 31))?; + writer.write_u32::(TX_V5_VERSION_GROUP_ID)?; + lock_time.zcash_serialize(&mut writer)?; + writer.write_u32::(expiry_height.0)?; + inputs.zcash_serialize(&mut writer)?; + outputs.zcash_serialize(&mut writer)?; + // write the rest + writer.write_all(rest)?; + } } Ok(()) } @@ -285,6 +300,26 @@ impl ZcashDeserialize for Transaction { joinsplit_data, }) } + (5, false) => { + let id = reader.read_u32::()?; + if id != TX_V5_VERSION_GROUP_ID { + return Err(SerializationError::Parse("expected TX_V5_VERSION_GROUP_ID")); + } + let lock_time = LockTime::zcash_deserialize(&mut reader)?; + let expiry_height = block::Height(reader.read_u32::()?); + let inputs = Vec::zcash_deserialize(&mut reader)?; + let outputs = Vec::zcash_deserialize(&mut reader)?; + let mut rest = Vec::new(); + reader.read_to_end(&mut rest)?; + + Ok(Transaction::V5 { + lock_time, + expiry_height, + inputs, + outputs, + rest, + }) + } (_, _) => Err(SerializationError::Parse("bad tx header")), } } diff --git a/zebra-chain/src/transaction/sighash.rs b/zebra-chain/src/transaction/sighash.rs index 525b5035..2b7544b5 100644 --- a/zebra-chain/src/transaction/sighash.rs +++ b/zebra-chain/src/transaction/sighash.rs @@ -1,6 +1,9 @@ use super::Transaction; use crate::{ - parameters::{ConsensusBranchId, NetworkUpgrade}, + parameters::{ + ConsensusBranchId, NetworkUpgrade, OVERWINTER_VERSION_GROUP_ID, SAPLING_VERSION_GROUP_ID, + TX_V5_VERSION_GROUP_ID, + }, serialization::{WriteZcashExt, ZcashSerialize}, transparent, }; @@ -12,9 +15,6 @@ use std::io; static ZIP143_EXPLANATION: &str = "Invalid transaction version: after Overwinter activation transaction versions 1 and 2 are rejected"; static ZIP243_EXPLANATION: &str = "Invalid transaction version: after Sapling activation transaction versions 1, 2, and 3 are rejected"; -const OVERWINTER_VERSION_GROUP_ID: u32 = 0x03C4_8270; -const SAPLING_VERSION_GROUP_ID: u32 = 0x892F_2085; - const ZCASH_SIGHASH_PERSONALIZATION_PREFIX: &[u8; 12] = b"ZcashSigHash"; const ZCASH_PREVOUTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashPrevoutHash"; const ZCASH_SEQUENCE_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashSequencHash"; @@ -129,6 +129,7 @@ impl<'a> SigHasher<'a> { Transaction::V1 { .. } | Transaction::V2 { .. } => unreachable!(ZIP143_EXPLANATION), Transaction::V3 { .. } => 3 | overwintered_flag, Transaction::V4 { .. } => 4 | overwintered_flag, + Transaction::V5 { .. } => 5 | overwintered_flag, }) } @@ -137,6 +138,7 @@ impl<'a> SigHasher<'a> { Transaction::V1 { .. } | Transaction::V2 { .. } => unreachable!(ZIP143_EXPLANATION), Transaction::V3 { .. } => OVERWINTER_VERSION_GROUP_ID, Transaction::V4 { .. } => SAPLING_VERSION_GROUP_ID, + Transaction::V5 { .. } => TX_V5_VERSION_GROUP_ID, }) } @@ -243,6 +245,9 @@ impl<'a> SigHasher<'a> { Transaction::V1 { .. } | Transaction::V2 { .. } => unreachable!(ZIP143_EXPLANATION), Transaction::V3 { joinsplit_data, .. } => joinsplit_data.is_some(), Transaction::V4 { joinsplit_data, .. } => joinsplit_data.is_some(), + Transaction::V5 { .. } => { + unimplemented!("v5 transaction hash as specified in ZIP-225 and ZIP-244") + } }; if !has_joinsplits { @@ -256,7 +261,7 @@ impl<'a> SigHasher<'a> { // This code, and the check above for has_joinsplits cannot be combined // into a single branch because the `joinsplit_data` type of each - // tranaction kind has a different type. + // transaction kind has a different type. // // For v3 joinsplit_data is a JoinSplitData // For v4 joinsplit_data is a JoinSplitData @@ -264,6 +269,8 @@ impl<'a> SigHasher<'a> { // The type parameter on these types prevents them from being unified, // which forces us to duplicate the logic in each branch even though the // code within each branch is identical. + // + // TODO: use a generic function to remove the duplicate code match self.trans { Transaction::V3 { joinsplit_data: Some(jsd), @@ -283,7 +290,20 @@ impl<'a> SigHasher<'a> { } (&mut hash).write_all(&<[u8; 32]>::from(jsd.pub_key)[..])?; } - _ => unreachable!("already checked transaction kind above"), + Transaction::V5 { .. } => { + unimplemented!("v5 transaction hash as specified in ZIP-225 and ZIP-244") + } + + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { + joinsplit_data: None, + .. + } + | Transaction::V4 { + joinsplit_data: None, + .. + } => unreachable!("already checked transaction kind above"), }; writer.write_all(hash.finalize().as_ref()) @@ -398,14 +418,15 @@ impl<'a> SigHasher<'a> { use Transaction::*; let shielded_data = match self.trans { - Transaction::V4 { + V4 { shielded_data: Some(shielded_data), .. } => shielded_data, - Transaction::V4 { + V4 { shielded_data: None, .. } => return writer.write_all(&[0; 32]), + V5 { .. } => unimplemented!("v5 transaction hash as specified in ZIP-225 and ZIP-244"), V1 { .. } | V2 { .. } | V3 { .. } => unreachable!(ZIP243_EXPLANATION), }; @@ -434,14 +455,15 @@ impl<'a> SigHasher<'a> { use Transaction::*; let shielded_data = match self.trans { - Transaction::V4 { + V4 { shielded_data: Some(shielded_data), .. } => shielded_data, - Transaction::V4 { + V4 { shielded_data: None, .. } => return writer.write_all(&[0; 32]), + V5 { .. } => unimplemented!("v5 transaction hash as specified in ZIP-225 and ZIP-244"), V1 { .. } | V2 { .. } | V3 { .. } => unreachable!(ZIP243_EXPLANATION), }; @@ -465,7 +487,8 @@ impl<'a> SigHasher<'a> { use Transaction::*; let value_balance = match self.trans { - Transaction::V4 { value_balance, .. } => value_balance, + V4 { value_balance, .. } => value_balance, + V5 { .. } => unimplemented!("v5 transaction hash as specified in ZIP-225 and ZIP-244"), V1 { .. } | V2 { .. } | V3 { .. } => unreachable!(ZIP243_EXPLANATION), }; diff --git a/zebra-consensus/src/transaction.rs b/zebra-consensus/src/transaction.rs index ae684ae9..a2716dff 100644 --- a/zebra-consensus/src/transaction.rs +++ b/zebra-consensus/src/transaction.rs @@ -258,6 +258,9 @@ where Ok(tx.hash()) } + Transaction::V5 { .. } => { + unimplemented!("v5 transaction validation as specified in ZIP-216, ZIP-224, ZIP-225, and ZIP-244") + } } } .instrument(span) diff --git a/zebra-consensus/src/transaction/check.rs b/zebra-consensus/src/transaction/check.rs index fb2316ab..621ac71d 100644 --- a/zebra-consensus/src/transaction/check.rs +++ b/zebra-consensus/src/transaction/check.rs @@ -71,6 +71,9 @@ pub fn has_inputs_and_outputs(tx: &Transaction) -> Result<(), TransactionError> Transaction::V1 { .. } | Transaction::V2 { .. } | Transaction::V3 { .. } => { unreachable!("tx version is checked first") } + Transaction::V5 { .. } => { + unimplemented!("v5 transaction format as specified in ZIP-225") + } } } @@ -114,6 +117,10 @@ pub fn coinbase_tx_no_joinsplit_or_spend(tx: &Transaction) -> Result<(), Transac Transaction::V1 { .. } | Transaction::V2 { .. } | Transaction::V3 { .. } => { unreachable!("tx version is checked first") } + + Transaction::V5 { .. } => { + unimplemented!("v5 coinbase validation as specified in ZIP-225 and the draft spec") + } } } else { Ok(()) diff --git a/zebra-state/src/service/non_finalized_state/chain.rs b/zebra-state/src/service/non_finalized_state/chain.rs index 82946e6c..1a302e6f 100644 --- a/zebra-state/src/service/non_finalized_state/chain.rs +++ b/zebra-state/src/service/non_finalized_state/chain.rs @@ -165,14 +165,16 @@ impl UpdateWith for Chain { .zip(transaction_hashes.iter().cloned()) .enumerate() { + use transaction::Transaction::*; let (inputs, shielded_data, joinsplit_data) = match transaction.deref() { - transaction::Transaction::V4 { + V4 { inputs, shielded_data, joinsplit_data, .. } => (inputs, shielded_data, joinsplit_data), - _ => unreachable!( + V5 { .. } => unimplemented!("v5 transaction format as specified in ZIP-225"), + V1 { .. } | V2 { .. } | V3 { .. } => unreachable!( "older transaction versions only exist in finalized blocks pre sapling", ), }; @@ -223,14 +225,16 @@ impl UpdateWith for Chain { for (transaction, transaction_hash) in block.transactions.iter().zip(transaction_hashes.iter()) { + use transaction::Transaction::*; let (inputs, shielded_data, joinsplit_data) = match transaction.deref() { - transaction::Transaction::V4 { + V4 { inputs, shielded_data, joinsplit_data, .. } => (inputs, shielded_data, joinsplit_data), - _ => unreachable!( + V5 { .. } => unimplemented!("v5 transaction format as specified in ZIP-225"), + V1 { .. } | V2 { .. } | V3 { .. } => unreachable!( "older transaction versions only exist in finalized blocks pre sapling", ), }; diff --git a/zebra-state/src/tests.rs b/zebra-state/src/tests.rs index 8510504f..247a80aa 100644 --- a/zebra-state/src/tests.rs +++ b/zebra-state/src/tests.rs @@ -53,6 +53,7 @@ impl FakeChainHelper for Arc { Transaction::V2 { inputs, .. } => &mut inputs[0], Transaction::V3 { inputs, .. } => &mut inputs[0], Transaction::V4 { inputs, .. } => &mut inputs[0], + Transaction::V5 { inputs, .. } => &mut inputs[0], }; match input {