Skip to content

feat: hybrid KEM (ML-KEM-768 + ECDH) for quantum-resistant note encryption#3

Open
QuantumExplorer wants to merge 19 commits into
dashifiedfrom
feat/hybrid-kem
Open

feat: hybrid KEM (ML-KEM-768 + ECDH) for quantum-resistant note encryption#3
QuantumExplorer wants to merge 19 commits into
dashifiedfrom
feat/hybrid-kem

Conversation

@QuantumExplorer
Copy link
Copy Markdown
Member

@QuantumExplorer QuantumExplorer commented Mar 3, 2026

Summary

  • Adds post-quantum key encapsulation (ML-KEM-768, FIPS 203) alongside classical ECDH to defend against Harvest Now, Decrypt Later attacks
  • Feature-gated behind hybrid-kem — all encryption uses the hybrid path when enabled; classic mode fully preserved without the flag
  • Hybrid KDF binds both shared secrets, the PQ ciphertext, and the ephemeral public key following the X-Wing security proof approach

What is included

Area Changes
src/hybrid_kem.rs (new) ML-KEM-768 keygen, deterministic encaps/decaps, hybrid KDF, PqError error type
src/keys.rs PQ key types (PqEncapsulationKey, PqDecapsulationKey), HybridSharedSecret, EphemeralSecretKey with PQ randomness, PreparedIncomingViewingKey with PQ dk
src/address.rs Address extended with optional ek_pq (1184 bytes), serialization updated
src/note.rs TransmittedNoteCiphertext gains ct_pq (1088 bytes) and wider out_ciphertext (112 bytes), Note::esk() derives PQ randomness
src/note_encryption.rs OrchardDomain hybrid impl: ka_agree_enc/ka_agree_dec with ML-KEM, hybrid KDF, OVK recovery with ct_pq threading, version byte 0x03, 5 hybrid integration tests
src/builder.rs OutputInfo::build performs ML-KEM encapsulation, graceful fallback for classic addresses
src/bundle/commitments.rs ct_pq included in noncompact tx ID hash
src/pczt/parse.rs PCZT parsing for ct_pq and larger out_ciphertext
Cargo.toml ml-kem dependency, hybrid-kem feature flag
Benchmarks Updated for Address clone semantics in hybrid mode

Design decisions

  1. Deterministic PQ encapsulation — ML-KEM randomness derived from rseed + rho + ek_pq via BLAKE2b, consistent with Orchards deterministic-from-rseed philosophy
  2. X-Wing KDF bindinghybrid_kdf(ss_ecdh, ss_pq, ct_pq, epk) prevents transcript binding attacks even if ML-KEMs IND-CCA2 is weakened
  3. ss_pq in out_ciphertext — OVK recovery needs the ML-KEM shared secret; out plaintext grows from 64 to 96 bytes (+16 tag = 112)
  4. ct_pq in CompactAction — light clients get full PQ protection at cost of ~1088 extra bytes
  5. ek_pq bound in encaps randomness — prevents cross-key randomness reuse
  6. Result returns for encapsulationencapsulate_deterministic returns Result<_, PqError> instead of panicking; decapsulate is infallible (ML-KEM implicit rejection)

Test plan

  • All 63 unit tests pass in hybrid mode (cargo test --features hybrid-kem)
  • Integration test passes in hybrid mode (1 test)
  • All 52 unit tests pass in classic mode (cargo test)
  • Integration test passes in classic mode (1 test)
  • Clippy clean in both modes (only pre-existing manual_div_ceil in fixed_bases.rs)
  • 12 hybrid-specific tests: 7 hybrid_kem unit + 5 note_encryption::hybrid_tests
  • CI passes

Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added hybrid post-quantum key encapsulation support with ML-KEM-768 combined with classical elliptic curve cryptography for quantum-resistant note encryption.
    • Extended address format to include per-diversifier post-quantum encapsulation keys when enabled.
  • Documentation

    • Added comprehensive design documentation for hybrid post-quantum key encapsulation.
    • Added key generation and address derivation user guide.
  • Chores

    • Updated Minimum Supported Rust Version (MSRV) from 1.70 to 1.85.1.
    • Updated dependencies for compatibility with hybrid-KEM implementation.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 3, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces hybrid post-quantum key encapsulation to Orchard by combining classical ECDH with deterministic ML-KEM-768. It updates dependencies (MSRV 1.70→1.85.1, adds ml-kem), implements a new hybrid KEM module, redesigns addresses to distinguish classical and PQ components, extends key material with PQ seeds, updates note encryption with hybrid KDF and per-diversifier PQ operations, integrates changes into builders and PCZT, and provides comprehensive design documentation.

Changes

Hybrid Post-Quantum Key Encapsulation

Layer / File(s) Summary
Foundation: Dependencies and Crypto Module
Cargo.toml, src/hybrid_kem.rs
MSRV updated to 1.85.1; optional ml-kem (v0.2, deterministic) dependency added; zcash_note_encryption git revision updated. Core hybrid_kem module implements deterministic PQ keypair generation from spending-key-derived seed, per-diversifier PQ operations, deterministic encapsulation/decapsulation with error handling, hybrid KDF combining ECDH and PQ material with explicit input binding, and diversifier hint encryption via ECDH-derived XOR mask. Includes unit tests for determinism, KDF input binding, per-diversifier derivation, and round-trip encryption/decryption.
Address Structure: Classical and Hybrid Split
src/address.rs
Introduces hybrid-only RawAddress (43-byte on-chain classical component) with serialization, deserialization, and diversifier/base-point accessors. Hybrid-only Address wraps RawAddress plus PqEncapsulationKey; equality compares only raw value for backward compatibility. Non-hybrid mode aliases RawAddress to classic Address. Both modes provide raw() and into_raw() helpers.
Key Material: PQ Seed Integration
src/keys.rs
SpendingKey gains pq_seed() method. FullViewingKey and IncomingViewingKey extended with optional pq_seed (ignored in equality); address derivation wraps RawAddress with per-diversifier PQ encapsulation keys via new wrap_raw_address helper. PreparedIncomingViewingKey restructured as named struct with ecdh scalar and optional pq_seed. EphemeralSecretKey changed from tuple-newtype to struct with ecdh scalar and optional pq_randomness. New public types: PqSeed (per-diversifier keypair derivation), PqEncapsulationKey, PqDecapsulationKey, HybridSharedSecret (carries ECDH/PQ/ciphertext with hybrid KDF method).
Note Structures: Recipient and Ciphertext Updates
src/note.rs
Note recipient changed from Address to RawAddress; hybrid-gated ek_pq field (Option) populated when Address passed to Note::new. TransmittedNoteCiphertext extended with hybrid ct_pq ([u8;1088]) and diversifier_hint ([u8;11]); out_ciphertext size variant ([u8;112] hybrid vs [u8;80] classic). Separate from_parts overloads for classic/hybrid modes. Property test generators updated to construct notes with raw recipients and populated ek_pq in hybrid mode.
Encryption Flow: Domain Trait, Key Agreement, KDF
src/note_encryption.rs
OrchardDomain associated types conditionally update (SharedSecret → HybridSharedSecret, DiversifiedTransmissionKey → OrchardDTK wrapping PQ context, Recipient → RawAddress). ka_agree_enc performs deterministic PQ encapsulation or consumes recovery material; ka_agree_dec provides ECDH-only fallback; new ka_agree_dec_with_pq decrypts hint and decapsulates with deterministic fallback. kdf conditionally selects hybrid vs classic derivation. outgoing_plaintext_bytes constructs hybrid payload (pk_d | esk.ecdh | ss_pq); extract_pk_d returns transport wrapper with recovery material. batch_kdf becomes per-item hybrid_kdf when PQ material present. Note plaintext version byte becomes 0x03 (hybrid) vs 0x02 (classic); compact-note parsing accepts both in hybrid mode. ShieldedOutput impls expose pq_ciphertext() and diversifier_hint() accessors. hybrid_tests module covers encryption/decryption round trips, recovery, determinism, unlinkability, and error cases.
Protocol Integration: Builder, PCZT, Commitments
src/builder.rs, src/pczt.rs, src/pczt/parse.rs, src/bundle/commitments.rs
OutputInfo::build clones recipient and refactors ciphertext derivation; hybrid mode encapsulates ct_pq and computes diversifier_hint deterministically. OutputInfo::into_pczt serializes recipient as raw bytes. Property generators updated to clone/pass raw addresses and notes. PCZT Spend and Output structs change recipient from Option to Option. pczt/parse.rs updates recipient parsing to RawAddress; hybrid mode adds ct_pq/diversifier_hint parameters with error variants for missing fields. bundle/commitments.rs adds conditional ct_pq update to noncompact hash in hybrid mode.
Public APIs and Module Exports
src/bundle.rs, src/lib.rs
Bundle decryption/recovery public methods (decrypt_outputs_with_keys, decrypt_output_with_key, recover_outputs_with_ovks, recover_output_with_ovk) return RawAddress instead of Address. lib.rs conditionally exports hybrid_kem module and extends address re-exports to include RawAddress.
Tests and Benchmarks: Generators and Benchmark Fixes
src/action.rs, benches/circuit.rs, benches/note_decryption.rs, tests/builder.rs
Property-test generators in action.rs split TransmittedNoteCiphertext::from_parts into hybrid-gated branches with appropriate ciphertext field sizes. Benchmarks clone recipient to avoid move conflicts across multiple add_output calls.
Design and User Documentation
book/src/SUMMARY.md, book/src/design/hybrid-kem.md, book/src/user/keys.md
SUMMARY.md adds link to hybrid-kem design doc. design/hybrid-kem.md provides comprehensive spec: per-diversifier deterministic PQ key derivation, two-level derivation from spending key, address structure (RawAddress + Address with ek_pq), Base58 encoding (1,227 bytes), encryption flow (ECDH+ML-KEM, masked hint, hybrid KDF), on-chain scanning/decryption procedure, performance implications (~1ms ML-KEM KeyGen, 3–6x slower scans), security properties, size impact (1,184 bytes address, 1,088-byte ciphertext, 11-byte hint), and design trade-offs. user/keys.md adds key generation section: ZIP 32 derivation, PQ encapsulation keypair generation, address formats (classical 43-byte vs hybrid 1,227-byte), RawAddress vs Address distinction, Base58 serialization, address scopes, and viewing key usage patterns.

Sequence Diagram(s)

sequenceDiagram
  participant Sender
  participant HybridKEM
  participant OrchardDomain
  participant ECDH
  participant Recipient
  Sender->>OrchardDomain: Note::new(Address with ek_pq)
  OrchardDomain->>OrchardDomain: extract ek_pq, convert to RawAddress
  Sender->>ECDH: derive ephemeral keypair
  ECDH->>OrchardDomain: (epk, esk.ecdh)
  Sender->>HybridKEM: derive_pq_encaps_randomness(rseed, rho, ek_pq)
  HybridKEM->>OrchardDomain: randomness
  Sender->>HybridKEM: encapsulate_deterministic(ek_pq, randomness)
  HybridKEM->>OrchardDomain: (ct_pq, ss_pq)
  Sender->>OrchardDomain: hybrid_kdf(ss_ecdh, ss_pq, ct_pq, epk)
  OrchardDomain->>OrchardDomain: symmetric_key [32 bytes]
  Sender->>OrchardDomain: outgoing_plaintext_bytes(pk_d, esk.ecdh, ss_pq)
  Sender->>OrchardDomain: AEAD encrypt note
  Sender->>Recipient: (epk, ct_pq, diversifier_hint, enc_ciphertext, out_ciphertext)
  Recipient->>HybridKEM: decrypt_diversifier_hint(ss_ecdh, epk, hint)
  HybridKEM->>OrchardDomain: diversifier
  Recipient->>HybridKEM: derive_pq_seed_for_diversifier(master_seed, diversifier)
  HybridKEM->>OrchardDomain: pq_seed
  Recipient->>HybridKEM: generate_pq_keypair_for_diversifier(pq_seed, diversifier)
  HybridKEM->>OrchardDomain: (ek_pq, dk_pq)
  Recipient->>HybridKEM: decapsulate(dk_pq, ct_pq)
  HybridKEM->>OrchardDomain: ss_pq
  Recipient->>OrchardDomain: hybrid_kdf(ss_ecdh, ss_pq, ct_pq, epk)
  OrchardDomain->>Recipient: symmetric_key
  Recipient->>OrchardDomain: AEAD decrypt note
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Quantum whispers in the fields of Orchard so bright,
ML-KEM and ECDH dance through the night,
Per-diversifier secrets, deterministically grown,
Harvest-now-decrypt deferred, the future's now shown!
With hints masked and hybrid keys strong,
The garden stands ready, post-quantum and long. 🌙✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and specifically describes the main change: implementing a hybrid KEM combining ML-KEM-768 with ECDH for quantum-resistant note encryption, which aligns with the comprehensive changeset across multiple files and modules.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/hybrid-kem

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
src/address.rs (1)

1-130: ⚠️ Potential issue | 🟡 Minor

Run rustfmt for this file to unblock CI.

This file is listed in the current formatting failures.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/address.rs` around lines 1 - 130, The file containing the Address struct
and its impl (symbols: Address, from_parts, from_parts_with_pq,
to_raw_address_bytes, from_raw_address_bytes) is not rustfmt-formatted; run the
Rust formatter (cargo fmt / rustfmt) on the crate to reformat that file and
commit the changes so CI passes, ensuring you format the module that defines
Address and its associated methods (including the testing module and any
cfg-annotated items).
src/pczt/verify.rs (1)

1-221: ⚠️ Potential issue | 🟡 Minor

Run rustfmt for this file to unblock CI.

The pipeline already reports cargo fmt -- --check diffs in this file; please format and re-push.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pczt/verify.rs` around lines 1 - 221, Run rustfmt (e.g., cargo fmt) to
apply standard formatting to this file and commit the resulting changes;
specifically ensure formatting is applied across the impls and items such as
Action::verify_cv_net, Spend::fvk_for_validation, Spend::verify_nullifier,
Spend::verify_rk, Output::verify_note_commitment, and the VerifyError
enum/Display impl so match arms, spacing, and wrapping conform to rustfmt's
rules, then push the formatted file to unblock the CI check.
src/keys.rs (1)

475-499: ⚠️ Potential issue | 🟠 Major

Hybrid-KEM material is lost on key serialization round-trip, with no documented alternative.

The to_bytes() and from_bytes() methods for FullViewingKey (lines 475–499) and IncomingViewingKey (lines 731–746) serialize to the Zcash Protocol Spec formats (96 and 64 bytes respectively), which predate PQ extensions. Deserialization unconditionally sets ek_pq and dk_pq to None, discarding any hybrid-KEM material.

In hybrid mode, users who persist and reload viewing keys will silently lose PQ capability, potentially downgrading security or breaking hybrid-aware transaction processing. While direct derivation methods like internal_ivk() do preserve PQ fields, serialization does not, creating an inconsistent user experience with no documented workaround.

Add either a versioned, PQ-preserving serialization path (or a separate hybrid key export/import API) with tests verifying PQ material survives persistence and reload.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/keys.rs` around lines 475 - 499, The to_bytes/from_bytes implementations
for FullViewingKey (and analogous IncomingViewingKey) currently serialize only
the legacy 96/64-byte fields and unconditionally set ek_pq/dk_pq to None, losing
hybrid-KEM material; update serialization to preserve PQ fields by introducing a
versioned encoding or separate hybrid export/import API: add new methods (e.g.,
to_bytes_v2/from_bytes_v2 or export_hybrid/import_hybrid) that include ek_pq and
dk_pq when the "hybrid-kem" feature is enabled, tag the byte stream with a small
version header so from_bytes can detect and parse legacy vs v2 formats, and add
tests that round-trip FullViewingKey::ek_pq/dk_pq (and IncomingViewingKey
equivalents) to ensure PQ material survives persistence and reload while keeping
the existing to_bytes/from_bytes behavior backward-compatible.
src/pczt.rs (1)

1-1: ⚠️ Potential issue | 🟡 Minor

Fix formatting issues flagged by CI.

The pipeline indicates cargo fmt -- --check detected formatting diffs. Run cargo fmt to resolve.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pczt.rs` at line 1, CI flagged formatting diffs; run rustfmt (cargo fmt)
across the crate to reformat src/pczt.rs and other files, then commit the
changes. Specifically, apply cargo fmt so the module-level doc comment ("PCZT
support for Orchard") and surrounding code in pczt.rs conform to rustfmt rules;
if you use rustfmt config, ensure it's applied consistently and include the
resulting changes in your PR.
src/builder.rs (1)

1-1: ⚠️ Potential issue | 🟡 Minor

Fix formatting issues flagged by CI.

The pipeline indicates cargo fmt -- --check detected formatting diffs. Run cargo fmt to resolve.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/builder.rs` at line 1, CI reported formatting diffs for src/builder.rs;
run rustfmt (cargo fmt) to apply the expected formatting changes, then re-run
cargo fmt -- --check to confirm. Specifically, format the module/file that
contains the Orchard transaction-building logic (src/builder.rs) so that
comments, docstrings (e.g., the //! module-level comment) and all
function/struct definitions in that file conform to rustfmt rules; commit the
resulting changes.
src/note_encryption.rs (2)

675-682: ⚠️ Potential issue | 🟠 Major

Hybrid compact flow is incomplete: CompactAction drops ct_pq, so compact decryption cannot work.

At Line 675 and Line 690, CompactAction does not store/copy PQ ciphertext; at Line 703 it also does not expose pq_ciphertext(). Downstream impact is visible in Line 1032+ where hybrid compact decryption is expected to fail.

Proposed fix sketch
 pub struct CompactAction {
     nullifier: Nullifier,
     cmx: ExtractedNoteCommitment,
     ephemeral_key: EphemeralKeyBytes,
     enc_ciphertext: [u8; COMPACT_NOTE_SIZE],
+    #[cfg(feature = "hybrid-kem")]
+    ct_pq: [u8; PQ_CT_SIZE],
 }

 impl<T, M: MemoSize> From<&Action<T, M>> for CompactAction {
     fn from(action: &Action<T, M>) -> Self {
         CompactAction {
             nullifier: *action.nullifier(),
             cmx: *action.cmx(),
             ephemeral_key: action.ephemeral_key(),
             enc_ciphertext: action.encrypted_note().enc_ciphertext.as_ref()[..COMPACT_NOTE_SIZE]
                 .try_into()
                 .unwrap(),
+            #[cfg(feature = "hybrid-kem")]
+            ct_pq: action.encrypted_note().ct_pq,
         }
     }
 }

 impl<M: MemoSize> ShieldedOutput<OrchardDomain<M>> for CompactAction {
@@
     fn enc_ciphertext_compact(&self) -> NoteBytesData<COMPACT_NOTE_SIZE> {
         NoteBytesData(self.enc_ciphertext)
     }
+
+    #[cfg(feature = "hybrid-kem")]
+    fn pq_ciphertext(&self) -> Option<&[u8]> {
+        Some(&self.ct_pq)
+    }
 }

Also applies to: 690-699, 703-719, 1032-1091

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/note_encryption.rs` around lines 675 - 682, CompactAction currently omits
the PQ hybrid ciphertext (ct_pq), so hybrid compact decryption cannot access the
PQ payload; add a field to CompactAction (e.g., ct_pq: [u8; PQ_CIPHERTEXT_SIZE]
or Vec<u8> matching how PQ ciphertexts are represented), populate/copy that
field wherever CompactAction instances are constructed (ensure the
constructor/new creation code copies the PQ ciphertext into the struct),
implement and export an accessor pq_ciphertext() on CompactAction that returns
the stored PQ ciphertext (by slice or fixed-size array reference), and update
Clone/serde/serialization logic if present so ct_pq is preserved during
clone/serialize/deserialize operations; reference the CompactAction struct,
enc_ciphertext, and pq_ciphertext() to locate where to add and wire this field.

4-1192: ⚠️ Potential issue | 🟡 Minor

CI is currently failing on formatting for this file.

cargo fmt -- --check reports diffs in src/note_encryption.rs. Please run cargo fmt before merging.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/note_encryption.rs` around lines 4 - 1192, The file fails rustfmt checks;
run rustfmt/cargo fmt to apply formatting changes across the note encryption
implementation (e.g. around OrchardDomain, prf_ock_orchard, CompactAction, and
associated impls). Fix by running `cargo fmt` (or `rustup component add rustfmt`
then `cargo fmt`) and committing the resulting whitespace/format diffs so the
implementations (including functions like prf_ock_orchard,
orchard_parse_note_plaintext_without_memo, OrchardDomain::note_plaintext_bytes,
outgoing_plaintext_bytes, and the CompactAction impls) are style-compliant.
🧹 Nitpick comments (1)
src/note.rs (1)

327-333: Use shared constants instead of hard-coded PQ sizes.

Line 329, Line 333, Line 362, and Line 363 embed protocol sizes directly. Prefer crate::hybrid_kem::PQ_CT_SIZE and a single constant for hybrid out-ciphertext length to prevent drift.

Also applies to: 362-364

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/note.rs` around lines 327 - 333, Replace hard-coded PQ lengths with
shared constants: change the ct_pq field declaration (symbol ct_pq) to use
crate::hybrid_kem::PQ_CT_SIZE instead of 1088, and replace the out_ciphertext
field (symbol out_ciphertext) to use a single shared constant (e.g.
crate::hybrid_kem::PQ_OUT_CT_SIZE) instead of 112; also update the other
occurrences that embed 1088/112 (the references around the later block where the
same sizes are used) to use the same two constants so all PQ ciphertext and
hybrid out-ciphertext sizes come from crate::hybrid_kem constants.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Cargo.toml`:
- Line 45: The zcash_note_encryption dependency in Cargo.toml is using a local
path which breaks CI; replace the path entries for zcash_note_encryption (both
occurrences currently specifying path = "../zcash_note_encryption" and the
variant with features) with a resolvable source such as a crates.io version or a
git = "..." reference (and keep the features key if needed), and remove the path
attribute from the committed Cargo.toml; for local development, document or add
a developer-only override using .cargo/config.toml or a [patch.crates-io] entry
to point to the local path so CI and non-local builds resolve correctly.

In `@src/builder.rs`:
- Around line 390-412: The code currently inserts a zeroed ct_pq when
recipient.ek_pq() is None, producing undecryptable ciphertext; replace that
silent fallback with a development-time check by adding a debug_assert that
recipient.ek_pq().is_some() (or equivalent) before constructing ct_pq so
developers are alerted if an address lacks ek_pq, then keep the existing
behavior in release (i.e., still use the zeroed array only if assertions are
disabled). Update the block around recipient.ek_pq() /
TransmittedNoteCiphertext::from_parts in the builder so the debug_assert
references recipient.ek_pq(), note.esk(), and the created ct_pq to make the
failure point clear (matching other code paths that panic in this scenario).

In `@src/hybrid_kem.rs`:
- Around line 1-327: CI is failing because this file is not rustfmt-formatted;
run the formatter (cargo fmt) to reformat the module containing derive_pq_seed,
generate_pq_keypair, derive_pq_encaps_randomness, encapsulate_deterministic,
decapsulate, hybrid_kdf and the tests, then add the resulting changes to the PR
(or run rustfmt with your project's config if custom) so cargo fmt -- --check
passes.

---

Outside diff comments:
In `@src/address.rs`:
- Around line 1-130: The file containing the Address struct and its impl
(symbols: Address, from_parts, from_parts_with_pq, to_raw_address_bytes,
from_raw_address_bytes) is not rustfmt-formatted; run the Rust formatter (cargo
fmt / rustfmt) on the crate to reformat that file and commit the changes so CI
passes, ensuring you format the module that defines Address and its associated
methods (including the testing module and any cfg-annotated items).

In `@src/builder.rs`:
- Line 1: CI reported formatting diffs for src/builder.rs; run rustfmt (cargo
fmt) to apply the expected formatting changes, then re-run cargo fmt -- --check
to confirm. Specifically, format the module/file that contains the Orchard
transaction-building logic (src/builder.rs) so that comments, docstrings (e.g.,
the //! module-level comment) and all function/struct definitions in that file
conform to rustfmt rules; commit the resulting changes.

In `@src/keys.rs`:
- Around line 475-499: The to_bytes/from_bytes implementations for
FullViewingKey (and analogous IncomingViewingKey) currently serialize only the
legacy 96/64-byte fields and unconditionally set ek_pq/dk_pq to None, losing
hybrid-KEM material; update serialization to preserve PQ fields by introducing a
versioned encoding or separate hybrid export/import API: add new methods (e.g.,
to_bytes_v2/from_bytes_v2 or export_hybrid/import_hybrid) that include ek_pq and
dk_pq when the "hybrid-kem" feature is enabled, tag the byte stream with a small
version header so from_bytes can detect and parse legacy vs v2 formats, and add
tests that round-trip FullViewingKey::ek_pq/dk_pq (and IncomingViewingKey
equivalents) to ensure PQ material survives persistence and reload while keeping
the existing to_bytes/from_bytes behavior backward-compatible.

In `@src/note_encryption.rs`:
- Around line 675-682: CompactAction currently omits the PQ hybrid ciphertext
(ct_pq), so hybrid compact decryption cannot access the PQ payload; add a field
to CompactAction (e.g., ct_pq: [u8; PQ_CIPHERTEXT_SIZE] or Vec<u8> matching how
PQ ciphertexts are represented), populate/copy that field wherever CompactAction
instances are constructed (ensure the constructor/new creation code copies the
PQ ciphertext into the struct), implement and export an accessor pq_ciphertext()
on CompactAction that returns the stored PQ ciphertext (by slice or fixed-size
array reference), and update Clone/serde/serialization logic if present so ct_pq
is preserved during clone/serialize/deserialize operations; reference the
CompactAction struct, enc_ciphertext, and pq_ciphertext() to locate where to add
and wire this field.
- Around line 4-1192: The file fails rustfmt checks; run rustfmt/cargo fmt to
apply formatting changes across the note encryption implementation (e.g. around
OrchardDomain, prf_ock_orchard, CompactAction, and associated impls). Fix by
running `cargo fmt` (or `rustup component add rustfmt` then `cargo fmt`) and
committing the resulting whitespace/format diffs so the implementations
(including functions like prf_ock_orchard,
orchard_parse_note_plaintext_without_memo, OrchardDomain::note_plaintext_bytes,
outgoing_plaintext_bytes, and the CompactAction impls) are style-compliant.

In `@src/pczt.rs`:
- Line 1: CI flagged formatting diffs; run rustfmt (cargo fmt) across the crate
to reformat src/pczt.rs and other files, then commit the changes. Specifically,
apply cargo fmt so the module-level doc comment ("PCZT support for Orchard") and
surrounding code in pczt.rs conform to rustfmt rules; if you use rustfmt config,
ensure it's applied consistently and include the resulting changes in your PR.

In `@src/pczt/verify.rs`:
- Around line 1-221: Run rustfmt (e.g., cargo fmt) to apply standard formatting
to this file and commit the resulting changes; specifically ensure formatting is
applied across the impls and items such as Action::verify_cv_net,
Spend::fvk_for_validation, Spend::verify_nullifier, Spend::verify_rk,
Output::verify_note_commitment, and the VerifyError enum/Display impl so match
arms, spacing, and wrapping conform to rustfmt's rules, then push the formatted
file to unblock the CI check.

---

Nitpick comments:
In `@src/note.rs`:
- Around line 327-333: Replace hard-coded PQ lengths with shared constants:
change the ct_pq field declaration (symbol ct_pq) to use
crate::hybrid_kem::PQ_CT_SIZE instead of 1088, and replace the out_ciphertext
field (symbol out_ciphertext) to use a single shared constant (e.g.
crate::hybrid_kem::PQ_OUT_CT_SIZE) instead of 112; also update the other
occurrences that embed 1088/112 (the references around the later block where the
same sizes are used) to use the same two constants so all PQ ciphertext and
hybrid out-ciphertext sizes come from crate::hybrid_kem constants.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c6d8faf and 2ef0fd2.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • Cargo.toml
  • benches/circuit.rs
  • benches/note_decryption.rs
  • rust-toolchain.toml
  • src/action.rs
  • src/address.rs
  • src/builder.rs
  • src/bundle/commitments.rs
  • src/hybrid_kem.rs
  • src/keys.rs
  • src/lib.rs
  • src/note.rs
  • src/note_encryption.rs
  • src/pczt.rs
  • src/pczt/parse.rs
  • src/pczt/prover.rs
  • src/pczt/verify.rs
  • tests/builder.rs

Comment thread Cargo.toml Outdated
Comment thread src/builder.rs
Comment thread src/hybrid_kem.rs
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/note_encryption.rs (1)

329-334: Hybrid output path currently repeats ML‑KEM encapsulation work.

ka_agree_enc encapsulates once, outgoing_plaintext_bytes encapsulates again, and output assembly computes ct_pq again in builder. This is avoidable cost and increases drift risk if one path changes.

Refactor direction
  • Compute (ct_pq, ss_pq) once per note.
  • Reuse that material for:
    • hybrid shared-secret/KDF path,
    • outgoing_plaintext_bytes (ss_pq),
    • transmitted ct_pq.

Also applies to: 467-473

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/note_encryption.rs` around lines 329 - 334, The hybrid ML‑KEM
encapsulation is being run multiple times; change ka_agree_enc,
outgoing_plaintext_bytes, and the builder assembly so you call
hybrid_kem::encapsulate_deterministic(&ek_pq.0, &esk.pq_randomness) exactly once
per note and capture its result (ct_pq, ss_pq), then pass ss_pq into the
HybridSharedSecret construction (instead of re-encapsulating), into
outgoing_plaintext_bytes, and finally use the same ct_pq when assembling the
transmitted ciphertext; apply the same single-call refactor for the other
occurrence around the ka_agree_enc/outgoing_plaintext_bytes/builder code at the
later block (lines referenced 467-473) so both paths reuse the same (ct_pq,
ss_pq) tuple.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Cargo.toml`:
- Line 45: Replace the git dependency declaration for zcash_note_encryption that
currently uses branch = "feat/hybrid-kem-support" with an immutable rev =
"db0eef3c62367d72e6d3728cefa507b8c374b248" so the Cargo.toml entry for the
zcash_note_encryption dependency (symbol: zcash_note_encryption) is pinned to
the exact commit instead of a moving branch; update both occurrences that
reference that branch to use the rev value to ensure reproducible,
tamper-resistant builds.

In `@src/builder.rs`:
- Around line 398-402: Replace the hard panic caused by expect on
crate::hybrid_kem::encapsulate_deterministic with proper error handling: call
encapsulate_deterministic for ek_pq (using ek_pq.0 and esk.pq_randomness) and
propagate or handle the Result instead of unwrapping—either return an Err from
the surrounding output-build function (propagate with ? or map_err) or
gracefully fall back (log the error and skip/mark ct_pq) consistent with the
hybrid fallback behavior used elsewhere; ensure ct_pq remains correctly set when
success and the function signature is updated to return a Result if needed.

---

Nitpick comments:
In `@src/note_encryption.rs`:
- Around line 329-334: The hybrid ML‑KEM encapsulation is being run multiple
times; change ka_agree_enc, outgoing_plaintext_bytes, and the builder assembly
so you call hybrid_kem::encapsulate_deterministic(&ek_pq.0, &esk.pq_randomness)
exactly once per note and capture its result (ct_pq, ss_pq), then pass ss_pq
into the HybridSharedSecret construction (instead of re-encapsulating), into
outgoing_plaintext_bytes, and finally use the same ct_pq when assembling the
transmitted ciphertext; apply the same single-call refactor for the other
occurrence around the ka_agree_enc/outgoing_plaintext_bytes/builder code at the
later block (lines referenced 467-473) so both paths reuse the same (ct_pq,
ss_pq) tuple.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2ef0fd2 and 378a4fe.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • Cargo.toml
  • src/address.rs
  • src/builder.rs
  • src/hybrid_kem.rs
  • src/note_encryption.rs
  • src/pczt.rs
  • src/pczt/verify.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/pczt.rs

Comment thread Cargo.toml Outdated
Comment thread src/builder.rs Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@book/src/design/hybrid-kem.md`:
- Around line 19-21: The fenced code blocks in hybrid-kem.md are missing
language specifiers which triggers markdownlint MD040; update each
triple-backtick fence containing expressions like pq_seed =
BLAKE2b-512("DashPQ_KeyDerive", sk), pq_seed_d = BLAKE2b-512("DashPQ_DivSeed__",
pq_seed || d), and the mask/hint block to use a language tag (suggest "text" or
"none") so the blocks become ```text ... ``` (apply the same change for the
other occurrences noted around lines 24-26 and 113-116).

In `@src/hybrid_kem.rs`:
- Around line 175-181: The public error enum PqError should be marked
non-exhaustive to allow adding variants later; update the PqError declaration by
adding the #[non_exhaustive] attribute above the enum (refer to PqError) so
external consumers cannot exhaustively match on it and future variants won't be
a breaking change.

In `@src/keys.rs`:
- Around line 408-411: The methods FullViewingKey::address_at and
FullViewingKey::address call wrap_raw_address which expects pq_seed and thus can
panic for FVKs constructed via from_bytes (pq_seed: None); change these APIs to
be fallible by returning Option/Result or add non-panicking variants (e.g.,
try_address_at and try_address) that return an error/None when pq_seed is
missing, update wrap_raw_address to return a fallible type or provide a
try_wrap_raw_address helper that checks self.pq_seed, and replace downstream
call sites to use the new fallible methods (or propagate the Result/Option) so
deserialized hybrid-mode FVKs no longer panic; apply the same changes to the
other affected methods referenced in the review (the address/address_at pairs
around lines 414-422, 429-437, 489-500).

In `@src/note_encryption.rs`:
- Around line 327-337: The fallback branch that silences errors from
hybrid_kem::encapsulate_deterministic currently returns HybridSharedSecret with
zeroed pq_ss and ct_pq, which makes the recipient's PQ decapsulation path fail
deterministically; instead propagate the encapsulation error (i.e., return a
Result::Err or bubble the error) from the enclosing function rather than
constructing zeroed values so the caller can handle/abort encryption; update
both occurrences around the HybridSharedSecret construction (the match on
hybrid_kem::encapsulate_deterministic producing (ct_pq, pq_ss) and the second
similar block at lines ~394-423) to return the error using the function's error
type instead of returning zeroed pq_ss/ct_pq.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 378a4fe and 68b2c6e.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • .github/workflows/ci.yml
  • .github/workflows/lints-stable.yml
  • Cargo.toml
  • book/src/SUMMARY.md
  • book/src/design/hybrid-kem.md
  • book/src/user/keys.md
  • src/action.rs
  • src/address.rs
  • src/builder.rs
  • src/bundle.rs
  • src/constants/fixed_bases.rs
  • src/hybrid_kem.rs
  • src/keys.rs
  • src/lib.rs
  • src/note.rs
  • src/note_encryption.rs
  • src/pczt.rs
  • src/pczt/parse.rs
✅ Files skipped from review due to trivial changes (1)
  • book/src/SUMMARY.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/action.rs

Comment thread book/src/design/hybrid-kem.md Outdated
Comment thread src/hybrid_kem.rs
Comment thread src/keys.rs
Comment thread src/note_encryption.rs Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
src/builder.rs (1)

391-399: ⚠️ Potential issue | 🟠 Major

Replace panic-based ML-KEM handling with a typed build error.

Line 397 currently uses .expect(...), which can abort bundle construction instead of returning BuildError. This is still a reliability footgun in the output-build path.

Suggested direction
-            let (ct_pq, _) =
-                crate::hybrid_kem::encapsulate_deterministic(&ek_pq.0, &esk.pq_randomness).expect(
-                    "ML-KEM encapsulation must succeed for keys derived from a valid spending key",
-                );
+            let (ct_pq, _) = crate::hybrid_kem::encapsulate_deterministic(
+                &ek_pq.0,
+                &esk.pq_randomness,
+            )
+            .map_err(BuildError::HybridKemEncapsulation)?;

You’d then propagate Result from this build path and add a BuildError variant for PQ encapsulation failure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/builder.rs` around lines 391 - 399, Replace the panic (.expect) in the
ML-KEM deterministic encapsulation path with a propagated BuildError: change the
block that computes encrypted_note (calling
crate::hybrid_kem::encapsulate_deterministic(&ek_pq.0, &esk.pq_randomness)) to
handle the Result and on Err return Err(BuildError::MlKemEncapsulationFailure {
source: err }) (or add a suitably named variant to the BuildError enum), and
update the surrounding build function signature/return flow to propagate Result
so the failure from encapsulate_deterministic is returned instead of panicking;
reference encapsulate_deterministic, encrypted_note, note.esk(),
self.recipient.ek_pq(), and the BuildError enum when making the change.
src/hybrid_kem.rs (1)

175-181: ⚠️ Potential issue | 🟠 Major

Mark PqError as non-exhaustive.

PqError is public and should not be exhaustively matchable by downstream crates.

🔧 Suggested patch
+#[non_exhaustive]
 #[derive(Debug)]
 pub enum PqError {

As per coding guidelines **/*.rs: Make all error enums #[non_exhaustive].

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hybrid_kem.rs` around lines 175 - 181, The public error enum PqError
should be marked non-exhaustive so downstream crates can't match it
exhaustively; add the #[non_exhaustive] attribute directly above the pub enum
PqError declaration in hybrid_kem.rs (leaving the existing variants
EncapsulationFailed and DecapsulationFailed unchanged) so future variants can be
added without breaking consumers.
book/src/design/hybrid-kem.md (1)

19-21: ⚠️ Potential issue | 🟡 Minor

Add language identifiers to fenced code blocks (MD040).

These fences still omit a language tag and will continue to fail markdownlint.

🛠️ Suggested patch
-   ```
+   ```text
    pq_seed = BLAKE2b-512("DashPQ_KeyDerive", sk)
    ```

-   ```
+   ```text
    pq_seed_d = BLAKE2b-512("DashPQ_DivSeed__", pq_seed || d)
    ```

-   ```
+   ```text
    mask = BLAKE2b-256("DashPQ_DivHint__", ss_ecdh_bytes || epk_bytes)[..11]
    hint = diversifier XOR mask
    ```

Also applies to: 24-26, 113-116

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@book/src/design/hybrid-kem.md` around lines 19 - 21, The Markdown fenced code
blocks in hybrid-kem.md are missing language identifiers (e.g., the blocks
containing pq_seed = BLAKE2b-512("DashPQ_KeyDerive", sk), pq_seed_d =
BLAKE2b-512("DashPQ_DivSeed__", pq_seed || d), and the block with mask =
BLAKE2b-256(...)/hint = diversifier XOR mask); update each triple-backtick fence
to include a language tag such as text (```text) so markdownlint rule MD040
passes, and apply the same change to the other occurrences noted (lines around
24–26 and 113–116).
🧹 Nitpick comments (1)
src/note_encryption.rs (1)

1037-1059: Consider extracting a shared hybrid-test fixture/helper.

The same ct_pq derivation + diversifier_hint + Action::from_parts setup is repeated in multiple tests. A small helper would reduce drift and make future field changes safer.

Also applies to: 1102-1123, 1169-1190, 1260-1281, 1323-1344, 1391-1412

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/note_encryption.rs` around lines 1037 - 1059, Multiple tests repeat the
ct_pq derivation, diversifier_hint computation, and Action::from_parts
construction (see ct_pq derived via
crate::hybrid_kem::encapsulate_deterministic, compute_test_hint, and
crate::action::Action::from_parts with
crate::note::TransmittedNoteCiphertext::from_parts); extract those steps into a
small test helper (e.g., a function like make_test_action or
build_hybrid_action) placed in the test module or a test utils file that takes
the varying inputs (note, recipient, epk_bytes, nf, cmx, enc_ciphertext,
out_ciphertext, cv_net) and returns the constructed Action and/or ct_pq and
diversifier_hint so tests at the listed locations can call the helper instead of
duplicating the logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/keys.rs`:
- Around line 550-567: The PqSeed type is crate-private (PqSeed(pub(crate) [u8;
64])) so external code cannot construct one to use APIs like set_pq_seed,
with_pq_seed, or pass bytes from SpendingKey::pq_seed(); fix this by making a
public constructor and/or conversion: add a public associated constructor (e.g.,
PqSeed::new(bytes: [u8;64]) -> Self) or implement From<[u8;64]>/TryFrom<&[u8]>
for PqSeed and make the tuple struct public (pub struct PqSeed(pub [u8;64])) or
keep tuple privacy but expose a pub fn from_bytes/try_from_bytes; ensure the
chosen API is referenced by name (PqSeed::new, From<[u8;64]> for PqSeed,
TryFrom<&[u8]> for PqSeed) so callers can convert SpendingKey::pq_seed() output
into a PqSeed and then call set_pq_seed/with_pq_seed.

---

Duplicate comments:
In `@book/src/design/hybrid-kem.md`:
- Around line 19-21: The Markdown fenced code blocks in hybrid-kem.md are
missing language identifiers (e.g., the blocks containing pq_seed =
BLAKE2b-512("DashPQ_KeyDerive", sk), pq_seed_d = BLAKE2b-512("DashPQ_DivSeed__",
pq_seed || d), and the block with mask = BLAKE2b-256(...)/hint = diversifier XOR
mask); update each triple-backtick fence to include a language tag such as text
(```text) so markdownlint rule MD040 passes, and apply the same change to the
other occurrences noted (lines around 24–26 and 113–116).

In `@src/builder.rs`:
- Around line 391-399: Replace the panic (.expect) in the ML-KEM deterministic
encapsulation path with a propagated BuildError: change the block that computes
encrypted_note (calling crate::hybrid_kem::encapsulate_deterministic(&ek_pq.0,
&esk.pq_randomness)) to handle the Result and on Err return
Err(BuildError::MlKemEncapsulationFailure { source: err }) (or add a suitably
named variant to the BuildError enum), and update the surrounding build function
signature/return flow to propagate Result so the failure from
encapsulate_deterministic is returned instead of panicking; reference
encapsulate_deterministic, encrypted_note, note.esk(), self.recipient.ek_pq(),
and the BuildError enum when making the change.

In `@src/hybrid_kem.rs`:
- Around line 175-181: The public error enum PqError should be marked
non-exhaustive so downstream crates can't match it exhaustively; add the
#[non_exhaustive] attribute directly above the pub enum PqError declaration in
hybrid_kem.rs (leaving the existing variants EncapsulationFailed and
DecapsulationFailed unchanged) so future variants can be added without breaking
consumers.

---

Nitpick comments:
In `@src/note_encryption.rs`:
- Around line 1037-1059: Multiple tests repeat the ct_pq derivation,
diversifier_hint computation, and Action::from_parts construction (see ct_pq
derived via crate::hybrid_kem::encapsulate_deterministic, compute_test_hint, and
crate::action::Action::from_parts with
crate::note::TransmittedNoteCiphertext::from_parts); extract those steps into a
small test helper (e.g., a function like make_test_action or
build_hybrid_action) placed in the test module or a test utils file that takes
the varying inputs (note, recipient, epk_bytes, nf, cmx, enc_ciphertext,
out_ciphertext, cv_net) and returns the constructed Action and/or ct_pq and
diversifier_hint so tests at the listed locations can call the helper instead of
duplicating the logic.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3f81e208-bff3-4997-b615-a425945b3524

📥 Commits

Reviewing files that changed from the base of the PR and between 68b2c6e and b2ed15c.

📒 Files selected for processing (5)
  • book/src/design/hybrid-kem.md
  • src/builder.rs
  • src/hybrid_kem.rs
  • src/keys.rs
  • src/note_encryption.rs

Comment thread src/keys.rs
@codecov
Copy link
Copy Markdown

codecov Bot commented May 24, 2026

Codecov Report

❌ Patch coverage is 86.70757% with 65 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.87%. Comparing base (0910521) to head (0067384).
⚠️ Report is 1 commits behind head on dashified.

Files with missing lines Patch % Lines
src/note_encryption.rs 80.48% 24 Missing ⚠️
src/keys.rs 85.71% 21 Missing ⚠️
src/pczt/parse.rs 71.42% 10 Missing ⚠️
src/note.rs 68.42% 6 Missing ⚠️
src/address.rs 95.55% 2 Missing ⚠️
src/builder.rs 92.59% 2 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##           dashified       #3      +/-   ##
=============================================
+ Coverage      77.71%   79.87%   +2.16%     
=============================================
  Files             40       41       +1     
  Lines           3715     4124     +409     
=============================================
+ Hits            2887     3294     +407     
- Misses           828      830       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

QuantumExplorer and others added 13 commits May 24, 2026 19:32
…ncryption

Adds post-quantum key encapsulation alongside classical ECDH to defend
against "Harvest Now, Decrypt Later" attacks. The hybrid approach derives
symmetric encryption keys from both an ECDH shared secret and an ML-KEM-768
shared secret, so breaking either mechanism alone is insufficient.

Feature-gated behind `hybrid-kem` — when enabled, all encryption uses the
hybrid path; classic mode is fully preserved without the feature flag.

Key changes:
- New `hybrid_kem` module: ML-KEM-768 keygen, encaps, decaps, hybrid KDF
- Key hierarchy: PQ keys derived deterministically from spending key
- Address extended with optional PQ encapsulation key (1184 bytes)
- Note encryption: hybrid KDF binds ss_ecdh, ss_pq, ct_pq, epk (X-Wing)
- Encapsulation randomness bound to rseed + rho + ek_pq
- OVK recovery path threads ct_pq for correct hybrid key derivation
- PCZT support for hybrid ciphertexts
- TransmittedNoteCiphertext carries ct_pq (1088 bytes) and larger out_ciphertext (112 bytes)
- 12 new hybrid-specific tests, all 65 tests pass in hybrid mode
- All 54 classic-mode tests continue to pass

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace local path dependency for zcash_note_encryption with git
  reference to dashpay/zcash_note_encryption feat/hybrid-kem-support
  branch (fixes CI builds)
- Add debug_assert for missing ek_pq in builder hybrid-kem path
- Run cargo fmt to fix all formatting issues

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix clippy manual_div_ceil lint by using .div_ceil() in fixed_bases.rs
- Update wasm32-wasi to wasm32-wasip1 for Rust 1.85 compatibility
- Pin zcash_note_encryption to rev instead of branch for reproducible builds
- Replace expect() panic with graceful error handling in PQ encapsulation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Addresses are reconstructed without ek_pq in several code paths
(from_raw_address_bytes, note decryption recovery), so equality
must compare only (d, pk_d) to avoid false mismatches.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduce RawAddress (43-byte on-chain form) as the internal type used by
Note, PCZT, and decryption paths. Address now wraps RawAddress with a
mandatory PqEncapsulationKey when hybrid-kem is enabled, enforcing at the
type level that the builder always has the PQ key available for hybrid
encryption. When hybrid-kem is off, RawAddress is a type alias for Address.

Also adds book documentation covering the hybrid KEM design (key derivation,
address encoding with Base58, QR considerations, encryption flow, security
properties) and user-facing docs for creating keys and addresses.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the single ML-KEM-768 keypair per spending key with
per-diversifier keypairs derived from a master pq_seed, restoring
address unlinkability that was broken when all diversified addresses
shared the same ek_pq.

Key changes:
- Two-level key derivation: sk → pq_seed → per-diversifier pq_seed_d → ML-KEM keypair
- PqSeed type replaces ek_pq/dk_pq pairs in FVK/IVK/PreparedIVK
- 11-byte ECDH-encrypted diversifier hint on-chain (no MAC for quantum privacy)
- Scanning decrypts hint → derives per-diversifier dk → decapsulates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add set_pq_seed/with_pq_seed to FVK and IVK so deserialized keys can
  re-attach PQ material (prevents panic and silent decryption failure)
- Make PQ encapsulation failure a hard error instead of silently falling
  back to ECDH-only (which produced undecryptable notes)
- Add combined keypair_for_diversifier to avoid double ML-KEM KeyGen
- Rename to_bytes → as_bytes on PqSeed/PqEncapsulationKey (returns ref)
- Remove redundant zero-init + copy in ka_agree_dec_with_pq
- Fix doc inaccuracies: KDF inputs, personalization labels, add
  serialization/PQ-seed section
- Add tests: wrong-IVK failure, non-default diversifier round-trip,
  deserialized IVK with re-attached PQ seed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Make OrchardDTKPq::None in ka_agree_enc unreachable (was silently
  degrading to ECDH-only, defeating PQ protection)
- Make outgoing_plaintext_bytes expect ek_pq and successful encapsulation
  (was silently zeroing ss_pq, breaking OVK recovery)
- Fix HybridSharedSecret doc comment: KDF binds all four inputs
  (both shared secrets, ct_pq, and epk), not just shared secrets
- Rename PqDecapsulationKey::to_bytes → as_bytes for consistency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Test MissingPqCiphertext and MissingDiversifierHint error variants
in Output::parse when ct_pq or diversifier_hint are None.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adapts the hybrid-kem test code to API changes introduced when rebasing
onto orchard 0.13.1:

- `NoteValue::zero()` was replaced by the `NoteValue::ZERO` const.
- `Action::from_parts` now returns `Option` (it rejects an identity rk);
  test call sites unwrap with `.expect`.
- Gate the new `dummy_other_fields` test helper so it builds the
  hybrid-sized `TransmittedNoteCiphertext` under the feature.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Points the dependency at the fork commit that adds the per-diversifier
hybrid KEM trait extension (diversifier_hint + 5-param ka_agree_dec_with_pq),
which the hybrid-kem feature requires. The previously pinned rev (db0eef3c)
only had the basic hybrid trait, so a clean build of hybrid-kem failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a `decrypt-10k` benchmark group measuring 10,000 full trial decryptions
in both the realistic "miss" (foreign note) and "hit" (owned note) cases, and
fixes the bench to derive the recipient via `FullViewingKey::address_at` so it
compiles under `hybrid-kem` (where `ivk.address_at` returns a `RawAddress` that
the builder won't accept).

Updates the hybrid-kem design doc with the measured results: the post-quantum
chain adds ~63 µs/note (~2x) for the dominant foreign-note scanning case —
roughly an order of magnitude below the earlier ~1 ms estimate — replacing the
conservative "3-6x / ~1ms" figures with benchmark-backed numbers and a
reproduction command.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- rustfmt: format the decrypt-10k bench and the hybrid-gated test helper
  so `cargo fmt -- --check` passes.
- book: add `text` language tags to the BLAKE2b pseudo-code fences in the
  hybrid-kem design doc; `mdbook test` treated the untagged blocks as Rust
  and failed to compile them.
- hybrid_kem: mark `PqError` `#[non_exhaustive]` per crate guidelines.
- keys: add `PqSeed::from_bytes` and `impl From<[u8; 64]> for PqSeed` so
  external callers can re-attach a PQ seed (e.g. from `SpendingKey::pq_seed`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds unit tests for the previously-untested hybrid-kem public surface that
codecov/patch flagged (keys.rs was the dominant gap):

- PqSeed: from_bytes / From<[u8;64]> / as_bytes / per-diversifier keygen / Debug
- PqEncapsulationKey & PqDecapsulationKey: from_bytes / as_bytes / Eq / Ord / Debug
- FullViewingKey & IncomingViewingKey: pq_seed / set_pq_seed / with_pq_seed,
  including the deserialize-then-reattach flow
- Address / RawAddress: accessors and the 43-byte raw serialization round-trip

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@QuantumExplorer
Copy link
Copy Markdown
Member Author

QuantumExplorer commented May 24, 2026

Hybrid post-quantum KEM — production-readiness checklist

Issues are disabled on this repo, so tracking this here. Current status: compiles, all tests green, cleanly feature-gated (hybrid-kem), with design docs. The items below are what stand between that and a value-bearing mainnet/consensus deployment.

🔴 Blocking (must-have before mainnet)

  • Independent professional cryptographic audit of the bespoke construction: hybrid KDF binding, deterministic ML-KEM randomness derived from rseed/rho, the un-MAC'd diversifier hint, and the per-diversifier key scheme. (The in-branch "security audit" commits were LLM-generated and are not a substitute.)
  • Review + properly release the dashpay/zcash_note_encryption fork. The diversifier-hint trait extension (diversifier_hint, 5-param ka_agree_dec_with_pq) is AI-authored, unreviewed, and pinned to a feature-branch git rev. Get human review, tag a release (consider upstreaming), pin to the tag.
  • Resolve the ml-kem dependency. RustCrypto ml-kem is not independently audited. Audit it or switch to an audited impl; decide 0.2 vs 0.3.x.
  • Cross-implementation KAT test vectors for PQ key derivation, deterministic encapsulation, diversifier hint, full hybrid encrypt/decrypt, and address encoding — committed and validated against an independent implementation (current tests only prove self-consistency → consensus-split risk).
  • Consensus integration plan: network upgrade / activation height, spec doc, fee & block-size policy for larger transactions (+1088B ct_pq, +11B hint), coordinated rollout with full node + wallets; verify ct_pq-in-txid-commitment matches the verifier.
  • Light-client scanning protocol for hybrid notes — compact decryption currently cannot decrypt them. (Codex P1: CompactAction drops ct_pq/hint → light clients miss hybrid funds.)
  • Versioned FVK/IVK serialization preserving pq_seed — first-class opt-in API added in 436fd2d (to_bytes_with_pq / from_bytes_with_pq for FVK=160B, IVK=128B). Canonical 96/64-byte formats stay upstream-compatible and intentionally omit the seed; the seed-less ECDH-only fallback remains documented. (Codex P1.)
  • Make FullViewingKey::address_at / address fallible instead of panicking when pq_seed is absent. — Done in 09ae5f5 via non-panicking try_address_at / try_address returning Result<Address, MissingPqSeed> (keeps classic API upstream-compatible; ergonomic methods now document their panic).
  • Commit diversifier_hint in the bundle txid hash so it can't be flipped without invalidating the commitment (recipient-DoS / malleability). — Done in e7c95b0 with a regression test. (Codex P1.)

🟠 Should-have

  • Fuzz the parse/decrypt paths (PCZT parse; malformed ct_pq / hint / ek_pq; edge diversifiers).
  • Adversarial / negative test coverage beyond happy paths.
  • Cryptographer validation of the privacy claims (quantum-privacy of the hint; "ivk-compromise alone reveals nothing").
  • DoS / performance modeling at network scale (per-output ML-KEM KeyGen during scanning; ~2x scan cost measured).
  • Clean up commit provenance; ensure all AI-generated code is human-reviewed.

🟡 Nice-to-have / follow-ups

  • Hybrid Address full serialization (to_bytes/from_bytes for the 1227-byte RawAddress + ek_pq form, plus HYBRID_ADDRESS_SIZE). — Done in 436fd2d. (Codex P2.)
  • Address-encoding spec (Base58, 1227 bytes) + wallet UX guidance (QR encodes raw binary).
  • Re-evaluate ml-kem 0.3.x migration once stable/audited.
  • CodeRabbit follow-ups: document the CompactAction ct_pq design decision; shared test fixtures to reduce duplication.

🤖 Generated with Claude Code

QuantumExplorer and others added 4 commits May 25, 2026 00:02
Adds non-panicking counterparts to FullViewingKey::address_at/address for the
hybrid-kem feature, returning Result<Address, MissingPqSeed> instead of panicking
when the FVK has no PQ seed attached (e.g. after from_bytes without re-attaching
it). The ergonomic address_at/address remain for the common case (FVK derived
from a SpendingKey) and now document their panic behavior and point to the
try_* variants.

This keeps classic (non-hybrid) Orchard's address_at/address API unchanged for
upstream compatibility, while giving hybrid callers a safe path — addressing the
panic-on-deserialized-FVK review finding.

- New: try_address_at, try_address, and the MissingPqSeed error type
  (Display + std::error::Error, #[non_exhaustive]).
- wrap_raw_address now delegates to a fallible try_wrap_raw_address.
- Test: a seed-less deserialized FVK returns Err(MissingPqSeed), not a panic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 11-byte diversifier hint is transmitted on-chain in each hybrid action but
was not covered by hash_bundle_txid_data (only ct_pq was). That left it
malleable: an attacker could flip the hint without changing the txid/bundle
commitment, causing the recipient to recover the wrong diversifier, derive the
wrong decapsulation key, and fail to decrypt the note (a recipient-side DoS /
griefing vector).

Include diversifier_hint in the non-compact action hash alongside ct_pq so any
mutation invalidates the commitment. Adds a regression test that flipping only
the hint changes the txid commitment.

Reported by Codex review (P1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds opt-in serialization that retains hybrid viewing/sending capability across
a byte round-trip, fixing the silent "missing funds" / dropped-PQ-key footguns
(Codex P1/P2). The canonical upstream formats are unchanged.

- FullViewingKey::to_bytes_with_pq / from_bytes_with_pq (96-byte FVK || 64-byte
  pq_seed = 160 bytes).
- IncomingViewingKey::to_bytes_with_pq / from_bytes_with_pq (64 || 64 = 128 bytes).
- Address::to_bytes / from_bytes for the full 1227-byte hybrid address (43-byte
  RawAddress || 1184-byte ek_pq), with an exported HYBRID_ADDRESS_SIZE const.
  RawAddress::to_raw_address_bytes still emits only the 43-byte classical form.
- Round-trip tests for all three.
- Update the hybrid-kem design doc: PQ-preserving serialization, the fallible
  try_address_at API, the full Address byte API, and that the diversifier hint is
  now committed in the bundle (txid) hash.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The existing design/hybrid-kem.md is a construction/reference spec. This adds a
from-first-principles companion chapter explaining the underlying cryptography:
the harvest-now-decrypt-later threat, ECDH key agreement, what a KEM is, ML-KEM-768
and MLWE, how the hybrid combiner is secure if either primitive holds (X-Wing-style
KDF binding), deterministic encapsulation, and the privacy reasoning behind the
per-diversifier keys and diversifier hint.

Nested under the hybrid-kem chapter in SUMMARY.md, with a cross-link from the spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The doc comment referenced `to_raw_address_bytes` without a path, which
rustdoc could not resolve and `#![deny(rustdoc::broken_intra_doc_links)]`
rejected (failing the Intra-doc links CI check). Qualify it as
`Self::to_raw_address_bytes`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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