Skip to content

Comments

Add FROST threshold signing for Orchard spends#150

Open
zmanian wants to merge 8 commits intozcash:mainfrom
zmanian:frost-tests-and-docs
Open

Add FROST threshold signing for Orchard spends#150
zmanian wants to merge 8 commits intozcash:mainfrom
zmanian:frost-tests-and-docs

Conversation

@zmanian
Copy link

@zmanian zmanian commented Feb 8, 2026

Summary

Add FROST (Flexible Round-Optimized Schnorr Threshold Signatures) support for Zcash Orchard spends, behind the frost feature flag. This enables t-of-n threshold signing for shielded transactions.

Implementation

Three new CLI commands:

  • wallet frost-dkg -- Interactive distributed key generation ceremony. All participants cooperate over three rounds to produce FROST key shares and a shared Orchard Full Viewing Key (ak from DKG, nk/rivk from coordinator). The UFVK is imported as a view-only wallet account; key material is stored in frost.toml (encrypted with age).

  • pczt frost-sign -- Coordinator-side signing ceremony. Reads a PCZT from stdin, extracts sighash and spend auth randomizers (alpha), orchestrates two rounds of message exchange, aggregates signature shares into orchard_redpallas::Signature<SpendAuth>, and applies them to the PCZT via Signer::apply_orchard_signature().

  • pczt frost-participate -- Participant-side signing. Loads the encrypted key package from frost.toml, generates nonce commitments (round 1), and computes signature shares with rerandomization (round 2).

Supporting modules

  • frost_serde.rs -- Serialization layer for all FROST protocol messages (DKG rounds, signing rounds, key packages) via hex-encoded JSON, since PallasBlake2b512 doesn't implement serde directly.
  • frost_config.rs -- FROST configuration file (frost.toml) management with age encryption/decryption for key material at rest.
  • Cargo.toml -- frost feature flag gating frost-core, frost-rerandomized, reddsa, and orchard/unstable-frost dependencies.

Documentation

doc/frost-walkthrough.md covers the full workflow end-to-end:

  • Key architecture (how ak/nk/rivk compose the Orchard FVK)
  • DKG ceremony step-by-step with example commands
  • Threshold signing ceremony with message flow diagram
  • File layout and frost.toml structure
  • Security considerations and troubleshooting

Test coverage

17 tests pass (cargo test --features frost -- frost):

Protocol and serde round-trips (11 tests):

Test What it covers
dkg_full_ceremony_2_of_3 Full 3-participant DKG with serde round-trips on all messages
frost_signing_with_rerandomization End-to-end signing: DKG, commit, sign, aggregate with JSON round-trips
identifier_hex_round_trip IdHex serde for identifiers 1-5
key_package_store_round_trip KeyPackageStore JSON round-trip
public_key_package_store_round_trip PublicKeyPackageStore JSON round-trip
dkg_round1_package_store_round_trip DkgRound1PackageStore JSON round-trip
dkg_round2_package_store_round_trip DkgRound2PackageStore JSON round-trip
signing_commitments_store_round_trip SigningCommitmentsStore JSON round-trip
signing_package_store_round_trip SigningPackageStore JSON round-trip
frost_config_round_trip FrostConfig TOML round-trip
encrypt_decrypt_round_trip age encryption/decryption of key material

Error handling (4 tests):

Test What it covers
id_hex_rejects_invalid_hex Non-hex input rejected
id_hex_rejects_wrong_length Wrong byte length rejected
key_package_store_rejects_truncated_fields Truncated fields rejected
signing_commitments_store_rejects_bad_hex Bad hex in commitments rejected

Zcash wallet bridge (2 tests):

Test What it covers
frost_dkg_to_orchard_fvk_and_address DKG group key to valid Orchard FullViewingKey, address derivation, UnifiedFullViewingKey wrapping, and FVK round-trip (includes ak sign bit retry logic)
frost_signature_to_orchard_spendauth FROST aggregate signature to orchard_redpallas::Signature<SpendAuth> conversion and byte-level round-trip

Test plan

  • cargo test --features frost -- frost passes (17/17)
  • cargo check --features frost compiles clean (no warnings)
  • Reviewer verifies doc accuracy against CLI help output
  • Manual 2-of-3 DKG + signing ceremony test

Generated with Claude Code

Add 13 tests to frost_serde.rs covering the full FROST protocol:
- DKG ceremony (2-of-3) with serde round-trips on all messages
- Signing ceremony with rerandomization and full JSON round-trips
- Standalone round-trip tests for all serde types (IdHex,
  KeyPackageStore, PublicKeyPackageStore, DkgRound1PackageStore,
  DkgRound2PackageStore, SigningCommitmentsStore, SigningPackageStore)
- Error-path tests for invalid hex, wrong lengths, truncated fields

Add doc/frost-walkthrough.md with end-to-end usage guide covering
DKG setup, address generation, chain scanning, threshold signing
ceremony, file layout, security considerations, and troubleshooting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@zmanian zmanian mentioned this pull request Feb 8, 2026
zmanian and others added 3 commits February 8, 2026 15:48
…nversion

Test the two critical integration boundaries between the FROST protocol
and the Zcash wallet: DKG group key to Orchard FullViewingKey (with ak
sign bit retry logic), and FROST aggregate signature to
orchard_redpallas::Signature<SpendAuth>.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a Testing section to the FROST walkthrough cataloguing all 17 tests
across protocol/serde round-trips, error handling, and the new Zcash
wallet bridge tests (FVK construction and signature conversion).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement the three CLI commands behind the `frost` feature flag:

- `wallet frost-dkg`: Interactive distributed key generation ceremony.
  Participants cooperate over three rounds to produce FROST key shares
  and a shared Orchard Full Viewing Key (ak from DKG, nk/rivk from
  coordinator). The resulting UFVK is imported as a view-only wallet
  account and key material is stored in frost.toml (encrypted with age).

- `pczt frost-sign`: Coordinator-side signing ceremony. Reads a PCZT,
  extracts sighash and spend auth randomizers, orchestrates two rounds
  of message exchange with participants, aggregates signature shares,
  converts to orchard_redpallas::Signature<SpendAuth>, and applies
  signatures to the PCZT.

- `pczt frost-participate`: Participant-side signing. Loads the
  encrypted key package from frost.toml, generates nonce commitments
  (round 1), and computes signature shares (round 2).

Supporting modules:
- frost_config.rs: FROST configuration file (frost.toml) management
  with age encryption/decryption for key material at rest.
- Cargo.toml: frost feature flag gating frost-core, frost-rerandomized,
  reddsa, and orchard/unstable-frost dependencies.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@zmanian zmanian changed the title Add FROST integration tests and walkthrough documentation Add FROST threshold signing for Orchard spends Feb 9, 2026
zmanian and others added 4 commits February 9, 2026 06:16
…ntation

Critical:
- Clear high bit on nk/rivk to ensure valid Pallas field elements (~75%
  DKG failure rate without this)
- Check ak sign bit after DKG and fail with clear message instead of
  cryptic FVK construction error
- Participant now verifies sighash in round 2 signing packages matches
  round 1 request (prevents malicious coordinator substitution)
- Alpha byte extraction returns proper error instead of silent corruption
  via unwrap_or(0)

Security:
- Encrypt nk/rivk with age before storing in frost.toml (same as
  key_package)
- Write frost.toml with 0600 permissions on Unix
- Remove unused --identity flag from frost-sign (was accepted but never
  verified)

Correctness:
- Duplicate participant detection in all DKG and signing collection loops
- Cross-round participant set validation before signature aggregation
- UUID formatted with expose_uuid() instead of Debug formatting
- Upper-bound validation on num_signers vs max_signers
- Birthday=0 rejected instead of causing underflow
- Round 2 from_id validated against known round 1 participants
- Duplicate account UUID check before writing to frost.toml
- PublicKeyPackageStore uses BTreeMap for deterministic serialization

Robustness:
- Replace expect() with proper error on block height and UTF-8 encoding
- Eliminate TOCTOU in FrostConfig::read; add fsync on write
- Surface age identity parse errors instead of silently dropping
- Trim input lines before JSON parsing in all interactive rounds
- IdHex inner field restricted to pub(crate)
- Config round-trip test now verifies all fields

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove alpha_hex from ActionSigningData (H2): the spend auth randomizer
  scalar was unnecessarily leaked to participants in the signing request.
  Participants only need the randomizer point (alpha * G) sent in Round 2.
- Reduce PCZT parsing from 3x to 2x (H5): extract alpha values from
  pczt.orchard().actions() before consuming into Signer, eliminating
  one redundant Pczt::parse call.
- Add frost_signing_sighash_mismatch_detected test: exercises the
  participant-side sighash verification from frost_participate.rs.
- Add frost_aggregate_wrong_signer_set_fails test: verifies aggregation
  rejects shares from signers not in the commitment set.
- Add frost_production_fvk_path test: covers the exact FVK construction,
  age encryption round-trip, and key package storage path from frost_dkg.rs.
- Update walkthrough test count from 17 to 20.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- H1: frost_sign reads PCZT from a file argument instead of stdin,
  fixing the broken mixed async/blocking stdin pattern where
  read_to_end consumed stdin to EOF before interactive JSON reads.
- H4: Document the brittle alpha extraction via serde JSON
  introspection with a HACK comment explaining the pczt crate
  limitation (no public getter for Spend.alpha).
- H8: Add security warnings to DKG Round 2 output labeling each
  package as a SECRET SHARE that must be sent privately.
- M9: Remove unnecessary async from frost_participate (all I/O is
  blocking std::io, no async operations).
- Extract duplicate account selection logic into
  FrostConfig::resolve_account(), used by both frost_sign and
  frost_participate.
- Skip empty/whitespace-only lines in all interactive input loops
  (frost_sign, frost_dkg) to prevent confusing JSON parse errors
  from accidental Enter presses.
- Update walkthrough for new frost-sign file argument syntax.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add zeroize dependency (gated behind frost feature) and implement Drop
  for KeyPackageStore to securely erase the signing_share field
- Fix FVK share input to skip empty lines, matching other interactive loops
- Add explanatory comment for AccountPurpose::ViewOnly on FROST accounts

Co-Authored-By: Claude Opus 4.6 <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