Cargo.lock at the commit in the changelog. Pins and verification dates are in docs/VERSIONS.md, the single source for versions. Spec titles are as listed in the ZIP index; see the spec appendix.
Contents
- Shielded pool: Ironwood
- No transparent or Sapling hops
- Unified Addresses, UFVK, UIVK
- Escrow key construction
- Threshold signing
- PCZT
- Fees
- Memos
- Payment requests
- Light client protocol
- Note encryption and scanning
- Bidder wallet keys
- Witnesses and anchors
- Escrow wallet
- Randomness
- Network and consensus
- Dependencies and upstream risk
Shielded pool: Ironwood
- What: escrow and bid notes live in the Ironwood value pool, which uses Orchard keys and receivers. Escrow and wallet proofs use the
PostNu6_3circuit. The scanner trial-decrypts both Orchard and Ironwood actions, so a bid paid from a wallet still on the Orchard pool is seen. - Code:
orchard 0.16.0.OrchardCircuitVersion::PostNu6_3incrates/sealforge-chain/src/spend.rscircuit()andcrates/sealforge-wallet/src/pay.rscircuit(). Change strategyShieldedPool::Ironwoodinspend.rspropose. Scanner:crates/sealforge-scan/src/lib.rs(OrchardVersion,IronwoodVersiondomains). - Why: Ironwood is the current shielded pool on the target networks.
- Not done: the in-browser wallet counts and labels Orchard-pool notes it receives but doesn’t spend them (WALLET-INTEGRATION.md). sealForge doesn’t move funds between pools.
- Spec: ZIP 258 (NU6.3 deployment), ZIP 326 (NU6.3 consequences for wallets), ZIP 2005 (Ironwood quantum recoverability). How the circuit and value pool behave is
orchard’s, as specified there. - Status:
Testnet.
No transparent or Sapling hops
- What: the escrow UA has only an Orchard receiver. Refund and payout addresses MUST decode as a Unified Address for the auction’s network with an Orchard receiver. Sapling-only and transparent addresses are rejected.
- Code:
crates/sealforge-escrow/src/address.rsescrow_address(UnifiedAddress::from_receivers(Some(orchard), None, …)).crates/sealforge-coordinator/src/validate.rsshielded_ua(ua.has_orchard()). At ingest, an unusable refund address is dropped: the bid counts, and its refund goes to manual review (SECURITY-REVIEW F2, testrecords_payments_with_memo_level_checks). - Why: paying a Sapling address from Ironwood crosses pools, which reveals the amount on chain. Transparent outputs reveal everything. One malformed address would also make every refund batch it joined fail to build (F2).
- Not done: no transparent inputs or outputs in any bid, refund or payout flow.
- Spec: ZIP 316 (receiver types).
- Status:
Testnet.
Unified Addresses, UFVK, UIVK
- What: each auction has one escrow UA, one UFVK and one UIVK, all encoded for the auction’s network.
- The UFVK stays private, encrypted at rest (
auctions.escrow_ufvk_enc,pgp_sym_encryptwithSF_DATA_KEY). The coordinator uses it to scan and to build spends. - The UIVK is derived from the UFVK and published at
publish. It contains the Orchard external-scope IVK only (zcash_keys 0.17.0-pre.2UnifiedFullViewingKey::to_unified_incoming_viewing_key, which callsto_ivk(Scope::External)). Auditors see every payment to the escrow. They don’t see outgoing refunds and payout, or the escrow’s internal-scope change.
- The UFVK stays private, encrypted at rest (
- Code:
crates/sealforge-escrow/src/address.rsescrow_view_keys. Publish:crates/sealforge-coordinator/src/routes/auctions.rspublish(decrypts the UFVK, storesescrow_ivk). A DBCHECKallowsescrow_ivkonly inpublished,refundingandsettled. - Why: selective disclosure. Publishing incoming-only viewing power is enough to recompute the result and doesn’t expose who was refunded when.
- Not done: the UIVK is never rotated or scoped per time range. Once published, it is permanent (5 Security, 11 Open questions).
- Spec: ZIP 316.
- Status:
Testnet.
Escrow key construction
- What: the Orchard FVK is
ak ‖ nk ‖ rivk(96 bytes).akis the FROST group verifying key from the trustees’ DKG. Orchard requires theakpoint to have ỹ = 0 (orchard 0.16.0src/keys.rs: “the corresponding public point (ak^ℙ) has ỹ = 0”). The ciphersuite enforces this:reddsa 0.5.2frost::redpallasnegates key packages and the group key into even-Y form in itspost_dkghook (into_even_y).nkandrivkare sampled by the coordinator withOsRng: 64 random bytes, the top two bits of each 32-byte value cleared so both encodings are below the Pallas field moduli, then accepted only iforchard::keys::FullViewingKey::from_bytesaccepts the 96 bytes (rejection sampling, at most 64 tries).
- Code:
crates/sealforge-escrow/src/address.rsescrow_fvk, called fromcrates/sealforge-coordinator/src/routes/dkg.rscompleteasescrow_fvk(&ak, &mut OsRng)once all three trustees report the same public key package.crates/sealforge-escrow/src/dkg.rsgroup_key_bytes. - Why: spend authority must be threshold-controlled, so
akcomes from FROST.nkandrivkgive viewing power, not spending power: they let the holder find and decrypt notes and compute nullifiers, and they are needed to build a spend, but they can’t authorize one. - What it gives the coordinator: because it samples
nkandrivk, the coordinator always holds full viewing power over the escrow. That includes seeing deposit-mode bids live, and outgoing notes. It holds no signing share. See 5 Security. - Not done:
nkandrivkare not jointly generated by the trustees, and not sampled by the organiser (11 Open questions). - Spec: ZIP 312 “FROST for Spend Authorization Multisignatures” covers threshold control of the spend authorization key. It does not specify how
nkandrivkare generated (its Key Generation section states that it doesn’t specify a key generation procedure), so sealForge’s choice is its own. The Orchard key components are as in the protocol specification (protocol.pdf), handled byorchard 0.16.0. - Status:
Testnet.
Threshold signing
- What: a 2-of-3 FROST DKG run on each trustee’s machine (4 Architecture). For each escrow spend in a PCZT, the trustees sign
(shielded_sighash, α). The aggregated signature is a RedPallasSpendAuthsignature that verifies underrk = ak + [α]G, and the coordinator applies it to the PCZT. - Code:
- DKG:
crates/sealforge-escrow/src/dkg.rs(frost::keys::dkg::part1/2/3). - Signing:
crates/sealforge-escrow/src/signing.rscommit,sign,aggregate.signdeserializes α into afrost_rerandomized::Randomizerand callsfrost_rerandomized::sign(marked#[allow(deprecated)], see upstream risk).aggregateusesRandomizedParams::from_randomizer. - Consensus check:
signing::verify_spend_authverifies withreddsa 0.6.1VerificationKey<SpendAuth>afterak.randomize(α), the same typeorchard 0.16uses. - Application:
crates/sealforge-chain/src/spend.rsapply_signatures(pczt::roles::low_level_signer), which checks each signature against the action’srkfirst.
- DKG:
- Why: no single party, the coordinator included, holds a key that can spend.
- Not done: trustees don’t receive or check the PCZT before signing. They sign the sighash the coordinator gives them (5 Security). No nonce reuse: nonces are deleted after round 2, and a reset round discards them.
- Spec: RFC 9591 (FROST), ZIP 312 (rerandomized FROST for spend authorization), RedDSA / RedPallas spend authorization in the protocol specification.
- Tests:
two_of_three_produces_consensus_valid_spend_auth_signatures(every 2-of-3 subset; output verified with reddsa 0.6.1),one_trustee_cannot_sign_alone,aggregates_posted_values_into_valid_signatures,browser_api_produces_valid_spend_signatures(WASM signer),dkg_e2e. - Status:
Testnet. Regtest spike txid in docs/spikes/frost-pczt.md.
PCZT
- What: every escrow spend goes through the PCZT roles:
- Code:
crates/sealforge-chain/src/spend.rs:EscrowWallet::build_payment/build_sweep→prove→finalize,apply_signatures,send_raw.zcash_client_backend 0.25.0-pre.2(create_pczt_from_proposal,extract_and_store_transaction_from_pczt) andpczt 0.10.0-pre.2(rolesio_finalizer,prover,signer,low_level_signer). Dummy spends are signed by the IO finalizer. sealForge collects α only for actions whosespend_auth_sigis still empty (the escrow notes). - Why: the PCZT separates building and proving (coordinator) from authorizing (trustees), and it carries α, which Orchard fixes per action.
- Not done: the PCZT isn’t sent to trustees, though the coordinator stores it (
tx_batches.pczt). - Spec: ZIP 374 “Partially Created Zcash Transaction Format” (Draft). Design notes: docs/spikes/frost-pczt.md.
- Status:
Testnet.
Fees
- What:
StandardFeeRule::Zip317withMultiOutputChangeStrategy(Ironwood change, split policy with a 10,000 zat minimum output). The bidder wallet asks the builder for the fee (Builder::get_fee(&FeeRule::standard())), so the quote matches whatbuild_for_pcztchecks. Escrow fees are paid from the escrow. The organiser can pre-fund a fee buffer (SF1FEE), and whatever isn’t spent on fees returns with the payout. The payout sweep sizes itself by retrying with the shortfall the proposal reports (build_sweep). - Code:
crates/sealforge-chain/src/spend.rspropose,build_sweep.crates/sealforge-wallet/src/pay.rsfee_for.crates/sealforge-core/src/memo.rsfee_buffer_memo. - Not done: no protocol fee (9 Product).
- Spec: ZIP 317 “Proportional Transfer Fee Mechanism”.
- Measured costs: 9 Product, unit economics.
- Status:
Testnet.
Memos
- What: ZIP 302 text memos. Bids carry
SF1, revealsSF1R, the fee bufferSF1FEE. Refunds and payouts carry no memo. Grammar and limits: 2.3 Wire formats. - Code: encode:
zcash_protocol::memo::Memo::from_str→MemoBytes(spend.rspropose); decode:Memo::from_bytes,Memo::Textonly (crates/sealforge-scan/src/lib.rs). Parsing:crates/sealforge-core/src/memo.rs. - Not done: no binary or structured (non-text) memo types.
- Spec: ZIP 302.
- Status:
Testnet.
Payment requests
- What: one ZIP 321 URI and QR per bid, payable from any shielded wallet that supports ZIP 321 and memos.
- Code:
crates/sealforge-core/src/zip321.rspayment_uri;packages/bid/src/zip321.ts. Format: 2.3. - Not done: multi-payment URIs and other optional parameters.
- Spec: ZIP 321 “Payment Request URIs”.
- Status:
Testnet.
Light client protocol
- What: scanning is two-phase. First, trial-decrypt compact blocks to find which transactions pay the escrow. Then fetch each matching full transaction to decrypt the value, output index and memo. Natively over gRPC (
tonic 0.14). In the browser over gRPC-web, through Envoy in front of Zaino. - Code:
crates/sealforge-chain/src/audit.rsscan_incoming(get_block_range,get_transaction).crates/sealforge-scan/src/lib.rsscan_compact_block,decrypt_transaction. Browser client:apps/web/src/lib/grpc-web.ts. Proxy:infra/envoy.yaml,infra/envoy-tls.yaml. The browser wallet also usesGetLatestBlock,GetTreeStateandSendTransaction. - Why: an auditor needs only a lightwallet server they choose. No full node, and no sealForge server.
- Not done: no full-node RPC dependency. The lightwallet server learns which blocks are fetched and the client’s IP (WALLET-INTEGRATION.md).
- Spec: ZIP 307 “Light Client Protocol for Payment Detection”. The gRPC service is lightwalletd’s
CompactTxStreamer, as served by Zaino. - Status:
Testnet(CLI againsttestnet.zec.rocks; browser throughsealforge-lwd.udokaam.dev).
Note encryption and scanning
- What: incoming-only decryption with the UIVK for audits:
try_compact_note_decryptionon compact actions, thentry_note_decryptionon full actions, for both the Orchard and the Ironwood domain versions. Full decryption with the UFVK in the coordinator’s escrow wallet. - Code:
zcash_note_encryption 0.5.1;orchard 0.16.0note_encryption::{NoteEncryptionDomain, OrchardVersion, IronwoodVersion, CompactAction}incrates/sealforge-scan/src/lib.rs. Escrow wallet:zcash_client_sqlite 0.23.0-pre.2viacrates/sealforge-chain/src/wallet.rs. - Not done: the auditor never uses the UFVK and never decrypts outgoing notes.
- Spec: note encryption in the protocol specification, as implemented by
zcash_note_encryptionandorchard. - Status:
Testnet.
Bidder wallet keys
- What: a 24-word BIP-39 phrase from 32 bytes of
crypto.getRandomValues, thenUnifiedSpendingKey::from_seed(params, seed, zip32::AccountId::ZERO). Each bid gets a fresh diversified UA with an Orchard receiver only. Change goes to the internal scope. - Code:
crates/sealforge-wallet/src/keys.rs(from_phrase,address(index),change_address). Browser key storage:apps/web/src/lib/wallet/vault.ts(PBKDF2-SHA-256 with 600,000 iterations, then AES-GCM-256, in IndexedDB). - Not done: no Sapling or transparent receivers. The seed is never sent to any server.
- Spec: ZIP 32 “Shielded Hierarchical Deterministic Wallets”, ZIP 316.
- Status:
Testnet.
Witnesses and anchors
- What: the wallet keeps one incremental witness per received note, starting from the birthday’s tree state (
GetTreeState). After each block, if the server supplied a nonzerochain_metadatatree size for Orchard or Ironwood, the wallet’s tree size MUST equal it, or scanning stops withTreeMismatch. A missed action is caught, and a note with a wrong witness is never spent. Reorgs are detected and handled by rescanning from the birthday. Spends use the Ironwood tree root as the anchor. - Code:
crates/sealforge-wallet/src/lib.rs(TreeMismatchcheck,ScanError::Reorg);crates/sealforge-wallet/src/pay.rsbuilder(ironwood_anchor).incrementalmerkletree 0.9. Test:scanning_follows_the_chain_and_refuses_gaps_and_reorgs. - Not done: the check is skipped when the server reports a tree size of 0. The escrow wallet uses
zcash_client_sqlite’s own tree handling instead. - Status:
Testnet.
Escrow wallet
- What: a view-only
zcash_client_sqliteaccount per auction, imported from the UFVK. It syncs from Zaino, proposes transfers and builds PCZTs. Wallet files live in an owner-only directory (0700, SECURITY-REVIEW F6). - Code:
crates/sealforge-chain/src/wallet.rs(EscrowWallet::create,sync,received),crates/sealforge-chain/src/spend.rs,crates/sealforge-chain/src/cache.rs. - Status:
Testnet.
Randomness
Every secret or randomizer comes from an OS CSPRNG.sealforge-core has none by design.
Network and consensus
- Testnet: NU7 testnet through the public lightwallet server
testnet.zec.rocks(evidence/testnet-genesis.md). Testnet blocks ran at about 25 s; the ZIP 218 25-second spacing is listed as Proposed in the ZIP index. - Regtest: Zebra and Zaino at the versions in VERSIONS.md. Regtest activates up to NU6.3 only, because that Zaino version doesn’t know NU7’s consensus branch ID (VERSIONS.md; configs in
infra/regtest/, launcherscripts/regtest.sh). - Mainnet: not run.
local-dkg,SF_REFUND_DELAY_SECSandSF_MIN_WINDOW_BLOCKSare refused on mainnet. - Spec: ZIP 259 (NU7 deployment, Proposed), ZIP 258 (NU6.3).
Dependencies and upstream risk
sealForge depends on pre-release Zcash crates and on a FROST ciphersuite that upstream has moved. This table states what that costs.
Not independently re-verified for these docs: the crates.io status of
zakura-reddsa-frost (the registry page didn’t return crate data when checked on 2026-11-13).