Skip to content

Commit 9a903dd

Browse files
authored
fix!: address more Ironwood review findings across chain, consensus, and state (#10886)
* fix: correct Ironwood v6 flag, history-tree, and empty-tree rules - chain: reject the enableCrossAddress flag (bit 2) on the v6 Orchard bundle at deserialization, matching orchard::Flags::from_byte, which reserves it for the Orchard pool in every tx version. Only the Ironwood bundle uses the FlagsV6 codec. Without this a crafted v6 tx parsed but then aborted the node in the txid-path expect() when to_librustzcash rejected the flag. - state: fall back to the legacy history-tree entry width on any current-width parse error, not just UnexpectedEof. A multi-peak legacy record misaligns at the wider width and bincode fails with a varint error, which the EOF-only gate turned into an upgrade crash-loop. - state: serve the empty Ironwood tree only before NU6.3 activation; from activation onward a missing tree is a real invariant violation, so fail loudly like the Orchard accessor instead of masking corruption. * refactor(state): thread Ironwood data and trees through named types - Yield ironwood_shielded_data from the per-transaction version match in both update_chain_tip_with_block and revert_chain_with, replacing the two hand-synced `if let V6` blocks. A future tx version now cannot silently skip Ironwood nullifier tracking in one direction only. - Pass the note commitment trees to Chain::new and Treestate::new in a NoteCommitmentTrees struct instead of adjacent positional Arc<orchard:: tree::NoteCommitmentTree> arguments (orchard and ironwood share the type), so an orchard/ironwood swap is a compile error rather than silent tree corruption. Drops two too_many_arguments allows. * perf(consensus): cut per-transaction network-upgrade and bundle work - Compute NetworkUpgrade::current once in check_structure_and_network_rules and pass it into orchard_value_balance_non_negative and coinbase_orchard_component_empty, instead of each rebuilding the activation-height map per transaction. - Store the halo2 Item bundle in an Arc so the eager clone tower-fallback makes for every request shares the bundle instead of deep-copying its actions and multi-KB proof; add_bundle only needs a reference. * refactor(chain): deduplicate history-tree rebuild and Ironwood checks - Share the per-variant InnerHistoryTree rebuild between prune() and the Clone impl via rebuilt_inner(), so a new history-tree version adds its arm once. - Test Ironwood-bundle presence with has_ironwood_shielded_data() instead of ironwood_actions().count(), which walked the AtLeastOne action list. - Delegate insert_fake_orchard_shielded_data to fake_v6_orchard_shielded_data. * fix(state): keep the indexer Spend enum's nullifier imports The refactor(state) commit dropped sprout/sapling/orchard from the shared zebra_chain import block (unused without the indexer feature), but the indexer-only Spend enum and its From impls still reference them, breaking `cargo check --features indexer` (9 x E0433). Re-add them alongside the ironwood import, cfg-gated to the indexer feature. * docs(chain): fix stale v6 Orchard flag comment The call-site comment still said the v6 Orchard bundle permits enableCrossAddress; the flag-mask fix reserves it for the Orchard pool (only Ironwood permits it). * refactor(chain): imply the v6 Orchard flag codec from the bundle type Address @arya2 review: tie the flagsOrchard codec to each v6 bundle newtype via a `V6FlagCodec` associated type (orchard::ShieldedDataV6 -> Flags, ironwood::ShieldedData -> FlagsV6) so the deserializers imply it instead of naming it explicitly. No behavior change.
1 parent 138a830 commit 9a903dd

18 files changed

Lines changed: 373 additions & 246 deletions

File tree

zebra-chain/src/history_tree.rs

Lines changed: 14 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,16 @@ impl NonEmptyHistoryTree {
352352
// Remove all non-peak entries
353353
self.peaks.retain(|k, _| peak_pos_set.contains(k));
354354
// Rebuild tree
355-
self.inner = match self.inner {
355+
self.inner = self.rebuilt_inner()?;
356+
Ok(())
357+
}
358+
359+
/// Rebuilds the inner tree from the cached `network`, `network_upgrade`, `size`, and `peaks`.
360+
///
361+
/// Shared by [`Self::prune`] and the [`Clone`] impl, which reconstruct the inner tree
362+
/// identically and differ only in how they handle the (practically impossible) rebuild error.
363+
fn rebuilt_inner(&self) -> Result<InnerHistoryTree, io::Error> {
364+
Ok(match &self.inner {
356365
InnerHistoryTree::PreOrchard(_) => {
357366
InnerHistoryTree::PreOrchard(Tree::<PreOrchard>::new_from_cache(
358367
&self.network,
@@ -380,8 +389,7 @@ impl NonEmptyHistoryTree {
380389
&Default::default(),
381390
)?)
382391
}
383-
};
384-
Ok(())
392+
})
385393
}
386394

387395
/// Return the hash of the tree root.
@@ -416,38 +424,9 @@ impl NonEmptyHistoryTree {
416424

417425
impl Clone for NonEmptyHistoryTree {
418426
fn clone(&self) -> Self {
419-
let tree = match self.inner {
420-
InnerHistoryTree::PreOrchard(_) => InnerHistoryTree::PreOrchard(
421-
Tree::<PreOrchard>::new_from_cache(
422-
&self.network,
423-
self.network_upgrade,
424-
self.size,
425-
&self.peaks,
426-
&Default::default(),
427-
)
428-
.expect("rebuilding an existing tree should always work"),
429-
),
430-
InnerHistoryTree::OrchardOnward(_) => InnerHistoryTree::OrchardOnward(
431-
Tree::<OrchardOnward>::new_from_cache(
432-
&self.network,
433-
self.network_upgrade,
434-
self.size,
435-
&self.peaks,
436-
&Default::default(),
437-
)
438-
.expect("rebuilding an existing tree should always work"),
439-
),
440-
InnerHistoryTree::IronwoodOnward(_) => InnerHistoryTree::IronwoodOnward(
441-
Tree::<IronwoodOnward>::new_from_cache(
442-
&self.network,
443-
self.network_upgrade,
444-
self.size,
445-
&self.peaks,
446-
&Default::default(),
447-
)
448-
.expect("rebuilding an existing tree should always work"),
449-
),
450-
};
427+
let tree = self
428+
.rebuilt_inner()
429+
.expect("rebuilding an existing tree should always work");
451430
NonEmptyHistoryTree {
452431
network: self.network.clone(),
453432
network_upgrade: self.network_upgrade,

zebra-chain/src/ironwood.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ impl From<orchard::Nullifier> for Nullifier {
3838

3939
/// Ironwood shielded data: a v6 Orchard-protocol bundle committed to the Ironwood pool.
4040
///
41-
/// Wraps [`orchard::ShieldedDataV6`] (which itself carries the NU6.3 flag-byte format that permits
42-
/// the `enableCrossAddress` flag). The Ironwood bundle shares the exact wire format of the v6
43-
/// Orchard bundle; this newtype keeps the two type-distinct so they cannot be accidentally
44-
/// interchanged, and so the Ironwood bundle can commit into a separate note commitment tree and
45-
/// nullifier set.
41+
/// Wraps [`orchard::ShieldedDataV6`] (the v6 Orchard bundle shape). The Ironwood bundle shares the
42+
/// exact wire format of the v6 Orchard bundle, but is the only pool that permits the
43+
/// `enableCrossAddress` flag (bit 2), which the Orchard pool reserves regardless of tx version. This
44+
/// newtype keeps the two type-distinct so they cannot be accidentally interchanged, and so the
45+
/// Ironwood bundle can commit into a separate note commitment tree and nullifier set.
4646
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
4747
pub struct ShieldedData(orchard::ShieldedDataV6);
4848

zebra-chain/src/orchard/shielded_data.rs

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -296,19 +296,19 @@ bitflags! {
296296
/// `enableCrossAddress` (NU6.3, bit 2): allow output notes to use a different
297297
/// protocol-level address than the spending key.
298298
///
299-
/// Reserved (MUST be 0) before NU6.3. Valid only in the NU6.3 flag-byte format (v6
300-
/// Orchard and Ironwood bundles); parsed via the `FlagsV6` newtype.
299+
/// Reserved (MUST be 0) for the Orchard pool in every tx version. Valid only for the
300+
/// Ironwood pool (v6), parsed via the `FlagsV6` newtype.
301301
const ENABLE_CROSS_ADDRESS = 0b00000100;
302302
}
303303
}
304304

305-
/// The Orchard flags of a v6 (NU6.3) Orchard or Ironwood bundle.
305+
/// The Orchard flags of an Ironwood (v6) bundle.
306306
///
307-
/// Newtype over [`Flags`] whose [`ZcashDeserialize`] impl uses the NU6.3 flag-byte format: bit 2
308-
/// (`enableCrossAddress`) is valid and only bits 3..7 are reserved. The bare [`Flags`] codec is the
309-
/// pre-NU6.3 (v5 Orchard) format, where bits 2..7 are all reserved. Encoding the format in the type
310-
/// keeps the v5 and v6 flag-parsing paths from being confused (parallels
311-
/// [`ShieldedDataV6`]).
307+
/// Newtype over [`Flags`] whose [`ZcashDeserialize`] impl uses the NU6.3 Ironwood flag-byte format:
308+
/// bit 2 (`enableCrossAddress`) is valid and only bits 3..7 are reserved. The bare [`Flags`] codec
309+
/// is the format for every Orchard-pool bundle (v5 *and* v6), where bits 2..7 are all reserved
310+
/// `enableCrossAddress` is permitted only for the Ironwood pool. Encoding the format in the type
311+
/// keeps the two flag-parsing paths from being confused (parallels [`ShieldedDataV6`]).
312312
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
313313
pub struct FlagsV6(Flags);
314314

@@ -380,28 +380,29 @@ impl ZcashDeserialize for Flags {
380380
/// > [NU5 onward] In a version 5 transaction, the reserved bits 2..7 of the flagsOrchard
381381
/// > field MUST be zero.
382382
///
383-
/// From NU6.3, the v6 Orchard and Ironwood flag bytes use bit 2 as `enableCrossAddress`, so
384-
/// only bits 3..7 are reserved (see [`FlagsV6`]).
383+
/// From NU6.3, the Ironwood flag byte uses bit 2 as `enableCrossAddress`, so only bits 3..7 are
384+
/// reserved (see [`FlagsV6`]); the Orchard pool keeps bit 2 reserved in every tx version.
385385
///
386386
/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
387387
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
388-
// The default codec is the pre-NU6.3 format, used by v5 Orchard bundles, where bits 2..7
389-
// (including `enableCrossAddress`) are reserved and MUST be zero. v6 Orchard and Ironwood
390-
// bundles deserialize via the `FlagsV6` newtype, which permits bit 2.
388+
// The default codec is the pre-NU6.3 format, used by v5 *and* v6 Orchard bundles, where
389+
// bits 2..7 (including `enableCrossAddress`) are reserved and MUST be zero. Only the Ironwood
390+
// bundle deserializes via the `FlagsV6` newtype, which permits bit 2.
391391
Flags::from_byte(reader.read_u8()?, Flags::PRE_NU6_3_RESERVED)
392392
}
393393
}
394394

395395
impl ZcashDeserialize for FlagsV6 {
396396
/// # Consensus
397397
///
398-
/// From NU6.3, the v6 Orchard and Ironwood flag bytes use bit 2 as `enableCrossAddress`, so
399-
/// only bits 3..7 are reserved and MUST be zero (cf. the v5 rule on [`Flags`]).
398+
/// From NU6.3, the Ironwood flag byte uses bit 2 as `enableCrossAddress`, so only bits 3..7 are
399+
/// reserved and MUST be zero (cf. the Orchard-pool rule on [`Flags`], which keeps bit 2
400+
/// reserved).
400401
///
401402
/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
402403
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
403-
// The NU6.3 format, used by v6 Orchard and Ironwood bundles: bit 2 (`enableCrossAddress`)
404-
// is valid and only bits 3..7 are reserved.
404+
// The NU6.3 Ironwood format: bit 2 (`enableCrossAddress`) is valid and only bits 3..7 are
405+
// reserved.
405406
Ok(FlagsV6(Flags::from_byte(
406407
reader.read_u8()?,
407408
Flags::NU6_3_RESERVED,

zebra-chain/src/transaction.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ impl Transaction {
324324
.orchard_flags()
325325
.unwrap_or_else(orchard::Flags::empty)
326326
.contains(orchard::Flags::ENABLE_SPENDS))
327-
|| (self.ironwood_actions().count() > 0
327+
|| (self.has_ironwood_shielded_data()
328328
&& self
329329
.ironwood_flags()
330330
.unwrap_or_else(orchard::Flags::empty)
@@ -342,7 +342,7 @@ impl Transaction {
342342
.orchard_flags()
343343
.unwrap_or_else(orchard::Flags::empty)
344344
.contains(orchard::Flags::ENABLE_OUTPUTS))
345-
|| (self.ironwood_actions().count() > 0
345+
|| (self.has_ironwood_shielded_data()
346346
&& self
347347
.ironwood_flags()
348348
.unwrap_or_else(orchard::Flags::empty)
@@ -369,7 +369,7 @@ impl Transaction {
369369
///
370370
/// Mirrors [`Self::has_enough_orchard_flags`] for the Ironwood pool.
371371
pub fn has_enough_ironwood_flags(&self) -> bool {
372-
if self.ironwood_actions().count() == 0 {
372+
if !self.has_ironwood_shielded_data() {
373373
return true;
374374
}
375375
self.ironwood_flags()

zebra-chain/src/transaction/arbitrary.rs

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ use reddsa::{orchard::Binding, Signature};
88

99
use crate::{
1010
amount::{self, Amount, NegativeAllowed, NonNegative},
11-
at_least_one,
1211
block::{self, arbitrary::MAX_PARTIAL_CHAIN_BLOCKS},
1312
orchard,
1413
parameters::{Network, NetworkUpgrade},
@@ -1078,28 +1077,12 @@ pub fn transactions_from_blocks<'a>(
10781077
pub fn insert_fake_orchard_shielded_data(
10791078
transaction: &mut Transaction,
10801079
) -> &mut orchard::ShieldedData {
1081-
// Create a dummy action
1082-
let mut runner = TestRunner::default();
1083-
let dummy_action = orchard::Action::arbitrary()
1084-
.new_tree(&mut runner)
1085-
.unwrap()
1086-
.current();
1087-
1088-
// Pair the dummy action with a fake signature
1089-
let dummy_authorized_action = orchard::AuthorizedAction {
1090-
action: dummy_action,
1091-
spend_auth_sig: Signature::from([0u8; 64]),
1092-
};
1093-
1094-
// Place the dummy action inside the Orchard shielded data
1095-
let dummy_shielded_data = orchard::ShieldedData {
1096-
flags: orchard::Flags::empty(),
1097-
value_balance: Amount::try_from(0).expect("invalid transaction amount"),
1098-
shared_anchor: orchard::tree::Root::default(),
1099-
proof: Halo2Proof(vec![]),
1100-
actions: at_least_one![dummy_authorized_action],
1101-
binding_sig: Signature::from([0u8; 64]),
1102-
};
1080+
// A single-action dummy bundle with no flags and a zero value balance.
1081+
let dummy_shielded_data = fake_v6_orchard_shielded_data(
1082+
orchard::Flags::empty(),
1083+
Amount::try_from(0).expect("invalid transaction amount"),
1084+
1,
1085+
);
11031086

11041087
// Replace the shielded data in the transaction
11051088
match transaction {

zebra-chain/src/transaction/serialize.rs

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -419,15 +419,45 @@ impl ZcashSerialize for orchard::ShieldedData {
419419
}
420420
}
421421

422-
// A v6 (NU6.3) Orchard or Ironwood bundle differs from a v5 Orchard bundle only in its flag-byte
423-
// format on *deserialization* (the NU6.3 format permits `enableCrossAddress`). It encodes
424-
// identically on the wire (the flag byte is written as-is), so the v6 Orchard and Ironwood
425-
// (de)serializers below delegate to the v5 Orchard bundle codec, only wrapping/unwrapping their
426-
// newtypes (`orchard::ShieldedDataV6` and `ironwood::ShieldedData`).
422+
/// The `flagsOrchard` codec a v6 Orchard-protocol bundle newtype uses on deserialization.
423+
///
424+
/// A v6 Orchard or Ironwood bundle encodes identically on the wire to a v5 Orchard bundle (the flag
425+
/// byte is written as-is); the pools differ only in the reserved-bit rule applied to `flagsOrchard`.
426+
/// The `enableCrossAddress` bit (bit 2) is permitted only for the Ironwood pool, and is reserved
427+
/// (MUST be 0) for the Orchard pool regardless of tx version — matching
428+
/// `orchard::bundle::Flags::from_byte`, which rejects bit 2 for `ValuePool::Orchard`. Tying the
429+
/// codec to the bundle type lets the (de)serializers below imply it instead of naming it explicitly.
430+
trait V6FlagCodec {
431+
/// The flag codec: `orchard::Flags` reserves bit 2, `orchard::FlagsV6` permits it.
432+
type Codec: ZcashDeserialize + Into<orchard::Flags>;
433+
}
434+
435+
impl V6FlagCodec for orchard::ShieldedDataV6 {
436+
// The v6 Orchard bundle parses with the pre-NU6.3 codec, exactly like v5.
437+
type Codec = orchard::Flags;
438+
}
439+
440+
impl V6FlagCodec for ironwood::ShieldedData {
441+
// Only the Ironwood bundle permits `enableCrossAddress`.
442+
type Codec = orchard::FlagsV6;
443+
}
444+
445+
/// Deserializes the shared Orchard-protocol bundle body of a v6 bundle newtype `T`, using the flag
446+
/// codec [implied by `T`](V6FlagCodec) rather than one named at the call site.
447+
fn deserialize_v6_orchard_shielded_data<R, T>(
448+
reader: R,
449+
) -> Result<Option<orchard::ShieldedData>, SerializationError>
450+
where
451+
R: io::Read,
452+
T: V6FlagCodec,
453+
{
454+
deserialize_orchard_shielded_data::<R, T::Codec>(reader)
455+
}
456+
427457
impl ZcashDeserialize for Option<orchard::ShieldedDataV6> {
428458
fn zcash_deserialize<R: io::Read>(reader: R) -> Result<Self, SerializationError> {
429459
Ok(
430-
deserialize_orchard_shielded_data::<R, orchard::FlagsV6>(reader)?
460+
deserialize_v6_orchard_shielded_data::<R, orchard::ShieldedDataV6>(reader)?
431461
.map(orchard::ShieldedDataV6::new),
432462
)
433463
}
@@ -442,7 +472,8 @@ impl ZcashSerialize for Option<orchard::ShieldedDataV6> {
442472
impl ZcashDeserialize for Option<ironwood::ShieldedData> {
443473
fn zcash_deserialize<R: io::Read>(reader: R) -> Result<Self, SerializationError> {
444474
Ok(
445-
Option::<orchard::ShieldedDataV6>::zcash_deserialize(reader)?
475+
deserialize_v6_orchard_shielded_data::<R, ironwood::ShieldedData>(reader)?
476+
.map(orchard::ShieldedDataV6::new)
446477
.map(ironwood::ShieldedData::new),
447478
)
448479
}
@@ -1155,9 +1186,9 @@ impl ZcashDeserialize for Transaction {
11551186

11561187
// A bundle of fields denoted in the spec as `nActionsOrchard`, `vActionsOrchard`,
11571188
// `flagsOrchard`,`valueBalanceOrchard`, `anchorOrchard`, `sizeProofsOrchard`,
1158-
// `proofsOrchard`, `vSpendAuthSigsOrchard`, and `bindingSigOrchard`. The
1159-
// `ShieldedDataV6` codec uses the NU6.3 flag-byte format (`enableCrossAddress`
1160-
// permitted).
1189+
// `proofsOrchard`, `vSpendAuthSigsOrchard`, and `bindingSigOrchard`. The v6 Orchard
1190+
// bundle reserves the `enableCrossAddress` bit (like v5); only the Ironwood bundle
1191+
// below permits it.
11611192
let orchard_shielded_data = (&mut limited_reader)
11621193
.zcash_deserialize_into::<Option<orchard::ShieldedDataV6>>()?;
11631194

@@ -1166,6 +1197,12 @@ impl ZcashDeserialize for Transaction {
11661197
let ironwood_shielded_data = (&mut limited_reader)
11671198
.zcash_deserialize_into::<Option<ironwood::ShieldedData>>()?;
11681199

1200+
// Unlike the v5 arm, the v6 arm does not round-trip through `to_librustzcash` here:
1201+
// that would drive librustzcash's proof parser (which rejects the structurally-fake
1202+
// proofs the test helpers produce), coupling wire deserialization to proof parsing.
1203+
// The `enableCrossAddress` divergence that would otherwise reach the txid-path
1204+
// `expect(...)` is already rejected above, inside `orchard::Flags::from_byte`, before
1205+
// this transaction is constructed.
11691206
Ok(Transaction::V6 {
11701207
network_upgrade,
11711208
lock_time,

zebra-chain/src/transaction/tests/vectors.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,6 +1122,56 @@ fn v6_transaction_with_bundles_round_trips() {
11221122
assert_eq!(tx, tx2);
11231123
}
11241124

1125+
/// The `enableCrossAddress` flag (bit 2) is permitted only for the Ironwood pool: a v6 Orchard
1126+
/// bundle carrying it MUST be rejected at deserialization (matching `orchard::Flags::from_byte`,
1127+
/// which reserves bit 2 for `ValuePool::Orchard` in every tx version), while the same flag on the
1128+
/// Ironwood bundle round-trips.
1129+
///
1130+
/// The Orchard case is the wire-layer guard: without it, a crafted bundle deserializes and then
1131+
/// aborts the node in the txid-path `expect(...)` when `to_librustzcash` rejects the flag.
1132+
#[test]
1133+
fn v6_orchard_bundle_rejects_cross_address_flag_on_the_wire() {
1134+
use crate::ironwood;
1135+
use crate::orchard::{Flags, ShieldedDataV6};
1136+
1137+
let _init_guard = zebra_test::init();
1138+
let zero = Amount::try_from(0).expect("zero is a valid amount");
1139+
1140+
// A v6 Orchard bundle with `enableCrossAddress` serializes (the flag byte is written as-is) but
1141+
// MUST NOT deserialize.
1142+
let orchard = ShieldedDataV6::new(arbitrary::fake_v6_orchard_shielded_data(
1143+
Flags::ENABLE_SPENDS | Flags::ENABLE_CROSS_ADDRESS,
1144+
zero,
1145+
1,
1146+
));
1147+
let tx = arbitrary::fake_v6_transaction(NetworkUpgrade::Nu6_3, Some(orchard), None);
1148+
let bytes = tx
1149+
.zcash_serialize_to_vec()
1150+
.expect("v6 transaction serializes");
1151+
let result: Result<Transaction, _> = bytes.zcash_deserialize_into();
1152+
assert!(
1153+
result.is_err(),
1154+
"a v6 Orchard bundle with enableCrossAddress must be rejected on the wire",
1155+
);
1156+
1157+
// The same flag on the Ironwood bundle is valid and round-trips.
1158+
let ironwood = ironwood::ShieldedData::new(ShieldedDataV6::new(
1159+
arbitrary::fake_v6_orchard_shielded_data(
1160+
Flags::ENABLE_SPENDS | Flags::ENABLE_CROSS_ADDRESS,
1161+
zero,
1162+
1,
1163+
),
1164+
));
1165+
let tx = arbitrary::fake_v6_transaction(NetworkUpgrade::Nu6_3, None, Some(ironwood));
1166+
let bytes = tx
1167+
.zcash_serialize_to_vec()
1168+
.expect("v6 transaction serializes");
1169+
let tx2: Transaction = bytes
1170+
.zcash_deserialize_into()
1171+
.expect("a v6 Ironwood bundle with enableCrossAddress round-trips");
1172+
assert_eq!(tx, tx2);
1173+
}
1174+
11251175
#[test]
11261176
fn test_coinbase_script() -> Result<()> {
11271177
let _init_guard = zebra_test::init();

0 commit comments

Comments
 (0)