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 <teor@riseup.net>
This commit is contained in:
parent
65fa1c6bd9
commit
8883543a85
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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<ShieldedData>,
|
||||
},
|
||||
/// 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<transparent::Input>,
|
||||
/// The transparent outputs from the transaction.
|
||||
outputs: Vec<transparent::Output>,
|
||||
/// The rest of the transaction as bytes
|
||||
rest: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
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()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -104,6 +104,26 @@ impl Transaction {
|
|||
)
|
||||
.boxed()
|
||||
}
|
||||
/// Generate a proptest strategy for V5 Transactions
|
||||
pub fn v5_strategy(ledger_state: LedgerState) -> BoxedStrategy<Self> {
|
||||
(
|
||||
any::<LockTime>(),
|
||||
any::<block::Height>(),
|
||||
transparent::Input::vec_strategy(ledger_state, 10),
|
||||
vec(any::<transparent::Output>(), 0..10),
|
||||
any::<Vec<u8>>(),
|
||||
)
|
||||
.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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
|
||||
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::<LittleEndian>(5 | (1 << 31))?;
|
||||
writer.write_u32::<LittleEndian>(TX_V5_VERSION_GROUP_ID)?;
|
||||
lock_time.zcash_serialize(&mut writer)?;
|
||||
writer.write_u32::<LittleEndian>(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::<LittleEndian>()?;
|
||||
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::<LittleEndian>()?);
|
||||
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")),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Bctv14Proof>
|
||||
// For v4 joinsplit_data is a JoinSplitData<Groth16Proof>
|
||||
|
|
@ -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),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
|
|
|
|||
|
|
@ -165,14 +165,16 @@ impl UpdateWith<PreparedBlock> 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<PreparedBlock> 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",
|
||||
),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ impl FakeChainHelper for Arc<Block> {
|
|||
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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue