Skip to content

feat(consensus): implement ZIP 233 behind a build feature - #857

Closed
evan-forbes wants to merge 13 commits into
feat/zip2003-disallow-v4from
feat/zip233-burn-amount
Closed

feat(consensus): implement ZIP 233 behind a build feature#857
evan-forbes wants to merge 13 commits into
feat/zip2003-disallow-v4from
feat/zip233-burn-amount

Conversation

@evan-forbes

@evan-forbes evan-forbes commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Stacked on #856 (ZIP 2003) → #851 (ZIP 218). Third of the remaining NU7 consensus changes.

Implements ZIP 233 "Network Sustainability Mechanism: Removing Funds From Circulation" behind the off-by-default zip233 cargo feature. Ported from the merged upstream zebra#8930, extended here with the ZIP 244 commitment, the activation rule, and the build-flag guard.

ZIP 233 is one of the two ZIPs behind Q1 of the ZCAP and coinholder polls, and it is what makes ZIP 234's reissuance and ZIP 235's fee burning expressible.

Transaction format

Transaction::V6 gains zip233_amount, serialized as an 8-byte little-endian amount after nExpiryHeight and committed to by the ZIP 244 header digest as T.1f.

zakura-primitives already implements the same two changes behind its own zip-233 feature, which zip233 turns on, so the native ZIP 244 digests and librustzcash stay in lockstep. The existing native_zip244_matches_test_vectors, v6_zip244_sighash_matches_librustzcash, and script_code_commitment_is_version_specific cross-checks all pass with the feature on, which is what shows the two implementations agree byte for byte.

The build flag

The zakura-primitives half is also gated on cfg(zcash_unstable = "nu7"), which no cargo feature can set on a dependency. A zip233 build without RUSTFLAGS='--cfg zcash_unstable="nu7"' would disagree with librustzcash about the v6 wire format — a consensus split, not a compile error.

Cargo features have to be additive, so this cannot be a compile_error!: cargo check --all-features runs in CI and would fail. Instead ZIP233_ENABLED requires the cfg as well as the feature, so a build missing the flag follows today's consensus rather than half of ZIP 233. The two sides are on and off together by construction. The CI job sets the flag.

A zcash_unstable = "nu7" build also gives NU7 the branch ID zcash_protocol maps it to (0xffffffff) rather than Zakura's 0xfffffffe placeholder, because Transaction::to_librustzcash round-trips the branch ID through it. Any other build is unchanged.

Consensus

  • zip233_amount_is_valid rejects a burn before NU7, which is when ZIP 233 activates. Amount<NonNegative> already bounds the amount to {0 .. MAX_MONEY}.
  • miner_fee subtracts the burn from the transaction's remaining value. The coinbase is checked against the block subsidy plus these fees, so this one subtraction is what stops the coinbase reclaiming the burn: the block's net transparent pool change becomes subsidy − burn, and the money reserve ZIP 234 reads grows by the burned amount.
  • has_inputs_and_outputs accepts a transaction whose only effect is a burn.

Tests

  • zip233_amount_is_committed_to_by_the_txid — two transactions differing only in the burn must have different txids, and both must round-trip and match librustzcash. Without this commitment the amount would be malleable.
  • The v6 serialization proptest now generates random burn amounts under the feature.
  • zip233_amount_activates_at_nu7_and_is_not_a_fee — the activation boundary and the fee exclusion.
  • A zip233 CI job mirroring the zip218 one, with the required RUSTFLAGS.

Default build: 1270 tests pass across zakura-chain, zakura-consensus, zakura-rpc, and zakura-state; clippy clean.

Follow-ups

  • The RPC transaction JSON does not expose zip233Amount.
  • A zip233 build changes the v6 wire format from NU6.3 onward, not from NU7, because that is what zakura-primitives does today (TxVersion::V6 => has_zip233() == true). A real deployment needs the format gated on the branch ID so NU6.3-era v6 transactions keep parsing, which is a change in the fork first.
  • NU7's final branch ID (0x77190ad8) still needs coordinating across Zakura and librustzcash.

