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 {
|
pub fn version(&self) -> u32 {
|
||||||
match self {
|
match self {
|
||||||
Transaction::V1 { .. } => 1,
|
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.
|
/// Get this transaction's network upgrade field, if any.
|
||||||
/// This field is serialized as `nConsensusBranchId` ([7.1]).
|
/// 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.
|
/// Access the [`transparent::OutPoint`]s spent by this transaction's [`transparent::Input`]s.
|
||||||
pub fn spent_outpoints(&self) -> impl Iterator<Item = transparent::OutPoint> + '_ {
|
pub fn spent_outpoints(&self) -> impl Iterator<Item = transparent::OutPoint> + '_ {
|
||||||
self.inputs()
|
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
|
/// Returns `true` if this transaction has valid inputs for a coinbase
|
||||||
/// transaction, that is, has a single input and it is a coinbase input
|
/// transaction, that is, has a single input and it is a coinbase input
|
||||||
/// (null prevout).
|
/// (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,
|
/// Iterate over the [`orchard::Action`]s in this transaction, if there are any,
|
||||||
/// regardless of version.
|
/// regardless of version.
|
||||||
pub fn orchard_actions(&self) -> impl Iterator<Item = &orchard::Action> {
|
pub fn orchard_actions(&self) -> impl Iterator<Item = &orchard::Action> {
|
||||||
|
@ -1035,14 +964,6 @@ impl Transaction {
|
||||||
.map_err(ValueBalanceError::Transparent)
|
.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,
|
/// Returns the `vpub_old` fields from `JoinSplit`s in this transaction,
|
||||||
/// regardless of version, in the order they appear in the 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,
|
/// Returns the `vpub_new` fields from `JoinSplit`s in this transaction,
|
||||||
/// regardless of version, in the order they appear in the 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,
|
/// Return a list of sprout value balances,
|
||||||
/// the changes in the transaction value pool due to each sprout `JoinSplit`.
|
/// 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)
|
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
|
/// Return the orchard value balance, the change in the transaction value
|
||||||
/// pool due to [`orchard::Action`]s.
|
/// pool due to [`orchard::Action`]s.
|
||||||
///
|
///
|
||||||
|
@ -1380,16 +1174,6 @@ impl Transaction {
|
||||||
ValueBalance::from_orchard_amount(orchard_value_balance)
|
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.
|
/// Returns the value balances for this transaction using the provided transparent outputs.
|
||||||
pub(crate) fn value_balance_from_outputs(
|
pub(crate) fn value_balance_from_outputs(
|
||||||
&self,
|
&self,
|
||||||
|
@ -1428,3 +1212,246 @@ impl Transaction {
|
||||||
self.value_balance_from_outputs(&outputs_from_utxos(utxos.clone()))
|
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))]
|
#[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))]
|
||||||
Zip317(#[from] zebra_chain::transaction::zip317::Error),
|
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 {
|
impl From<ValidateContextError> for TransactionError {
|
||||||
|
|
|
@ -380,6 +380,7 @@ where
|
||||||
// Do quick checks first
|
// Do quick checks first
|
||||||
check::has_inputs_and_outputs(&tx)?;
|
check::has_inputs_and_outputs(&tx)?;
|
||||||
check::has_enough_orchard_flags(&tx)?;
|
check::has_enough_orchard_flags(&tx)?;
|
||||||
|
check::consensus_branch_id(&tx, req.height(), &network)?;
|
||||||
|
|
||||||
// Validate the coinbase input consensus rules
|
// Validate the coinbase input consensus rules
|
||||||
if req.is_mempool() && tx.is_coinbase() {
|
if req.is_mempool() && tx.is_coinbase() {
|
||||||
|
|
|
@ -495,3 +495,44 @@ pub fn tx_transparent_coinbase_spends_maturity(
|
||||||
|
|
||||||
Ok(())
|
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 chrono::{DateTime, TimeZone, Utc};
|
||||||
use color_eyre::eyre::Report;
|
use color_eyre::eyre::Report;
|
||||||
|
use futures::{FutureExt, TryFutureExt};
|
||||||
use halo2::pasta::{group::ff::PrimeField, pallas};
|
use halo2::pasta::{group::ff::PrimeField, pallas};
|
||||||
use tower::{buffer::Buffer, service_fn, ServiceExt};
|
use tower::{buffer::Buffer, service_fn, ServiceExt};
|
||||||
|
|
||||||
|
@ -1002,58 +1003,47 @@ async fn v5_transaction_is_rejected_before_nu5_activation() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn v5_transaction_is_accepted_after_nu5_activation_mainnet() {
|
fn v5_transaction_is_accepted_after_nu5_activation() {
|
||||||
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) {
|
|
||||||
let _init_guard = zebra_test::init();
|
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") });
|
for network in Network::iter() {
|
||||||
let verifier = Verifier::new_for_tests(&network, state_service);
|
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)
|
let state = service_fn(|_| async { unreachable!("Service should not be called") });
|
||||||
.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 expected_hash = transaction.unmined_id();
|
let mut tx = fake_v5_transactions_for_network(&network, network.block_iter())
|
||||||
let expiry_height = transaction
|
.next_back()
|
||||||
.expiry_height()
|
.expect("At least one fake V5 transaction in the test vectors");
|
||||||
.expect("V5 must have expiry_height");
|
|
||||||
|
|
||||||
let result = verifier
|
if tx.expiry_height().expect("V5 must have expiry_height") < nu5_activation_height {
|
||||||
.oneshot(Request::Block {
|
*tx.expiry_height_mut() = nu5_activation_height;
|
||||||
transaction: Arc::new(transaction),
|
tx.update_network_upgrade(NetworkUpgrade::Nu5)
|
||||||
known_utxos: Arc::new(HashMap::new()),
|
.expect("updating the network upgrade for a V5 tx should succeed");
|
||||||
height: expiry_height,
|
}
|
||||||
time: DateTime::<Utc>::MAX_UTC,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert_eq!(
|
let expected_hash = tx.unmined_id();
|
||||||
result.expect("unexpected error response").tx_id(),
|
let expiry_height = tx.expiry_height().expect("V5 must have expiry_height");
|
||||||
expected_hash
|
|
||||||
);
|
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.
|
/// 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;
|
*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()
|
.clone()
|
||||||
.oneshot(Request::Block {
|
.oneshot(Request::Block {
|
||||||
transaction: Arc::new(new_transaction.clone()),
|
transaction: Arc::new(new_transaction.clone()),
|
||||||
|
@ -1883,7 +1879,9 @@ async fn v5_coinbase_transaction_expiry_height() {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.expect("unexpected error response").tx_id(),
|
verification_result
|
||||||
|
.expect("successful verification")
|
||||||
|
.tx_id(),
|
||||||
new_transaction.unmined_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
|
/// Tests if a non-coinbase V5 transaction with an expiry height exceeding the maximum is rejected.
|
||||||
/// maximum is rejected.
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn v5_transaction_with_exceeding_expiry_height() {
|
async fn v5_transaction_with_exceeding_expiry_height() {
|
||||||
let state_service =
|
let state = service_fn(|_| async { unreachable!("State service should not be called") });
|
||||||
service_fn(|_| async { unreachable!("State service should not be called") });
|
|
||||||
let verifier = Verifier::new_for_tests(&Network::Mainnet, state_service);
|
|
||||||
|
|
||||||
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(
|
let (input, output, known_utxos) = mock_transparent_transfer(
|
||||||
fund_height,
|
height_max.previous().expect("valid height"),
|
||||||
true,
|
true,
|
||||||
0,
|
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.
|
// This expiry height exceeds the maximum defined by the specification.
|
||||||
|
@ -1970,25 +1964,27 @@ async fn v5_transaction_with_exceeding_expiry_height() {
|
||||||
expiry_height,
|
expiry_height,
|
||||||
sapling_shielded_data: None,
|
sapling_shielded_data: None,
|
||||||
orchard_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 {
|
.oneshot(Request::Block {
|
||||||
transaction: Arc::new(transaction.clone()),
|
transaction: Arc::new(transaction),
|
||||||
known_utxos: Arc::new(known_utxos),
|
known_utxos: Arc::new(known_utxos),
|
||||||
height: block_height,
|
height: height_max,
|
||||||
time: DateTime::<Utc>::MAX_UTC,
|
time: DateTime::<Utc>::MAX_UTC,
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result,
|
verification_result,
|
||||||
Err(TransactionError::MaximumExpiryHeight {
|
Err(TransactionError::MaximumExpiryHeight {
|
||||||
expiry_height,
|
expiry_height,
|
||||||
is_coinbase: false,
|
is_coinbase: false,
|
||||||
block_height,
|
block_height: height_max,
|
||||||
transaction_hash: transaction.hash(),
|
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.
|
/// Test if V5 transaction with an internal double spend of transparent funds is rejected.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn v5_transaction_with_conflicting_transparent_spend_is_rejected() {
|
async fn v5_transaction_with_conflicting_transparent_spend_is_rejected() {
|
||||||
let network = Network::Mainnet;
|
for network in Network::iter() {
|
||||||
let network_upgrade = NetworkUpgrade::Nu5;
|
let canopy_activation_height = NetworkUpgrade::Canopy
|
||||||
|
.activation_height(&network)
|
||||||
|
.expect("Canopy activation height is specified");
|
||||||
|
|
||||||
let canopy_activation_height = NetworkUpgrade::Canopy
|
let height = (canopy_activation_height + 10).expect("valid height");
|
||||||
.activation_height(&network)
|
|
||||||
.expect("Canopy activation height is specified");
|
|
||||||
|
|
||||||
let transaction_block_height =
|
// Create a fake transparent transfer that should succeed
|
||||||
(canopy_activation_height + 10).expect("transaction block height is too large");
|
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 =
|
let transaction = Transaction::V5 {
|
||||||
(transaction_block_height - 1).expect("fake source fund block height is too small");
|
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 state = service_fn(|_| async { unreachable!("State service should not be called") });
|
||||||
let (input, output, known_utxos) = mock_transparent_transfer(
|
|
||||||
fake_source_fund_height,
|
|
||||||
true,
|
|
||||||
0,
|
|
||||||
Amount::try_from(1).expect("invalid value"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create a V4 transaction
|
let verification_result = Verifier::new_for_tests(&network, state)
|
||||||
let transaction = Transaction::V5 {
|
.oneshot(Request::Block {
|
||||||
inputs: vec![input.clone(), input.clone()],
|
transaction: Arc::new(transaction),
|
||||||
outputs: vec![output],
|
known_utxos: Arc::new(known_utxos),
|
||||||
lock_time: LockTime::Height(block::Height(0)),
|
height,
|
||||||
expiry_height: (transaction_block_height + 1).expect("expiry height is too large"),
|
time: DateTime::<Utc>::MAX_UTC,
|
||||||
sapling_shielded_data: None,
|
})
|
||||||
orchard_shielded_data: None,
|
.await;
|
||||||
network_upgrade,
|
|
||||||
};
|
|
||||||
|
|
||||||
let state_service =
|
assert_eq!(
|
||||||
service_fn(|_| async { unreachable!("State service should not be called") });
|
verification_result,
|
||||||
let verifier = Verifier::new_for_tests(&network, state_service);
|
Err(TransactionError::DuplicateTransparentSpend(
|
||||||
|
input.outpoint().expect("Input should have an outpoint")
|
||||||
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
|
|
||||||
))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test if signed V4 transaction with a dummy [`sprout::JoinSplit`] is accepted.
|
/// 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
|
// Utility functions
|
||||||
|
|
||||||
/// Create a mock transparent transfer to be included in a transaction.
|
/// Create a mock transparent transfer to be included in a transaction.
|
||||||
|
|
Loading…
Reference in New Issue