From c9aa1fd4f45f32b1718ec2b3fa9c9a97155033a5 Mon Sep 17 00:00:00 2001 From: Paul <3682187+PaulLaux@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:34:39 +0400 Subject: [PATCH 1/7] fix(deps): allow-list every patched git source for cargo-deny `deny.toml` has `unknown-git = "deny"` and `lint.yml` runs `cargo deny check sources`, so every git source in `Cargo.lock` must appear in `allow-git`. Three did not: - `QED-it/halo2`, newly introduced by the v5.2.0 merge (halo2_gadgets, halo2_proofs, halo2_poseidon), replacing the `zcash/halo2` entry; - `zcash/sinsemilla` and `zcash/zcash_note_encryption`, both still in the patch table but dropped from the list while resolving the merge conflict. Also drop the now-unused `zcash/librustzcash.git` entry: the patch table points every librustzcash crate at `QED-it/librustzcash`. --- deny.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/deny.toml b/deny.toml index 13b944d2315..18ff47d0d3a 100644 --- a/deny.toml +++ b/deny.toml @@ -206,9 +206,11 @@ allow-registry = ["https://github.com/rust-lang/crates.io-index"] # List of URLs for allowed Git repositories # TODO: Remove the URLs once the patch table in Cargo.toml is removed. allow-git = [ - "https://github.com/zcash/librustzcash.git", + "https://github.com/zcash/sinsemilla", + "https://github.com/zcash/zcash_note_encryption", # TODO: remove these QED-it fork entries once the required changes are merged upstream. + "https://github.com/QED-it/halo2", "https://github.com/QED-it/librustzcash", "https://github.com/QED-it/orchard", "https://github.com/QED-it/sapling-crypto", From 9a8b851b6a4be81169ded0d54848f65ccde86cfa Mon Sep 17 00:00:00 2001 From: Paul <3682187+PaulLaux@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:36:51 +0400 Subject: [PATCH 2/7] build(deps): restore upstream's incrementalmerkletree 0.8.2 requirement The v5.2.0 merge kept the fork's older `0.8.1`. Both resolve to 0.8.2 in `Cargo.lock`, so this is drift, not a behaviour change. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 829ff6035ee..57de23eebaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ edition = "2021" # `cargo release` settings [workspace.dependencies] -incrementalmerkletree = { version = "0.8.1", features = ["legacy-api"] } +incrementalmerkletree = { version = "0.8.2", features = ["legacy-api"] } # TODO: Remove the `temporary-zebra` feature once upstream `orchard` exposes the APIs # Zebra needs without this transition feature. orchard = { version = "0.14", features = ["zsa-issuance", "temporary-zebra"] } From 0466605212c5c59b0d6e136b3ebe1c5fa0c8bb06 Mon Sep 17 00:00:00 2001 From: Paul <3682187+PaulLaux@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:36:51 +0400 Subject: [PATCH 3/7] ci: restore retries for sync_large_checkpoints_mempool_mainnet The nextest rework dropped this test when moving the retry overrides from `profile.default` to `profile.all-tests`. It is `#[ignore]`d, so only the `--run-ignored=all` run is affected. Also grouped with the other live-Mainnet sync tests so they don't compete for peers. --- .config/nextest.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.config/nextest.toml b/.config/nextest.toml index 58c969aab29..dc0326009a9 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -180,6 +180,14 @@ test-group = 'serial-live-mainnet' slow-timeout = { period = "5m", terminate-after = 2 } retries = 2 +# `sync_large_checkpoints_mempool_mainnet` is `#[ignore]`d, so it only runs under +# `--run-ignored`. It enforces its own 8m deadline, so the cap here is a hang backstop. +[[profile.all-tests.overrides]] +filter = 'test(=sync_large_checkpoints_mempool_mainnet)' +test-group = 'serial-live-mainnet' +slow-timeout = { period = "10m", terminate-after = 2 } +retries = 3 + # Known-flaky acceptance tests, retried by name. # # `disconnects_from_misbehaving_peers` starts two nodes on incompatible custom From 4597dedadf78d19fc3fc7f272c641b21c875ce8a Mon Sep 17 00:00:00 2001 From: Paul <3682187+PaulLaux@users.noreply.github.com> Date: Mon, 31 Aug 2026 22:45:01 +0400 Subject: [PATCH 4/7] fix(consensus): verify NU7 vanilla Orchard bundles with the vanilla key The v5.2.0 merge collapsed two independent axes of verifier selection into one: upstream's circuit era (pre/post-NU6.2, GHSA-jfw5-j458-pfv6), keyed on the network upgrade, and the fork's flavor, keyed on the bundle. Only the era survived, as `Nu7 => &VERIFIER_ZSA`. V5 transactions stay valid at NU7 and carry vanilla bundles, so they were verified against the OrchardZSA key, which cannot verify a vanilla proof: any NU7 block with a V5 Orchard transaction would be rejected. Both cfg flags are on by default, so the default build was affected. Route on the bundle at the call site, as before the merge, and restore `verifier_for` to upstream's era-only mapping. The routing test asserted the NU7-to-ZSA mapping rather than catching it, so it is fixed too. --- zebra-consensus/src/primitives/halo2.rs | 16 ++++------ zebra-consensus/src/primitives/halo2/tests.rs | 31 +++++++++--------- zebra-consensus/src/transaction.rs | 32 ++++++++++++------- 3 files changed, 42 insertions(+), 37 deletions(-) diff --git a/zebra-consensus/src/primitives/halo2.rs b/zebra-consensus/src/primitives/halo2.rs index bf4ec737513..08a5579f53e 100644 --- a/zebra-consensus/src/primitives/halo2.rs +++ b/zebra-consensus/src/primitives/halo2.rs @@ -259,7 +259,11 @@ pub static VERIFIER_POST_NU6_2: Lazy = #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] pub static VERIFIER_ZSA: Lazy = Lazy::new(|| batch_verifier(&VERIFYING_KEY_ZSA)); -/// Returns the global Halo2 verifier for Orchard bundles in blocks at `network_upgrade`. +/// Returns the global Halo2 verifier for `OrchardVanilla` bundles in blocks at `network_upgrade`. +/// +/// Era and flavor are independent axes; this covers only the era. `OrchardZSA` bundles are routed +/// by flavor to [`VERIFIER_ZSA`] before this is reached. NU7 carries both flavors, because V5 +/// transactions stay valid at NU7, so the upgrade alone cannot pick the key. /// /// The Orchard Action circuit — and therefore its verifying key — changed at NU6.2 (the fixed /// variable-base scalar-multiplication circuit; see GHSA-jfw5-j458-pfv6), and a proof produced @@ -285,15 +289,7 @@ pub fn verifier_for(network_upgrade: NetworkUpgrade) -> &'static VerifierService // NU6.2 ships the fixed circuit, and every upgrade after it inherits that fixed circuit, // so all of them verify under the fixed key. - Nu6_2 => &VERIFIER_POST_NU6_2, - - // Without ZSA V6 support, NU7 continues using the fixed post-NU6.2 Orchard circuit. - #[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))] - Nu7 => &VERIFIER_POST_NU6_2, - - // With ZSA V6 support, NU7 uses the dedicated OrchardZSA circuit and verifying key. - #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] - Nu7 => &VERIFIER_ZSA, + Nu6_2 | Nu7 => &VERIFIER_POST_NU6_2, // `ZFuture` only exists under the `zcash_unstable = "zfuture"` cfg. It is a post-NU6.2 // upgrade, so it inherits the fixed circuit and is bound to the fixed key here on purpose diff --git a/zebra-consensus/src/primitives/halo2/tests.rs b/zebra-consensus/src/primitives/halo2/tests.rs index b0aa9914a48..a5f0d9ff1a9 100644 --- a/zebra-consensus/src/primitives/halo2/tests.rs +++ b/zebra-consensus/src/primitives/halo2/tests.rs @@ -10,7 +10,8 @@ //! * the same proof is **rejected** by the post-NU6.2 (fixed) key, so the verifier is not //! "fail-open" — it does not accept whatever it is handed regardless of era; and //! * [`verifier_for`] routes each network upgrade to the service holding the matching key, -//! with NU6.2 and every later upgrade going to the fixed-key verifier. +//! with NU6.2 and every later upgrade going to the fixed-key verifier, and never to the ZSA +//! verifier — flavor is routed at the call site, not here. use std::sync::Arc; @@ -107,9 +108,6 @@ async fn verifier_for_routes_each_upgrade_to_the_correct_key() { let pre: &'static super::VerifierService = &VERIFIER_PRE_NU6_2; let post: &'static super::VerifierService = &VERIFIER_POST_NU6_2; - #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] - let zsa: &'static super::VerifierService = &VERIFIER_ZSA; - // Everything before NU6.2 (including upgrades from before Orchard existed) routes to the // insecure key, which is the only key any pre-NU6.2 Orchard history verifies under. for nu in [ @@ -123,22 +121,23 @@ async fn verifier_for_routes_each_upgrade_to_the_correct_key() { ); } - // NU6.2 and every later upgrade route to the fixed key, except NU7 in ZSA builds, - // which routes to the dedicated ZSA verifier. Nu7 still guards that "NU6.2 and later" - // does not silently fall back to the insecure verifier for future upgrades. + // NU6.2 and every later upgrade route to the fixed key. Nu7 is included because it carries + // vanilla bundles too, in V5 transactions. for nu in [NetworkUpgrade::Nu6_2, NetworkUpgrade::Nu7] { - #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] - if nu == NetworkUpgrade::Nu7 { - assert!( - std::ptr::eq(verifier_for(nu), zsa), - "{nu:?} must route to the ZSA verifier" - ); - continue; - } - assert!( std::ptr::eq(verifier_for(nu), post), "{nu:?} must route to the post-NU6.2 (fixed) verifier" ); } + + // The ZSA circuit is a separate axis, so `verifier_for` must never return its verifier. + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + { + let zsa: &'static super::VerifierService = &VERIFIER_ZSA; + + assert!( + !std::ptr::eq(zsa, pre) && !std::ptr::eq(zsa, post), + "the ZSA verifier must not alias either era verifier" + ); + } } diff --git a/zebra-consensus/src/transaction.rs b/zebra-consensus/src/transaction.rs index 5d1413c9158..5b507db0ac1 100644 --- a/zebra-consensus/src/transaction.rs +++ b/zebra-consensus/src/transaction.rs @@ -1288,14 +1288,16 @@ where /// Verifies a transaction's Orchard shielded data. /// - /// `network_upgrade` is the network upgrade active at the verified transaction's block - /// height. It selects the Orchard verifier: the Orchard Action circuit (and its verifying - /// key) changed at NU6.2 to fix the variable-base scalar-multiplication bug - /// (GHSA-jfw5-j458-pfv6), so pre-NU6.2 bundles must be verified against the historical - /// (insecure) key and NU6.2+ bundles against the fixed key. A proof from one era does not - /// verify under the other era's key. [`primitives::halo2::verifier_for`] maps the upgrade to - /// the verifier holding the matching key; the two verifiers keep separate batches, so eras - /// are never mixed. + /// A proof only verifies under the key for the circuit that produced it, so the verifier is + /// selected on two independent axes: + /// + /// * flavor: `OrchardZSA` bundles use the ZSA circuit, whatever the upgrade. + /// * era, for `OrchardVanilla` bundles: the vanilla circuit changed at NU6.2 to fix + /// GHSA-jfw5-j458-pfv6, so [`primitives::halo2::verifier_for`] maps `network_upgrade` — the + /// upgrade at the transaction's block height — to the historical or fixed key. + /// + /// Both are needed: NU7 carries vanilla bundles (in V5 transactions, still valid at NU7) as + /// well as ZSA ones. Each verifier holds one key and batches separately, so circuits never mix. fn verify_orchard_bundle( bundle: Option>, sighash: &SigHash, @@ -1316,10 +1318,18 @@ where // Actions in one transaction. So we queue it for verification // only once instead of queuing it up for every Action description. // - // Route the bundle to the verifier for its circuit era: pre-NU6.2 bundles only - // verify under the insecure key, NU6.2+ bundles only under the fixed key. + // Route the bundle to the verifier holding the key for its circuit: by flavor for + // OrchardZSA, otherwise by era. + let verifier = match &bundle { + OrchardBundle::OrchardVanilla(_) => { + primitives::halo2::verifier_for(network_upgrade) + } + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + OrchardBundle::OrchardZSA(_) => &*primitives::halo2::VERIFIER_ZSA, + }; + async_checks.push( - primitives::halo2::verifier_for(network_upgrade) + verifier .clone() .oneshot(primitives::halo2::Item::new(bundle, *sighash)), ); From dc871d0c83272c1549d6f9404d92b34dfda25a1c Mon Sep 17 00:00:00 2001 From: Paul <3682187+PaulLaux@users.noreply.github.com> Date: Tue, 1 Sep 2026 11:44:29 +0400 Subject: [PATCH 5/7] test(consensus): guard vanilla Orchard routing at NU7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a regression test for the previous commit. Both the correct and the incorrect key reject the only vanilla fixture available — a pre-NU6.2 proof from the mainnet test blocks — so the outcome cannot distinguish them and the test asserts the verifier choice instead. To make that choice observable, move the flavor match out of the call site into `verifier_for_bundle`, which composes flavor over `verifier_for`. This also leaves `verifier_for` identical to upstream, so it merges cleanly next time. Verified by reintroducing `Nu7 => &VERIFIER_ZSA`: the new test and the existing routing test both fail. The ZSA direction stays covered end to end by `check_orchard_zsa_workflow`. A true acceptance test for a V5 Orchard transaction at NU7 still needs a post-NU6.2 vanilla fixture, which the test vectors do not have. --- zebra-consensus/src/primitives/halo2.rs | 23 +++++++-- zebra-consensus/src/primitives/halo2/tests.rs | 51 +++++++++++++++++-- zebra-consensus/src/transaction.rs | 19 +++---- 3 files changed, 74 insertions(+), 19 deletions(-) diff --git a/zebra-consensus/src/primitives/halo2.rs b/zebra-consensus/src/primitives/halo2.rs index 08a5579f53e..59d9414b128 100644 --- a/zebra-consensus/src/primitives/halo2.rs +++ b/zebra-consensus/src/primitives/halo2.rs @@ -261,9 +261,9 @@ pub static VERIFIER_ZSA: Lazy = Lazy::new(|| batch_verifier(&VE /// Returns the global Halo2 verifier for `OrchardVanilla` bundles in blocks at `network_upgrade`. /// -/// Era and flavor are independent axes; this covers only the era. `OrchardZSA` bundles are routed -/// by flavor to [`VERIFIER_ZSA`] before this is reached. NU7 carries both flavors, because V5 -/// transactions stay valid at NU7, so the upgrade alone cannot pick the key. +/// Era and flavor are independent axes; this covers only the era. [`verifier_for_bundle`] composes +/// flavor on top, sending `OrchardZSA` bundles to [`VERIFIER_ZSA`] instead. NU7 carries both +/// flavors, because V5 transactions stay valid at NU7, so the upgrade alone cannot pick the key. /// /// The Orchard Action circuit — and therefore its verifying key — changed at NU6.2 (the fixed /// variable-base scalar-multiplication circuit; see GHSA-jfw5-j458-pfv6), and a proof produced @@ -300,6 +300,23 @@ pub fn verifier_for(network_upgrade: NetworkUpgrade) -> &'static VerifierService } } +/// Returns the global Halo2 verifier holding the key for `bundle`'s circuit, in a block at +/// `network_upgrade`. +/// +/// This composes the two axes: flavor picks the circuit family, and for vanilla bundles +/// [`verifier_for`] then picks the era. NU7 needs both, because a V5 transaction there carries a +/// vanilla bundle while a V6 one carries a ZSA bundle. +pub fn verifier_for_bundle( + bundle: &OrchardBundle, + network_upgrade: NetworkUpgrade, +) -> &'static VerifierService { + match bundle { + OrchardBundle::OrchardVanilla(_) => verifier_for(network_upgrade), + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + OrchardBundle::OrchardZSA(_) => &VERIFIER_ZSA, + } +} + /// Halo2 proof verifier implementation /// /// This is the core implementation for the batch verification logic of the diff --git a/zebra-consensus/src/primitives/halo2/tests.rs b/zebra-consensus/src/primitives/halo2/tests.rs index a5f0d9ff1a9..c5f597118fe 100644 --- a/zebra-consensus/src/primitives/halo2/tests.rs +++ b/zebra-consensus/src/primitives/halo2/tests.rs @@ -11,7 +11,8 @@ //! "fail-open" — it does not accept whatever it is handed regardless of era; and //! * [`verifier_for`] routes each network upgrade to the service holding the matching key, //! with NU6.2 and every later upgrade going to the fixed-key verifier, and never to the ZSA -//! verifier — flavor is routed at the call site, not here. +//! verifier — flavor is composed on top by [`verifier_for_bundle`], which must send vanilla +//! bundles to the era verifier even at NU7. use std::sync::Arc; @@ -26,8 +27,8 @@ use zebra_chain::{ }; use super::{ - verifier_for, Item, VERIFIER_POST_NU6_2, VERIFIER_PRE_NU6_2, VERIFYING_KEY_POST_NU6_2, - VERIFYING_KEY_PRE_NU6_2, + verifier_for, verifier_for_bundle, Item, VERIFIER_POST_NU6_2, VERIFIER_PRE_NU6_2, + VERIFYING_KEY_POST_NU6_2, VERIFYING_KEY_PRE_NU6_2, }; #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] @@ -141,3 +142,47 @@ async fn verifier_for_routes_each_upgrade_to_the_correct_key() { ); } } + +/// A vanilla Orchard bundle at NU7 is routed by era, not to the ZSA verifier. +/// +/// V5 transactions stay valid at NU7 and carry vanilla bundles, so routing on the upgrade alone — +/// as the v5.2.0 merge briefly did — sends them to the OrchardZSA key, which cannot verify a +/// vanilla proof. +/// +/// The choice is asserted rather than the outcome, because both the right and the wrong key reject +/// this pre-NU6.2 fixture. The ZSA direction is covered end to end by `check_orchard_zsa_workflow`. +#[tokio::test(flavor = "multi_thread")] +async fn vanilla_bundle_at_nu7_is_routed_by_era_not_flavor() { + let (bundle, _sighash) = pre_nu6_2_bundle_and_sighash(); + + assert!( + matches!(bundle, OrchardBundle::OrchardVanilla(_)), + "a V5 transaction's bundle must be the vanilla flavor" + ); + + assert!( + std::ptr::eq( + verifier_for_bundle(&bundle, NetworkUpgrade::Nu7), + &*VERIFIER_POST_NU6_2 + ), + "a vanilla bundle at NU7 must route to the post-NU6.2 (fixed) verifier" + ); + + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + assert!( + !std::ptr::eq( + verifier_for_bundle(&bundle, NetworkUpgrade::Nu7), + &*VERIFIER_ZSA + ), + "a vanilla bundle must never route to the ZSA verifier" + ); + + // The era axis still applies below NU6.2, so historical blocks re-sync. + assert!( + std::ptr::eq( + verifier_for_bundle(&bundle, NetworkUpgrade::Nu5), + &*VERIFIER_PRE_NU6_2 + ), + "a vanilla bundle at NU5 must route to the pre-NU6.2 (insecure) verifier" + ); +} diff --git a/zebra-consensus/src/transaction.rs b/zebra-consensus/src/transaction.rs index 5b507db0ac1..b3f342fd3a5 100644 --- a/zebra-consensus/src/transaction.rs +++ b/zebra-consensus/src/transaction.rs @@ -1293,8 +1293,10 @@ where /// /// * flavor: `OrchardZSA` bundles use the ZSA circuit, whatever the upgrade. /// * era, for `OrchardVanilla` bundles: the vanilla circuit changed at NU6.2 to fix - /// GHSA-jfw5-j458-pfv6, so [`primitives::halo2::verifier_for`] maps `network_upgrade` — the - /// upgrade at the transaction's block height — to the historical or fixed key. + /// GHSA-jfw5-j458-pfv6, so `network_upgrade` — the upgrade at the transaction's block + /// height — selects the historical or the fixed key. + /// + /// [`primitives::halo2::verifier_for_bundle`] composes both. /// /// Both are needed: NU7 carries vanilla bundles (in V5 transactions, still valid at NU7) as /// well as ZSA ones. Each verifier holds one key and batches separately, so circuits never mix. @@ -1318,18 +1320,9 @@ where // Actions in one transaction. So we queue it for verification // only once instead of queuing it up for every Action description. // - // Route the bundle to the verifier holding the key for its circuit: by flavor for - // OrchardZSA, otherwise by era. - let verifier = match &bundle { - OrchardBundle::OrchardVanilla(_) => { - primitives::halo2::verifier_for(network_upgrade) - } - #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] - OrchardBundle::OrchardZSA(_) => &*primitives::halo2::VERIFIER_ZSA, - }; - + // Route the bundle to the verifier holding the key for its circuit. async_checks.push( - verifier + primitives::halo2::verifier_for_bundle(&bundle, network_upgrade) .clone() .oneshot(primitives::halo2::Item::new(bundle, *sighash)), ); From 680f05ceecc2b323245ce7ed9f03f9848033fd6a Mon Sep 17 00:00:00 2001 From: Paul <3682187+PaulLaux@users.noreply.github.com> Date: Tue, 1 Sep 2026 11:49:04 +0400 Subject: [PATCH 6/7] fix(chain): count distinct Assets in the ZIP-317 issuance fee `nReferenceNotes` stands in for `nAssetCreations`, which cannot be computed from the transaction alone. Counting Issue Actions overcounts whenever two actions in one transaction carry a reference note for the same Asset: an Asset is created at most once, so those cannot each imply a creation. Count distinct Asset bases instead. Still a floor on `nAssetCreations`, so the fee cannot be underpaid, but a tighter one. --- .../src/transaction/unmined/zip317/zsa.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/zebra-chain/src/transaction/unmined/zip317/zsa.rs b/zebra-chain/src/transaction/unmined/zip317/zsa.rs index 9ee0ea94e2b..2e012801ff1 100644 --- a/zebra-chain/src/transaction/unmined/zip317/zsa.rs +++ b/zebra-chain/src/transaction/unmined/zip317/zsa.rs @@ -6,6 +6,8 @@ //! [ZIP-227]: https://zips.z.cash/zip-0227.html //! [ZIP-317]: https://zips.z.cash/zip-0317#fee-calculation +use std::collections::HashSet; + use zcash_primitives::transaction::fees::zip317::CREATION_COST; use crate::transaction::Transaction; @@ -13,7 +15,8 @@ use crate::transaction::Transaction; /// Returns the number of ZIP-317 `logical_actions` contributed by ZIP-227 issuance in /// `transaction`: `nIssueNotes + CREATION_COST * nReferenceNotes`. /// -/// `nReferenceNotes` is the number of Issue Actions whose first note is a [reference note]. +/// `nReferenceNotes` is the number of distinct Assets whose Issue Action in this transaction +/// starts with a [reference note]. /// It is used in place of `nAssetCreations`, which the current ZIP-227 defines in terms of the /// Global Issuance State and which therefore cannot be computed from the transaction alone: /// @@ -32,8 +35,9 @@ use crate::transaction::Transaction; /// /// A reference note is required on the first issuance of an Asset and optional on subsequent /// issuance, so `nReferenceNotes >= nAssetCreations` and the fee can never be underpaid. It is -/// higher only if an issuer voluntarily places a reference note first in a re-issuance action, -/// which has no practical benefit and only raises their own fee. +/// higher only if an issuer voluntarily places a reference note first when re-issuing an Asset, +/// which has no practical benefit and only raises their own fee. Counting distinct Assets rather +/// than Issue Actions keeps that overcount as small as the transaction data allows. /// /// [reference note]: https://zips.z.cash/zip-0227.html#reference-notes // @@ -50,8 +54,12 @@ pub fn logical_actions(transaction: &Transaction) -> usize { let n_reference_notes = issue_bundle .actions() .iter() - .filter(|action| action.get_reference_note().is_some()) - .count(); + .filter_map(|action| action.get_reference_note()) + // An Asset is created at most once, so count each Asset once however many Issue + // Actions in this transaction carry a reference note for it. + .map(|note| note.asset().to_bytes()) + .collect::>() + .len(); n_issue_notes + (CREATION_COST * n_reference_notes) } From 794f86aedb8672229d408a9a269babaa189c9293 Mon Sep 17 00:00:00 2001 From: Paul <3682187+PaulLaux@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:02:32 +0400 Subject: [PATCH 7/7] fix(chain): require an NU7 branch ID in V6 transactions The V6 arm reused the V5 guard verbatim during the v5.2.0 merge, so it rejected only pre-NU5 branch IDs while claiming to reject pre-NU5 ones for a version that is only valid from NU7. `verify_v6_transaction_network_upgrade` already rejects everything before NU7, so nothing invalid was accepted, but the parse-time floor did not match the rule it was copied to enforce. `NetworkUpgrade` derives `Ord` over variants declared in activation-height order, so `< Nu7` admits exactly NU7 and later. --- zebra-chain/src/transaction/serialize.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/zebra-chain/src/transaction/serialize.rs b/zebra-chain/src/transaction/serialize.rs index 12a08ea1148..0b8d4ffb2f3 100644 --- a/zebra-chain/src/transaction/serialize.rs +++ b/zebra-chain/src/transaction/serialize.rs @@ -1341,11 +1341,11 @@ impl ZcashDeserialize for Transaction { // Convert it to a NetworkUpgrade let network_upgrade = NetworkUpgrade::try_from(limited_reader.read_u32::()?)?; - // V6 transactions are only valid from NU5 onward, so reject - // transactions with pre-NU5 consensus branch IDs. - if network_upgrade < NetworkUpgrade::Nu5 { + // V6 transactions are only valid from NU7 onward, so reject + // transactions with pre-NU7 consensus branch IDs. + if network_upgrade < NetworkUpgrade::Nu7 { return Err(SerializationError::Parse( - "v6 transaction must have NU5 or later consensus branch ID", + "v6 transaction must have NU7 or later consensus branch ID", )); } // Denoted as `lock_time` in the spec.