Implements ZIP 218 "25-second Block Target Spacing" (zcash/zips#1215),
gated on the new `zip218` cargo feature so a default build keeps today's
consensus. Even in a `zip218` build the rules stay dormant until NU7 has
an activation height on the configured network, which no production
network sets yet.

Block target spacing and issuance:

- `PostNU7PoWTargetSpacing` = 25 seconds, an NU7 era in `target_spacings`,
  and a `NU7PoWTargetSpacingRatio` of 3.
- `halving` folds over the per-era spacings instead of branching on
  Blossom, and `block_subsidy` scales by `era_spacing /
  pre_blossom_spacing`. Both are the spec's segmented sum with the common
  denominator factored out, so a future spacing change is one tuple, not
  another branch. Issuance per unit of wall-clock time is unchanged.

Difficulty:

- `PostNU7PoWAveragingWindow` = 102, and `PoWAveragingWindow` becomes a
  height-dependent function that `AdjustedDifficulty` consults for the
  mean target and the median timespan.
- `POW_ADJUSTMENT_BLOCK_SPAN` now covers the widest window the build can
  use, so a `zip218` build carries 113 predecessors of difficulty context
  from genesis and ignores the entries past the window in force at the
  candidate height. The span stays 28 in a default build.

Shielded action limits:

- Per-block limits of 330 Orchard actions, 300 Sapling spends + outputs,
  and 25 Sprout JoinSplits, plus a global shielded budget of 330 over
  `orchard + sapling_ios + 2 * joinsplits`.
- `shielded_action_limits_are_valid` runs over any iterator of
  transactions: the block verifier passes the whole block, and the
  transaction verifier passes one transaction, so the mempool rejects a
  transaction whose own counts can never be mined.
- `getblocktemplate` tracks the same limits through a `BlockTemplateLimits`
  struct, which replaces the three loose counters and the
  `TryUpdateBlockLimits` trait, so template selection cannot build a block
  the verifier would reject.

The non-consensus constants ZIP 218 recommends scaling (the expiry delta,
`MAX_REORG_LENGTH`, and the block download and retention windows) are left
for a follow-up.
`linked_validation_context` hardcoded 27 predecessors, duplicating
`POW_PREDECESSOR_CONTEXT_SPAN` rather than reading it. The recovery audit
requires exactly `min(finalized_height, POW_PREDECESSOR_CONTEXT_SPAN)`
context rows, so the two disagree as soon as the span changes, and full
state initialization fails its own audit.

Read the constant instead, and derive the boundary test's expectations
from it. Also generalize the tests that hardcoded the 28-block difficulty
adjustment span or the 17-block averaging window, and bump
zakura-consensus to 8.0.0 for the new `TransactionError` variants.
…eight

Implements ZIP 2003 "Disallow version 4 transactions". Zakura already
rejected V4 transactions at NU7, but the height was welded to the NU7
activation height. The NU7 scope polls offer three deprecation dates, so
`V4Deprecation` names the choice instead:

- `AtNu7`, the ZIP 2003 default and today's behaviour, resolving to the
  NU7 activation height. A network that does not activate NU7 keeps
  accepting V4 transactions.
- `AtHeight(height)`, for a network that activates NU7 and deprecates V4
  transactions later.
- `Never`, for a network that keeps accepting V4 transactions.

`verify_v4_transaction_network_upgrade` now rejects on the resolved
height rather than on the NU7 arm of its network upgrade match, so
Mainnet and the default Testnet keep the rule they have today.

Testnet configures the choice through `testnet_parameters.v4_deprecation`,
which accepts `"nu7"`, `"never"`, or a block height.
Implements ZIP 233 "Network Sustainability Mechanism: Removing Funds From
Circulation", gated on the new `zip233` cargo feature so a default build
keeps today's v6 transaction format and consensus.

Transaction format:

- `Transaction::V6` gains `zip233_amount`, serialized as an 8-byte
  little-endian amount after `nExpiryHeight`, and committed to by the ZIP
  244 header digest as T.1f. `zakura-primitives` implements the same two
  changes behind its own `zip-233` feature, which the `zip233` feature
  turns on, so the native ZIP 244 digests and `librustzcash` agree.
- That `zakura-primitives` code is also gated on the `zcash_unstable =
  "nu7"` cfg, which no cargo feature can set. A `zip233` build without
  `RUSTFLAGS='--cfg zcash_unstable="nu7"'` would disagree with
  `librustzcash` about the v6 wire format, which is a consensus split
  rather than a compile error, so `compile_error!` fails the build
  instead.
- A `zcash_unstable = "nu7"` build gives NU7 the branch ID
  `zcash_protocol` maps it to, because `Transaction::to_librustzcash`
  round-trips the branch ID through it. The placeholder is unchanged in
  any other build.

Consensus:

- `zip233_amount_is_valid` rejects a transaction that removes value from
  circulation before NU7, which is when ZIP 233 activates.
  `Amount<NonNegative>` already bounds the amount to {0 .. MAX_MONEY}.
- `miner_fee` subtracts the burn from the transaction's remaining value.
  The coinbase is checked against the block subsidy plus these fees, so
  subtracting it here is what stops the coinbase reclaiming the burn and
  makes the value leave the chain value pools for good.
- `has_inputs_and_outputs` accepts a transaction whose only effect is a
  burn, because the burn is somewhere for its value to go.

Deferred: the RPC transaction JSON does not expose `zip233Amount` yet.
A `zcash_unstable = "nu7"` build gives NU7 the branch ID `zcash_protocol`
recognises, so that case no longer offers an unrecognized branch ID to
librustzcash. The other two cases still cover a failed conversion.
…reak

Cargo features have to be additive: `cargo check --all-features` must
compile. The `compile_error!` that fired when the `zip233` feature was
set without `RUSTFLAGS='--cfg zcash_unstable="nu7"'` broke that check.

`ZIP233_ENABLED` now requires the cfg as well as the feature, which is
exactly what `zakura-primitives` gates its half of ZIP 233 on. The two
sides are therefore on and off together and cannot disagree about the v6
wire format, so the split the `compile_error!` detected is now
impossible to express. A `zip233` build without the flag follows today's
consensus rather than half of ZIP 233.

Adding `zip233_amount` to `Transaction::V6` is
`enum_struct_variant_field_added`, so `zakura-chain` takes a major
version bump, as does `zakura-consensus` for its new `TransactionError`
variant.
`zakura-chain`, `zakura-header-chain` and `zakura-state` each gained a `zip218`
feature, and each sits at a version already on crates.io without it. Publishing
their dependents at bumped versions leaves `<crate>/zip218` unresolvable on the
index, which `scripts/check-crate-publish-graph.sh` rejects. Adding a feature is
a minor change, so bump `zakura-chain` to 6.2.0, `zakura-header-chain` to 1.2.0
and `zakura-state` to 7.2.0, and move every in-workspace requirement with them.

crates.io published `zakura-header-chain` 1.1.0 today, which is what turned this
latent gap into a red check.
# Conflicts:
#	Cargo.lock
#	crates/zakura-chain/Cargo.toml
#	crates/zakura-consensus/Cargo.toml
#	crates/zakura-header-chain/Cargo.toml
#	crates/zakura-network/Cargo.toml
#	crates/zakura-node-services/Cargo.toml
#	crates/zakura-rpc/Cargo.toml
#	crates/zakura-script/Cargo.toml
#	crates/zakura-state/Cargo.toml
#	crates/zakura-utils/Cargo.toml
#	crates/zakurad/Cargo.toml
@evan-forbes
evan-forbes force-pushed the feat/zip2003-disallow-v4 branch from 0ba4e21 to 81b6ba0 Compare September 5, 2026 05:22
@evan-forbes

Copy link
Copy Markdown
Contributor Author

The August 2026 NU7 poll explicitly excludes transaction-format changes and names ZIP 233 as inapplicable to NU7. Closing this superseded scope. PR #859 has been rebuilt without ZIP 233 or ZIP 235 and now stacks directly on #856.

@evan-forbes evan-forbes closed this Sep 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant