Skip to content

feat(bundle): expose Flags/BundleFormat and add serde for downstream integration#23

Draft
czarcas7ic wants to merge 3 commits into
adam/protocol-api-on-ironwoodfrom
adam/expose-flags-public-api
Draft

feat(bundle): expose Flags/BundleFormat and add serde for downstream integration#23
czarcas7ic wants to merge 3 commits into
adam/protocol-api-on-ironwoodfrom
adam/expose-flags-public-api

Conversation

@czarcas7ic

@czarcas7ic czarcas7ic commented Jun 22, 2026

Copy link
Copy Markdown

Why (draft for review before upstreaming)

Downstream consumers with their own transaction serializer — e.g. zebra-chain, which does not serialize via zcash_primitives — currently can't reuse this crate's Orchard flag handling, so they re-implement a parallel copy: their own Flags bitflags (including the cross-address bit), a FlagFormat { PreNu6_3, Nu6_3 } enum that mirrors BundleFormat, and the byte (de)serialization. That duplicated, consensus-critical logic has to be kept bit-for-bit in sync with this crate.

This crate already has everything needed — Flags, BundleFormat, to_byte/from_byte, the accessors and constants — it just isn't all reachable/usable from outside. These small changes close the gap so a consumer can delegate, the same way zcash_primitives does.

Changes

  1. Re-export Flags and BundleFormat at the crate root (next to BundleProtocol), so consumers write orchard::Flags / orchard::BundleFormat.
  2. Flags::from_parts made public — the cross-address-enabled constructor. Restricted sets stay available via the existing public constants (ENABLED, SPENDS_DISABLED, OUTPUTS_DISABLED, CROSS_ADDRESS_DISABLED), so the intentionally-unrepresentable "restricted + spends-disabled" combo stays unreachable from the public API. from_parts_with_cross_address stays crate-internal.
  3. serde::{Serialize, Deserialize} on Flags so it can be embedded in consumers' serialized types.

The byte round-trip (to_byte/from_byte(BundleFormat)) and the accessors/constants are already public, so no other changes are needed for a consumer to delegate. A doctest on from_parts exercises the full public path.

serde: tested per protocol

serde_round_trip_preserves_cross_address_bit_per_protocol proves the discriminating cross-address bit (0b100) survives a serde round-trip for every BundleProtocol: pre-NU6.3 and NU6.3 Orchard encode it as 0 (reserved / enableCrossAddress = 0), while NU6.3 Ironwood encodes it as 1. The existing flags_byte_encoding and flags_parsing_diverges_between_eras tests already lock down the consensus byte itself.

Open question (feedback wanted)

The serde derive uses the obvious struct-of-three-bools representation. Two things to decide:

  • Whether a compact/byte representation is preferred over the derived struct.
  • derive(Deserialize) can construct the "restricted + spends-disabled" combination that from_parts intentionally forbids (note from_byte can already produce it, so this is not a new soundness gap). If serde should honor that constructor invariant, a hand-written Deserialize would be needed.

(Consumers such as zebra currently serialize flags in a bitflags-1.x legacy format; aligning the two representations is a separate consumer-side decision.)

Draft to preview the shape before opening upstream.

…integration

Let an external transaction serializer reuse Orchard's flag types and consensus
byte encoding instead of re-implementing them. (zebra-chain has its own
transaction codec and currently maintains a parallel `Flags` + a pre/post-NU6.3
"format" enum + byte (de)serialization that must be kept in lock-step with this
crate.)

- Re-export `Flags` and `BundleFormat` from the crate root, alongside the
  already-exported `BundleProtocol`.
- Make `Flags::from_parts` public. It keeps cross-address transfers enabled (the
  documented safe default); restricted combinations remain reachable via the
  existing public constants, so "restricted + spends-disabled" stays
  unrepresentable. `from_parts_with_cross_address` stays crate-internal.
- Derive `serde::{Serialize, Deserialize}` on `Flags` so consumers can embed it
  in their own serialized types.

With these, an integrator can name the types, construct flags, round-trip the
consensus byte through the existing `Flags::to_byte`/`from_byte(BundleFormat)`,
and serialize flags, with no duplicated logic.
Demonstrates the now-public surface a downstream serializer uses: the
re-exported orchard::{Flags, BundleFormat}, construction via Flags::from_parts,
and a consensus-byte round-trip through to_byte/from_byte.
…otocol

Gates the serde derive on Flags: proves pre-NU6.3 and NU6.3 Orchard encode the
cross-address bit (0b100) as 0 while NU6.3 Ironwood encodes it as 1, and that a
serde round-trip preserves the value (hence the encoded flag byte) for every
BundleProtocol. Adds serde_json as a dev-dependency for the round-trip.
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