add(consensus): Check consensus branch ids in tx verifier (#9063)
* Add a consensus branch id check to tx verifier * Allow updating tx network upgrades * Fix unit tests for txs * Remove `println` * Move test-only tx methods out of the default impl * Add a test for checking consensus branch ids * Simplify some tests * Docs formatting * Update zebra-consensus/src/transaction/check.rs Co-authored-by: Conrado Gouvea <conrado@zfnd.org> * Add `effectiveVersion` to txs * Refactor the consensus branch ID check * Update zebra-consensus/src/error.rs Co-authored-by: Alfredo Garcia <oxarbitrage@gmail.com> * Refactor the consensus branch ID check * Remove `effective_version` * Refactor tests for consensus branch ID check --------- Co-authored-by: Conrado Gouvea <conrado@zfnd.org> Co-authored-by: Alfredo Garcia <oxarbitrage@gmail.com>
This commit is contained in:
parent
a3bb1e2e05
commit
bd122b6f7c
|
@ -324,7 +324,17 @@ impl Transaction {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return the version of this transaction.
|
||||
/// Returns the version of this transaction.
|
||||
///
|
||||
/// Note that the returned version is equal to `effectiveVersion`, described in [§ 7.1
|
||||
/// Transaction Encoding and Consensus]:
|
||||
///
|
||||
/// > `effectiveVersion` [...] is equal to `min(2, version)` when `fOverwintered = 0` and to
|
||||
/// > `version` otherwise.
|
||||
///
|
||||
/// Zebra handles the `fOverwintered` flag via the [`Self::is_overwintered`] method.
|
||||
///
|
||||
/// [§ 7.1 Transaction Encoding and Consensus]: <https://zips.z.cash/protocol/protocol.pdf#txnencoding>
|
||||
pub fn version(&self) -> u32 {
|
||||
match self {
|
||||
Transaction::V1 { .. } => 1,
|
||||
|
@ -429,32 +439,6 @@ impl Transaction {
|
|||
}
|
||||
}
|
||||
|
||||
/// Modify the expiry height of this transaction.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - if called on a v1 or v2 transaction
|
||||
#[cfg(any(test, feature = "proptest-impl"))]
|
||||
pub fn expiry_height_mut(&mut self) -> &mut block::Height {
|
||||
match self {
|
||||
Transaction::V1 { .. } | Transaction::V2 { .. } => {
|
||||
panic!("v1 and v2 transactions are not supported")
|
||||
}
|
||||
Transaction::V3 {
|
||||
ref mut expiry_height,
|
||||
..
|
||||
}
|
||||
| Transaction::V4 {
|
||||
ref mut expiry_height,
|
||||
..
|
||||
}
|
||||
| Transaction::V5 {
|
||||
ref mut expiry_height,
|
||||
..
|
||||
} => expiry_height,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get this transaction's network upgrade field, if any.
|
||||
/// This field is serialized as `nConsensusBranchId` ([7.1]).
|
||||
///
|
||||
|
@ -484,18 +468,6 @@ impl Transaction {
|
|||
}
|
||||
}
|
||||
|
||||
/// Modify the transparent inputs of this transaction, regardless of version.
|
||||
#[cfg(any(test, feature = "proptest-impl"))]
|
||||
pub fn inputs_mut(&mut self) -> &mut Vec<transparent::Input> {
|
||||
match self {
|
||||
Transaction::V1 { ref mut inputs, .. } => inputs,
|
||||
Transaction::V2 { ref mut inputs, .. } => inputs,
|
||||
Transaction::V3 { ref mut inputs, .. } => inputs,
|
||||
Transaction::V4 { ref mut inputs, .. } => inputs,
|
||||
Transaction::V5 { ref mut inputs, .. } => inputs,
|
||||
}
|
||||
}
|
||||
|
||||
/// Access the [`transparent::OutPoint`]s spent by this transaction's [`transparent::Input`]s.
|
||||
pub fn spent_outpoints(&self) -> impl Iterator<Item = transparent::OutPoint> + '_ {
|
||||
self.inputs()
|
||||
|
@ -514,28 +486,6 @@ impl Transaction {
|
|||
}
|
||||
}
|
||||
|
||||
/// Modify the transparent outputs of this transaction, regardless of version.
|
||||
#[cfg(any(test, feature = "proptest-impl"))]
|
||||
pub fn outputs_mut(&mut self) -> &mut Vec<transparent::Output> {
|
||||
match self {
|
||||
Transaction::V1 {
|
||||
ref mut outputs, ..
|
||||
} => outputs,
|
||||
Transaction::V2 {
|
||||
ref mut outputs, ..
|
||||
} => outputs,
|
||||
Transaction::V3 {
|
||||
ref mut outputs, ..
|
||||
} => outputs,
|
||||
Transaction::V4 {
|
||||
ref mut outputs, ..
|
||||
} => outputs,
|
||||
Transaction::V5 {
|
||||
ref mut outputs, ..
|
||||
} => outputs,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if this transaction has valid inputs for a coinbase
|
||||
/// transaction, that is, has a single input and it is a coinbase input
|
||||
/// (null prevout).
|
||||
|
@ -943,27 +893,6 @@ impl Transaction {
|
|||
}
|
||||
}
|
||||
|
||||
/// 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),
|
||||
|
||||
Transaction::V1 { .. }
|
||||
| Transaction::V2 { .. }
|
||||
| Transaction::V3 { .. }
|
||||
| Transaction::V4 { .. }
|
||||
| Transaction::V5 {
|
||||
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<Item = &orchard::Action> {
|
||||
|
@ -1035,14 +964,6 @@ impl Transaction {
|
|||
.map_err(ValueBalanceError::Transparent)
|
||||
}
|
||||
|
||||
/// Modify the transparent output values of this transaction, regardless of version.
|
||||
#[cfg(any(test, feature = "proptest-impl"))]
|
||||
pub fn output_values_mut(&mut self) -> impl Iterator<Item = &mut Amount<NonNegative>> {
|
||||
self.outputs_mut()
|
||||
.iter_mut()
|
||||
.map(|output| &mut output.value)
|
||||
}
|
||||
|
||||
/// Returns the `vpub_old` fields from `JoinSplit`s in this transaction,
|
||||
/// regardless of version, in the order they appear in the transaction.
|
||||
///
|
||||
|
@ -1090,55 +1011,6 @@ impl Transaction {
|
|||
}
|
||||
}
|
||||
|
||||
/// Modify the `vpub_old` fields from `JoinSplit`s in this transaction,
|
||||
/// regardless of version, in the order they appear in the transaction.
|
||||
///
|
||||
/// See `output_values_to_sprout` for details.
|
||||
#[cfg(any(test, feature = "proptest-impl"))]
|
||||
pub fn output_values_to_sprout_mut(
|
||||
&mut self,
|
||||
) -> Box<dyn Iterator<Item = &mut Amount<NonNegative>> + '_> {
|
||||
match self {
|
||||
// JoinSplits with Bctv14 Proofs
|
||||
Transaction::V2 {
|
||||
joinsplit_data: Some(joinsplit_data),
|
||||
..
|
||||
}
|
||||
| Transaction::V3 {
|
||||
joinsplit_data: Some(joinsplit_data),
|
||||
..
|
||||
} => Box::new(
|
||||
joinsplit_data
|
||||
.joinsplits_mut()
|
||||
.map(|joinsplit| &mut joinsplit.vpub_old),
|
||||
),
|
||||
// JoinSplits with Groth16 Proofs
|
||||
Transaction::V4 {
|
||||
joinsplit_data: Some(joinsplit_data),
|
||||
..
|
||||
} => Box::new(
|
||||
joinsplit_data
|
||||
.joinsplits_mut()
|
||||
.map(|joinsplit| &mut joinsplit.vpub_old),
|
||||
),
|
||||
// No JoinSplits
|
||||
Transaction::V1 { .. }
|
||||
| Transaction::V2 {
|
||||
joinsplit_data: None,
|
||||
..
|
||||
}
|
||||
| Transaction::V3 {
|
||||
joinsplit_data: None,
|
||||
..
|
||||
}
|
||||
| Transaction::V4 {
|
||||
joinsplit_data: None,
|
||||
..
|
||||
}
|
||||
| Transaction::V5 { .. } => Box::new(std::iter::empty()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the `vpub_new` fields from `JoinSplit`s in this transaction,
|
||||
/// regardless of version, in the order they appear in the transaction.
|
||||
///
|
||||
|
@ -1186,55 +1058,6 @@ impl Transaction {
|
|||
}
|
||||
}
|
||||
|
||||
/// Modify the `vpub_new` fields from `JoinSplit`s in this transaction,
|
||||
/// regardless of version, in the order they appear in the transaction.
|
||||
///
|
||||
/// See `input_values_from_sprout` for details.
|
||||
#[cfg(any(test, feature = "proptest-impl"))]
|
||||
pub fn input_values_from_sprout_mut(
|
||||
&mut self,
|
||||
) -> Box<dyn Iterator<Item = &mut Amount<NonNegative>> + '_> {
|
||||
match self {
|
||||
// JoinSplits with Bctv14 Proofs
|
||||
Transaction::V2 {
|
||||
joinsplit_data: Some(joinsplit_data),
|
||||
..
|
||||
}
|
||||
| Transaction::V3 {
|
||||
joinsplit_data: Some(joinsplit_data),
|
||||
..
|
||||
} => Box::new(
|
||||
joinsplit_data
|
||||
.joinsplits_mut()
|
||||
.map(|joinsplit| &mut joinsplit.vpub_new),
|
||||
),
|
||||
// JoinSplits with Groth Proofs
|
||||
Transaction::V4 {
|
||||
joinsplit_data: Some(joinsplit_data),
|
||||
..
|
||||
} => Box::new(
|
||||
joinsplit_data
|
||||
.joinsplits_mut()
|
||||
.map(|joinsplit| &mut joinsplit.vpub_new),
|
||||
),
|
||||
// No JoinSplits
|
||||
Transaction::V1 { .. }
|
||||
| Transaction::V2 {
|
||||
joinsplit_data: None,
|
||||
..
|
||||
}
|
||||
| Transaction::V3 {
|
||||
joinsplit_data: None,
|
||||
..
|
||||
}
|
||||
| Transaction::V4 {
|
||||
joinsplit_data: None,
|
||||
..
|
||||
}
|
||||
| Transaction::V5 { .. } => Box::new(std::iter::empty()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a list of sprout value balances,
|
||||
/// the changes in the transaction value pool due to each sprout `JoinSplit`.
|
||||
///
|
||||
|
@ -1331,35 +1154,6 @@ impl Transaction {
|
|||
ValueBalance::from_sapling_amount(sapling_value_balance)
|
||||
}
|
||||
|
||||
/// Modify the `value_balance` field from the `sapling::ShieldedData` in this transaction,
|
||||
/// regardless of version.
|
||||
///
|
||||
/// See `sapling_value_balance` for details.
|
||||
#[cfg(any(test, feature = "proptest-impl"))]
|
||||
pub fn sapling_value_balance_mut(&mut self) -> Option<&mut Amount<NegativeAllowed>> {
|
||||
match self {
|
||||
Transaction::V4 {
|
||||
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::V1 { .. }
|
||||
| Transaction::V2 { .. }
|
||||
| Transaction::V3 { .. }
|
||||
| Transaction::V4 {
|
||||
sapling_shielded_data: None,
|
||||
..
|
||||
}
|
||||
| Transaction::V5 {
|
||||
sapling_shielded_data: None,
|
||||
..
|
||||
} => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the orchard value balance, the change in the transaction value
|
||||
/// pool due to [`orchard::Action`]s.
|
||||
///
|
||||
|
@ -1380,16 +1174,6 @@ impl Transaction {
|
|||
ValueBalance::from_orchard_amount(orchard_value_balance)
|
||||
}
|
||||
|
||||
/// Modify the `value_balance` field from the `orchard::ShieldedData` in this transaction,
|
||||
/// regardless of version.
|
||||
///
|
||||
/// See `orchard_value_balance` for details.
|
||||
#[cfg(any(test, feature = "proptest-impl"))]
|
||||
pub fn orchard_value_balance_mut(&mut self) -> Option<&mut Amount<NegativeAllowed>> {
|
||||
self.orchard_shielded_data_mut()
|
||||
.map(|shielded_data| &mut shielded_data.value_balance)
|
||||
}
|
||||
|
||||
/// Returns the value balances for this transaction using the provided transparent outputs.
|
||||
pub(crate) fn value_balance_from_outputs(
|
||||
&self,
|
||||
|
@ -1428,3 +1212,246 @@ impl Transaction {
|
|||
self.value_balance_from_outputs(&outputs_from_utxos(utxos.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "proptest-impl"))]
|
||||
impl Transaction {
|
||||
/// Updates the [`NetworkUpgrade`] for this transaction.
|
||||
///
|
||||
/// ## Notes
|
||||
///
|
||||
/// - Updating the network upgrade for V1, V2, V3 and V4 transactions is not possible.
|
||||
pub fn update_network_upgrade(&mut self, nu: NetworkUpgrade) -> Result<(), &str> {
|
||||
match self {
|
||||
Transaction::V1 { .. }
|
||||
| Transaction::V2 { .. }
|
||||
| Transaction::V3 { .. }
|
||||
| Transaction::V4 { .. } => Err(
|
||||
"Updating the network upgrade for V1, V2, V3 and V4 transactions is not possible.",
|
||||
),
|
||||
Transaction::V5 {
|
||||
ref mut network_upgrade,
|
||||
..
|
||||
} => {
|
||||
*network_upgrade = nu;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Modify the expiry height of this transaction.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - if called on a v1 or v2 transaction
|
||||
pub fn expiry_height_mut(&mut self) -> &mut block::Height {
|
||||
match self {
|
||||
Transaction::V1 { .. } | Transaction::V2 { .. } => {
|
||||
panic!("v1 and v2 transactions are not supported")
|
||||
}
|
||||
Transaction::V3 {
|
||||
ref mut expiry_height,
|
||||
..
|
||||
}
|
||||
| Transaction::V4 {
|
||||
ref mut expiry_height,
|
||||
..
|
||||
}
|
||||
| Transaction::V5 {
|
||||
ref mut expiry_height,
|
||||
..
|
||||
} => expiry_height,
|
||||
}
|
||||
}
|
||||
|
||||
/// Modify the transparent inputs of this transaction, regardless of version.
|
||||
pub fn inputs_mut(&mut self) -> &mut Vec<transparent::Input> {
|
||||
match self {
|
||||
Transaction::V1 { ref mut inputs, .. } => inputs,
|
||||
Transaction::V2 { ref mut inputs, .. } => inputs,
|
||||
Transaction::V3 { ref mut inputs, .. } => inputs,
|
||||
Transaction::V4 { ref mut inputs, .. } => inputs,
|
||||
Transaction::V5 { ref mut inputs, .. } => inputs,
|
||||
}
|
||||
}
|
||||
|
||||
/// Modify the `value_balance` field from the `orchard::ShieldedData` in this transaction,
|
||||
/// regardless of version.
|
||||
///
|
||||
/// See `orchard_value_balance` for details.
|
||||
pub fn orchard_value_balance_mut(&mut self) -> Option<&mut Amount<NegativeAllowed>> {
|
||||
self.orchard_shielded_data_mut()
|
||||
.map(|shielded_data| &mut shielded_data.value_balance)
|
||||
}
|
||||
|
||||
/// Modify the `value_balance` field from the `sapling::ShieldedData` in this transaction,
|
||||
/// regardless of version.
|
||||
///
|
||||
/// See `sapling_value_balance` for details.
|
||||
pub fn sapling_value_balance_mut(&mut self) -> Option<&mut Amount<NegativeAllowed>> {
|
||||
match self {
|
||||
Transaction::V4 {
|
||||
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::V1 { .. }
|
||||
| Transaction::V2 { .. }
|
||||
| Transaction::V3 { .. }
|
||||
| Transaction::V4 {
|
||||
sapling_shielded_data: None,
|
||||
..
|
||||
}
|
||||
| Transaction::V5 {
|
||||
sapling_shielded_data: None,
|
||||
..
|
||||
} => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Modify the `vpub_new` fields from `JoinSplit`s in this transaction,
|
||||
/// regardless of version, in the order they appear in the transaction.
|
||||
///
|
||||
/// See `input_values_from_sprout` for details.
|
||||
pub fn input_values_from_sprout_mut(
|
||||
&mut self,
|
||||
) -> Box<dyn Iterator<Item = &mut Amount<NonNegative>> + '_> {
|
||||
match self {
|
||||
// JoinSplits with Bctv14 Proofs
|
||||
Transaction::V2 {
|
||||
joinsplit_data: Some(joinsplit_data),
|
||||
..
|
||||
}
|
||||
| Transaction::V3 {
|
||||
joinsplit_data: Some(joinsplit_data),
|
||||
..
|
||||
} => Box::new(
|
||||
joinsplit_data
|
||||
.joinsplits_mut()
|
||||
.map(|joinsplit| &mut joinsplit.vpub_new),
|
||||
),
|
||||
// JoinSplits with Groth Proofs
|
||||
Transaction::V4 {
|
||||
joinsplit_data: Some(joinsplit_data),
|
||||
..
|
||||
} => Box::new(
|
||||
joinsplit_data
|
||||
.joinsplits_mut()
|
||||
.map(|joinsplit| &mut joinsplit.vpub_new),
|
||||
),
|
||||
// No JoinSplits
|
||||
Transaction::V1 { .. }
|
||||
| Transaction::V2 {
|
||||
joinsplit_data: None,
|
||||
..
|
||||
}
|
||||
| Transaction::V3 {
|
||||
joinsplit_data: None,
|
||||
..
|
||||
}
|
||||
| Transaction::V4 {
|
||||
joinsplit_data: None,
|
||||
..
|
||||
}
|
||||
| Transaction::V5 { .. } => Box::new(std::iter::empty()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Modify the `vpub_old` fields from `JoinSplit`s in this transaction,
|
||||
/// regardless of version, in the order they appear in the transaction.
|
||||
///
|
||||
/// See `output_values_to_sprout` for details.
|
||||
pub fn output_values_to_sprout_mut(
|
||||
&mut self,
|
||||
) -> Box<dyn Iterator<Item = &mut Amount<NonNegative>> + '_> {
|
||||
match self {
|
||||
// JoinSplits with Bctv14 Proofs
|
||||
Transaction::V2 {
|
||||
joinsplit_data: Some(joinsplit_data),
|
||||
..
|
||||
}
|
||||
| Transaction::V3 {
|
||||
joinsplit_data: Some(joinsplit_data),
|
||||
..
|
||||
} => Box::new(
|
||||
joinsplit_data
|
||||
.joinsplits_mut()
|
||||
.map(|joinsplit| &mut joinsplit.vpub_old),
|
||||
),
|
||||
// JoinSplits with Groth16 Proofs
|
||||
Transaction::V4 {
|
||||
joinsplit_data: Some(joinsplit_data),
|
||||
..
|
||||
} => Box::new(
|
||||
joinsplit_data
|
||||
.joinsplits_mut()
|
||||
.map(|joinsplit| &mut joinsplit.vpub_old),
|
||||
),
|
||||
// No JoinSplits
|
||||
Transaction::V1 { .. }
|
||||
| Transaction::V2 {
|
||||
joinsplit_data: None,
|
||||
..
|
||||
}
|
||||
| Transaction::V3 {
|
||||
joinsplit_data: None,
|
||||
..
|
||||
}
|
||||
| Transaction::V4 {
|
||||
joinsplit_data: None,
|
||||
..
|
||||
}
|
||||
| Transaction::V5 { .. } => Box::new(std::iter::empty()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Modify the transparent output values of this transaction, regardless of version.
|
||||
pub fn output_values_mut(&mut self) -> impl Iterator<Item = &mut Amount<NonNegative>> {
|
||||
self.outputs_mut()
|
||||
.iter_mut()
|
||||
.map(|output| &mut output.value)
|
||||
}
|
||||
|
||||
/// Modify the [`orchard::ShieldedData`] in this transaction,
|
||||
/// regardless of version.
|
||||
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),
|
||||
|
||||
Transaction::V1 { .. }
|
||||
| Transaction::V2 { .. }
|
||||
| Transaction::V3 { .. }
|
||||
| Transaction::V4 { .. }
|
||||
| Transaction::V5 {
|
||||
orchard_shielded_data: None,
|
||||
..
|
||||
} => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Modify the transparent outputs of this transaction, regardless of version.
|
||||
pub fn outputs_mut(&mut self) -> &mut Vec<transparent::Output> {
|
||||
match self {
|
||||
Transaction::V1 {
|
||||
ref mut outputs, ..
|
||||
} => outputs,
|
||||
Transaction::V2 {
|
||||
ref mut outputs, ..
|
||||
} => outputs,
|
||||
Transaction::V3 {
|
||||
ref mut outputs, ..
|
||||
} => outputs,
|
||||
Transaction::V4 {
|
||||
ref mut outputs, ..
|
||||
} => outputs,
|
||||
Transaction::V5 {
|
||||
ref mut outputs, ..
|
||||
} => outputs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -241,6 +241,12 @@ pub enum TransactionError {
|
|||
)]
|
||||
#[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))]
|
||||
Zip317(#[from] zebra_chain::transaction::zip317::Error),
|
||||
|
||||
#[error("transaction uses an incorrect consensus branch id")]
|
||||
WrongConsensusBranchId,
|
||||
|
||||
#[error("wrong tx format: tx version is ≥ 5, but `nConsensusBranchId` is missing")]
|
||||
MissingConsensusBranchId,
|
||||
}
|
||||
|
||||
impl From<ValidateContextError> for TransactionError {
|
||||
|
|
|
@ -380,6 +380,7 @@ where
|
|||
// Do quick checks first
|
||||
check::has_inputs_and_outputs(&tx)?;
|
||||
check::has_enough_orchard_flags(&tx)?;
|
||||
check::consensus_branch_id(&tx, req.height(), &network)?;
|
||||
|
||||
// Validate the coinbase input consensus rules
|
||||
if req.is_mempool() && tx.is_coinbase() {
|
||||
|
|
|
@ -495,3 +495,44 @@ pub fn tx_transparent_coinbase_spends_maturity(
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks the `nConsensusBranchId` field.
|
||||
///
|
||||
/// # Consensus
|
||||
///
|
||||
/// ## [7.1.2 Transaction Consensus Rules]
|
||||
///
|
||||
/// > [**NU5** onward] If `effectiveVersion` ≥ 5, the `nConsensusBranchId` field **MUST** match the
|
||||
/// > consensus branch ID used for SIGHASH transaction hashes, as specified in [ZIP-244].
|
||||
///
|
||||
/// ### Notes
|
||||
///
|
||||
/// - When deserializing transactions, Zebra converts the `nConsensusBranchId` into
|
||||
/// [`NetworkUpgrade`].
|
||||
///
|
||||
/// - The values returned by [`Transaction::version`] match `effectiveVersion` so we use them in
|
||||
/// place of `effectiveVersion`. More details in [`Transaction::version`].
|
||||
///
|
||||
/// [ZIP-244]: <https://zips.z.cash/zip-0244>
|
||||
/// [7.1.2 Transaction Consensus Rules]: <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
|
||||
pub fn consensus_branch_id(
|
||||
tx: &Transaction,
|
||||
height: Height,
|
||||
network: &Network,
|
||||
) -> Result<(), TransactionError> {
|
||||
let current_nu = NetworkUpgrade::current(network, height);
|
||||
|
||||
if current_nu < NetworkUpgrade::Nu5 || tx.version() < 5 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(tx_nu) = tx.network_upgrade() else {
|
||||
return Err(TransactionError::MissingConsensusBranchId);
|
||||
};
|
||||
|
||||
if tx_nu != current_nu {
|
||||
return Err(TransactionError::WrongConsensusBranchId);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ use std::{collections::HashMap, sync::Arc};
|
|||
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use color_eyre::eyre::Report;
|
||||
use futures::{FutureExt, TryFutureExt};
|
||||
use halo2::pasta::{group::ff::PrimeField, pallas};
|
||||
use tower::{buffer::Buffer, service_fn, ServiceExt};
|
||||
|
||||
|
@ -1002,58 +1003,47 @@ async fn v5_transaction_is_rejected_before_nu5_activation() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn v5_transaction_is_accepted_after_nu5_activation_mainnet() {
|
||||
v5_transaction_is_accepted_after_nu5_activation_for_network(Network::Mainnet)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v5_transaction_is_accepted_after_nu5_activation_testnet() {
|
||||
v5_transaction_is_accepted_after_nu5_activation_for_network(Network::new_default_testnet())
|
||||
}
|
||||
|
||||
fn v5_transaction_is_accepted_after_nu5_activation_for_network(network: Network) {
|
||||
fn v5_transaction_is_accepted_after_nu5_activation() {
|
||||
let _init_guard = zebra_test::init();
|
||||
zebra_test::MULTI_THREADED_RUNTIME.block_on(async {
|
||||
let nu5 = NetworkUpgrade::Nu5;
|
||||
let nu5_activation_height = nu5
|
||||
.activation_height(&network)
|
||||
.expect("NU5 activation height is specified");
|
||||
let blocks = network.block_iter();
|
||||
|
||||
let state_service = service_fn(|_| async { unreachable!("Service should not be called") });
|
||||
let verifier = Verifier::new_for_tests(&network, state_service);
|
||||
for network in Network::iter() {
|
||||
zebra_test::MULTI_THREADED_RUNTIME.block_on(async {
|
||||
let nu5_activation_height = NetworkUpgrade::Nu5
|
||||
.activation_height(&network)
|
||||
.expect("NU5 activation height is specified");
|
||||
|
||||
let mut transaction = fake_v5_transactions_for_network(&network, blocks)
|
||||
.next_back()
|
||||
.expect("At least one fake V5 transaction in the test vectors");
|
||||
if transaction
|
||||
.expiry_height()
|
||||
.expect("V5 must have expiry_height")
|
||||
< nu5_activation_height
|
||||
{
|
||||
let expiry_height = transaction.expiry_height_mut();
|
||||
*expiry_height = nu5_activation_height;
|
||||
}
|
||||
let state = service_fn(|_| async { unreachable!("Service should not be called") });
|
||||
|
||||
let expected_hash = transaction.unmined_id();
|
||||
let expiry_height = transaction
|
||||
.expiry_height()
|
||||
.expect("V5 must have expiry_height");
|
||||
let mut tx = fake_v5_transactions_for_network(&network, network.block_iter())
|
||||
.next_back()
|
||||
.expect("At least one fake V5 transaction in the test vectors");
|
||||
|
||||
let result = verifier
|
||||
.oneshot(Request::Block {
|
||||
transaction: Arc::new(transaction),
|
||||
known_utxos: Arc::new(HashMap::new()),
|
||||
height: expiry_height,
|
||||
time: DateTime::<Utc>::MAX_UTC,
|
||||
})
|
||||
.await;
|
||||
if tx.expiry_height().expect("V5 must have expiry_height") < nu5_activation_height {
|
||||
*tx.expiry_height_mut() = nu5_activation_height;
|
||||
tx.update_network_upgrade(NetworkUpgrade::Nu5)
|
||||
.expect("updating the network upgrade for a V5 tx should succeed");
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
result.expect("unexpected error response").tx_id(),
|
||||
expected_hash
|
||||
);
|
||||
})
|
||||
let expected_hash = tx.unmined_id();
|
||||
let expiry_height = tx.expiry_height().expect("V5 must have expiry_height");
|
||||
|
||||
let verification_result = Verifier::new_for_tests(&network, state)
|
||||
.oneshot(Request::Block {
|
||||
transaction: Arc::new(tx),
|
||||
known_utxos: Arc::new(HashMap::new()),
|
||||
height: expiry_height,
|
||||
time: DateTime::<Utc>::MAX_UTC,
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
verification_result
|
||||
.expect("successful verification")
|
||||
.tx_id(),
|
||||
expected_hash
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Test if V4 transaction with transparent funds is accepted.
|
||||
|
@ -1872,7 +1862,13 @@ async fn v5_coinbase_transaction_expiry_height() {
|
|||
|
||||
*new_transaction.expiry_height_mut() = new_expiry_height;
|
||||
|
||||
let result = verifier
|
||||
// Setting the new expiry height as the block height will activate NU6, so we need to set NU6
|
||||
// for the tx as well.
|
||||
new_transaction
|
||||
.update_network_upgrade(NetworkUpgrade::Nu6)
|
||||
.expect("updating the network upgrade for a V5 tx should succeed");
|
||||
|
||||
let verification_result = verifier
|
||||
.clone()
|
||||
.oneshot(Request::Block {
|
||||
transaction: Arc::new(new_transaction.clone()),
|
||||
|
@ -1883,7 +1879,9 @@ async fn v5_coinbase_transaction_expiry_height() {
|
|||
.await;
|
||||
|
||||
assert_eq!(
|
||||
result.expect("unexpected error response").tx_id(),
|
||||
verification_result
|
||||
.expect("successful verification")
|
||||
.tx_id(),
|
||||
new_transaction.unmined_id()
|
||||
);
|
||||
}
|
||||
|
@ -1941,22 +1939,18 @@ async fn v5_transaction_with_too_low_expiry_height() {
|
|||
);
|
||||
}
|
||||
|
||||
/// Tests if a non-coinbase V5 transaction with an expiry height exceeding the
|
||||
/// maximum is rejected.
|
||||
/// Tests if a non-coinbase V5 transaction with an expiry height exceeding the maximum is rejected.
|
||||
#[tokio::test]
|
||||
async fn v5_transaction_with_exceeding_expiry_height() {
|
||||
let state_service =
|
||||
service_fn(|_| async { unreachable!("State service should not be called") });
|
||||
let verifier = Verifier::new_for_tests(&Network::Mainnet, state_service);
|
||||
let state = service_fn(|_| async { unreachable!("State service should not be called") });
|
||||
|
||||
let block_height = block::Height::MAX;
|
||||
let height_max = block::Height::MAX;
|
||||
|
||||
let fund_height = (block_height - 1).expect("fake source fund block height is too small");
|
||||
let (input, output, known_utxos) = mock_transparent_transfer(
|
||||
fund_height,
|
||||
height_max.previous().expect("valid height"),
|
||||
true,
|
||||
0,
|
||||
Amount::try_from(1).expect("invalid value"),
|
||||
Amount::try_from(1).expect("valid amount"),
|
||||
);
|
||||
|
||||
// This expiry height exceeds the maximum defined by the specification.
|
||||
|
@ -1970,25 +1964,27 @@ async fn v5_transaction_with_exceeding_expiry_height() {
|
|||
expiry_height,
|
||||
sapling_shielded_data: None,
|
||||
orchard_shielded_data: None,
|
||||
network_upgrade: NetworkUpgrade::Nu5,
|
||||
network_upgrade: NetworkUpgrade::Nu6,
|
||||
};
|
||||
|
||||
let result = verifier
|
||||
let transaction_hash = transaction.hash();
|
||||
|
||||
let verification_result = Verifier::new_for_tests(&Network::Mainnet, state)
|
||||
.oneshot(Request::Block {
|
||||
transaction: Arc::new(transaction.clone()),
|
||||
transaction: Arc::new(transaction),
|
||||
known_utxos: Arc::new(known_utxos),
|
||||
height: block_height,
|
||||
height: height_max,
|
||||
time: DateTime::<Utc>::MAX_UTC,
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
verification_result,
|
||||
Err(TransactionError::MaximumExpiryHeight {
|
||||
expiry_height,
|
||||
is_coinbase: false,
|
||||
block_height,
|
||||
transaction_hash: transaction.hash(),
|
||||
block_height: height_max,
|
||||
transaction_hash,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -2105,59 +2101,49 @@ async fn v5_transaction_with_transparent_transfer_is_rejected_by_the_script() {
|
|||
/// Test if V5 transaction with an internal double spend of transparent funds is rejected.
|
||||
#[tokio::test]
|
||||
async fn v5_transaction_with_conflicting_transparent_spend_is_rejected() {
|
||||
let network = Network::Mainnet;
|
||||
let network_upgrade = NetworkUpgrade::Nu5;
|
||||
for network in Network::iter() {
|
||||
let canopy_activation_height = NetworkUpgrade::Canopy
|
||||
.activation_height(&network)
|
||||
.expect("Canopy activation height is specified");
|
||||
|
||||
let canopy_activation_height = NetworkUpgrade::Canopy
|
||||
.activation_height(&network)
|
||||
.expect("Canopy activation height is specified");
|
||||
let height = (canopy_activation_height + 10).expect("valid height");
|
||||
|
||||
let transaction_block_height =
|
||||
(canopy_activation_height + 10).expect("transaction block height is too large");
|
||||
// Create a fake transparent transfer that should succeed
|
||||
let (input, output, known_utxos) = mock_transparent_transfer(
|
||||
height.previous().expect("valid height"),
|
||||
true,
|
||||
0,
|
||||
Amount::try_from(1).expect("valid amount"),
|
||||
);
|
||||
|
||||
let fake_source_fund_height =
|
||||
(transaction_block_height - 1).expect("fake source fund block height is too small");
|
||||
let transaction = Transaction::V5 {
|
||||
inputs: vec![input.clone(), input.clone()],
|
||||
outputs: vec![output],
|
||||
lock_time: LockTime::Height(block::Height(0)),
|
||||
expiry_height: height.next().expect("valid height"),
|
||||
sapling_shielded_data: None,
|
||||
orchard_shielded_data: None,
|
||||
network_upgrade: NetworkUpgrade::Canopy,
|
||||
};
|
||||
|
||||
// Create a fake transparent transfer that should succeed
|
||||
let (input, output, known_utxos) = mock_transparent_transfer(
|
||||
fake_source_fund_height,
|
||||
true,
|
||||
0,
|
||||
Amount::try_from(1).expect("invalid value"),
|
||||
);
|
||||
let state = service_fn(|_| async { unreachable!("State service should not be called") });
|
||||
|
||||
// Create a V4 transaction
|
||||
let transaction = Transaction::V5 {
|
||||
inputs: vec![input.clone(), input.clone()],
|
||||
outputs: vec![output],
|
||||
lock_time: LockTime::Height(block::Height(0)),
|
||||
expiry_height: (transaction_block_height + 1).expect("expiry height is too large"),
|
||||
sapling_shielded_data: None,
|
||||
orchard_shielded_data: None,
|
||||
network_upgrade,
|
||||
};
|
||||
let verification_result = Verifier::new_for_tests(&network, state)
|
||||
.oneshot(Request::Block {
|
||||
transaction: Arc::new(transaction),
|
||||
known_utxos: Arc::new(known_utxos),
|
||||
height,
|
||||
time: DateTime::<Utc>::MAX_UTC,
|
||||
})
|
||||
.await;
|
||||
|
||||
let state_service =
|
||||
service_fn(|_| async { unreachable!("State service should not be called") });
|
||||
let verifier = Verifier::new_for_tests(&network, state_service);
|
||||
|
||||
let result = verifier
|
||||
.oneshot(Request::Block {
|
||||
transaction: Arc::new(transaction),
|
||||
known_utxos: Arc::new(known_utxos),
|
||||
height: transaction_block_height,
|
||||
time: DateTime::<Utc>::MAX_UTC,
|
||||
})
|
||||
.await;
|
||||
|
||||
let expected_outpoint = input.outpoint().expect("Input should have an outpoint");
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(TransactionError::DuplicateTransparentSpend(
|
||||
expected_outpoint
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
verification_result,
|
||||
Err(TransactionError::DuplicateTransparentSpend(
|
||||
input.outpoint().expect("Input should have an outpoint")
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test if signed V4 transaction with a dummy [`sprout::JoinSplit`] is accepted.
|
||||
|
@ -2577,6 +2563,161 @@ fn v5_with_duplicate_orchard_action() {
|
|||
});
|
||||
}
|
||||
|
||||
/// Checks that the tx verifier handles consensus branch ids in V5 txs correctly.
|
||||
#[tokio::test]
|
||||
async fn v5_consensus_branch_ids() {
|
||||
let mut state = MockService::build().for_unit_tests();
|
||||
|
||||
let (input, output, known_utxos) = mock_transparent_transfer(
|
||||
Height(1),
|
||||
true,
|
||||
0,
|
||||
Amount::try_from(10001).expect("valid amount"),
|
||||
);
|
||||
|
||||
let known_utxos = Arc::new(known_utxos);
|
||||
|
||||
// NU5 is the first network upgrade that supports V5 txs.
|
||||
let mut network_upgrade = NetworkUpgrade::Nu5;
|
||||
|
||||
let mut tx = Transaction::V5 {
|
||||
inputs: vec![input],
|
||||
outputs: vec![output],
|
||||
lock_time: LockTime::unlocked(),
|
||||
expiry_height: Height::MAX_EXPIRY_HEIGHT,
|
||||
sapling_shielded_data: None,
|
||||
orchard_shielded_data: None,
|
||||
network_upgrade,
|
||||
};
|
||||
|
||||
let outpoint = match tx.inputs()[0] {
|
||||
transparent::Input::PrevOut { outpoint, .. } => outpoint,
|
||||
transparent::Input::Coinbase { .. } => panic!("requires a non-coinbase transaction"),
|
||||
};
|
||||
|
||||
for network in Network::iter() {
|
||||
let verifier = Buffer::new(Verifier::new_for_tests(&network, state.clone()), 10);
|
||||
|
||||
while let Some(next_nu) = network_upgrade.next_upgrade() {
|
||||
// Check an outdated network upgrade.
|
||||
let height = next_nu.activation_height(&network).expect("height");
|
||||
|
||||
let block_req = verifier
|
||||
.clone()
|
||||
.oneshot(Request::Block {
|
||||
transaction: Arc::new(tx.clone()),
|
||||
known_utxos: known_utxos.clone(),
|
||||
// The consensus branch ID of the tx is outdated for this height.
|
||||
height,
|
||||
time: DateTime::<Utc>::MAX_UTC,
|
||||
})
|
||||
.map_err(|err| *err.downcast().expect("`TransactionError` type"));
|
||||
|
||||
let mempool_req = verifier
|
||||
.clone()
|
||||
.oneshot(Request::Mempool {
|
||||
transaction: tx.clone().into(),
|
||||
// The consensus branch ID of the tx is outdated for this height.
|
||||
height,
|
||||
})
|
||||
.map_err(|err| *err.downcast().expect("`TransactionError` type"));
|
||||
|
||||
let (block_rsp, mempool_rsp) = futures::join!(block_req, mempool_req);
|
||||
|
||||
assert_eq!(block_rsp, Err(TransactionError::WrongConsensusBranchId));
|
||||
assert_eq!(mempool_rsp, Err(TransactionError::WrongConsensusBranchId));
|
||||
|
||||
// Check the currently supported network upgrade.
|
||||
let height = network_upgrade.activation_height(&network).expect("height");
|
||||
|
||||
let block_req = verifier
|
||||
.clone()
|
||||
.oneshot(Request::Block {
|
||||
transaction: Arc::new(tx.clone()),
|
||||
known_utxos: known_utxos.clone(),
|
||||
// The consensus branch ID of the tx is supported by this height.
|
||||
height,
|
||||
time: DateTime::<Utc>::MAX_UTC,
|
||||
})
|
||||
.map_ok(|rsp| rsp.tx_id())
|
||||
.map_err(|e| format!("{e}"));
|
||||
|
||||
let mempool_req = verifier
|
||||
.clone()
|
||||
.oneshot(Request::Mempool {
|
||||
transaction: tx.clone().into(),
|
||||
// The consensus branch ID of the tx is supported by this height.
|
||||
height,
|
||||
})
|
||||
.map_ok(|rsp| rsp.tx_id())
|
||||
.map_err(|e| format!("{e}"));
|
||||
|
||||
let state_req = async {
|
||||
state
|
||||
.expect_request(zebra_state::Request::UnspentBestChainUtxo(outpoint))
|
||||
.map(|r| {
|
||||
r.respond(zebra_state::Response::UnspentBestChainUtxo(
|
||||
known_utxos.get(&outpoint).map(|utxo| utxo.utxo.clone()),
|
||||
))
|
||||
})
|
||||
.await;
|
||||
|
||||
state
|
||||
.expect_request_that(|req| {
|
||||
matches!(
|
||||
req,
|
||||
zebra_state::Request::CheckBestChainTipNullifiersAndAnchors(_)
|
||||
)
|
||||
})
|
||||
.map(|r| {
|
||||
r.respond(zebra_state::Response::ValidBestChainTipNullifiersAndAnchors)
|
||||
})
|
||||
.await;
|
||||
};
|
||||
|
||||
let (block_rsp, mempool_rsp, _) = futures::join!(block_req, mempool_req, state_req);
|
||||
let txid = tx.unmined_id();
|
||||
|
||||
assert_eq!(block_rsp, Ok(txid));
|
||||
assert_eq!(mempool_rsp, Ok(txid));
|
||||
|
||||
// Check a network upgrade that Zebra doesn't support yet.
|
||||
tx.update_network_upgrade(next_nu)
|
||||
.expect("V5 txs support updating NUs");
|
||||
|
||||
let height = network_upgrade.activation_height(&network).expect("height");
|
||||
|
||||
let block_req = verifier
|
||||
.clone()
|
||||
.oneshot(Request::Block {
|
||||
transaction: Arc::new(tx.clone()),
|
||||
known_utxos: known_utxos.clone(),
|
||||
// The consensus branch ID of the tx is not supported by this height.
|
||||
height,
|
||||
time: DateTime::<Utc>::MAX_UTC,
|
||||
})
|
||||
.map_err(|err| *err.downcast().expect("`TransactionError` type"));
|
||||
|
||||
let mempool_req = verifier
|
||||
.clone()
|
||||
.oneshot(Request::Mempool {
|
||||
transaction: tx.clone().into(),
|
||||
// The consensus branch ID of the tx is not supported by this height.
|
||||
height,
|
||||
})
|
||||
.map_err(|err| *err.downcast().expect("`TransactionError` type"));
|
||||
|
||||
let (block_rsp, mempool_rsp) = futures::join!(block_req, mempool_req);
|
||||
|
||||
assert_eq!(block_rsp, Err(TransactionError::WrongConsensusBranchId));
|
||||
assert_eq!(mempool_rsp, Err(TransactionError::WrongConsensusBranchId));
|
||||
|
||||
// Shift the network upgrade for the next loop iteration.
|
||||
network_upgrade = next_nu;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
|
||||
/// Create a mock transparent transfer to be included in a transaction.
|
||||
|
|
Loading…
Reference in New Issue