Skip to content

stealth addresses, CAT minting, offer fixes#17

Open
almogdepaz wants to merge 52 commits intomainfrom
stealth_addresses_new
Open

stealth addresses, CAT minting, offer fixes#17
almogdepaz wants to merge 52 commits intomainfrom
stealth_addresses_new

Conversation

@almogdepaz
Copy link
Copy Markdown
Owner

Summary

  • stealth addresses: dual-key stealth address system with nullifier + signature authorization modes, encrypted nonce scanning (x25519 + ChaCha20Poly1305), per-recipient nonce counters to prevent collision
  • CAT minting: TAIL program verification in zkVM guest, mint proof type, genesis-linked single-issuance with nullifier-based replay protection
  • TAIL-on-delta: CAT2-style TAIL authorization for burn/melt operations (spend-path supply changes)
  • offer system fixes: stable offer ID indexing, taker spent tracking, correct per-output tail_hash assignment, maker pubkey linkage enforcement (NM-002)
  • security fixes: checked_add overflow protection in balance enforcement, settlement double-spend guard, scan dedup by serial_commitment (not puzzle_hash), TAIL hash verification in spend-path delta authorization (F-01)
  • infra: merkle tree migration to SparseMerkleTree, e2e risc0 test suite (6 tests), CAT minting test suite

Security review

Differential review completed — see VEIL_DIFFERENTIAL_REVIEW_2026-03-15.md.

  • 1 HIGH finding (F-01: TAIL substitution) — fixed in 794b051
  • 3 MEDIUM findings — documented, non-blocking
  • 6 pre-existing bugs fixed by this branch

Test plan

  • cargo test-mock — unit + integration tests with mock backend
  • cargo test-risc0 --test test_e2e_risc0 — e2e with real proofs
  • cargo test-mock --test test_cat_minting — CAT minting suite
  • verify F-01 fix: add regression test for mismatched tail_source (TODO)

almogdepaz and others added 30 commits January 18, 2026 20:59
  - changed ProofOutput.nullifier to nullifiers vec
  - added Input.additional_coins for ring spends
  - guest programs process multiple coins in single proof
  - process_announcements() verifies across all ring coins
  - recursive aggregation handles multiple nullifiers
  - backwards compatible with single-coin spends
  - add coin_commitment as coin_id for CREATE_COIN_ANNOUNCEMENT
    (privacy alternative to chia's parent-based coin_id)
  - risc0/sp1 guests compute per-coin announcement hashes
  - mock backend supports both puzzle and coin announcements
  - all 4 announcement opcodes now functional (60, 61, 62, 63)
  - add secp256k1 ECDH-based stealth address protocol (src/wallet/stealth.rs)
  - derive unique puzzle_hash per payment for receiver unlinkability
  - view/spend key separation (view key can scan but not spend)
  - update simulator to store ephemeral_pubkey with coins
  - update wallet keys: x25519 note encryption → secp256k1 stealth keys
  - update CLI send/scan commands for stealth flow
  - delete encrypted_notes.rs (no longer needed)
  - add CAT_PROTOCOL.md documenting privacy-preserving CAT handling
  - add STEALTH_ADDRESSES.md with full protocol specification
  - nullifier (default): ~10K cycles, view key can spend, fast proving
  - signature: ~2-5M cycles, view/spend separation, for custody setups

  changes:
  - add StealthMode enum, StealthSpendAuth, StealthPaymentV2, ScannedStealthCoin
  - add derive_nullifier_secrets_from_shared_secret() with domain separation
  - add STEALTH_NULLIFIER_PUZZLE_HASH constant (trivial puzzle for nullifier mode)
  - add create_stealth_payment_with_mode() for mode-aware payment creation
  - update scan_coins_v2() to auto-detect mode from puzzle_hash
  - CLI: add --stealth-mode flag to sim send (nullifier|signature)
  - CLI: scan command auto-detects and reports mode

  refactor:
  - flatten clvm_zk_core chialisp module into lib.rs
  - update all imports from clvm_zk_core::chialisp::X to clvm_zk_core::X

  docs:
  - add stealth addresses section to veil_crypto_design_v2.md
  - update domain separator list with stealth-related tags
  - fix incorrect asset type leakage claims (STARKs hide internal structure)
  - fix nullifier comment to include amount in hash
removed empty nullifier padding in recursive guest code - we don't need backward compatibility
analyzed clvm_tools_rs compilation pipeline to find optimization opportunities beyond precompiled puzzles.

findings:
- compiler pipeline: parse_sexp -> frontend -> codegen
- optimization flags already disabled (optimize: false, frontend_opt: false)
- 500s bottleneck is from allocations (BTreeMap, Vec, Rc) in zkvm
- no_std allocator + zkvm memory = 100-1000x slower than native

verdict: no obvious no-tradeoff optimizations in compiler
- would require: cycle profiling, algorithmic changes, months of work
- high risk of bugs, requires upstream coordination
- precompiled puzzle approach (iteration 2) is correct solution

conclusion: ralph loop complete
- achieved 22x speedup for conditional offers (580s -> 26s)
- no more obvious optimizations without tradeoffs available
- remaining improvements require empirical testing or major rewrites
replaced inline Vec-based implementations with clvm_zk_core optimized
fixed-array functions:
- compute_serial_commitment
- compute_coin_commitment
- verify_merkle_proof
- compute_nullifier

results:
- settlement: 354s → 317s (10.4% faster)
- total demo: 397s → 358s (9.8% faster)
- code: -100 lines of duplication

Vec allocations in zkvm are 100-1000x slower than stack arrays.
using clvm_zk_core's optimized implementations eliminates this
overhead while improving maintainability.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
replaced Vec-based ECDH payment puzzle derivation with fixed-size
array ([0u8; 47] for 15-byte domain + 32-byte secret).

performance impact: negligible (~1s, within noise)
rationale: code consistency, eliminates all Vec allocations

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
final timings after settlement guest optimization:
- conditional: 580s → 25s (23.2x faster)
- settlement: 372s → 316s (1.18x faster)
- total demo: 969s → 357s (2.7x overall)

iteration 8 eliminated all Vec allocations from settlement guest
using clvm_zk_core's optimized fixed-array implementations.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
attempted to reuse condition.args Vec allocation with .clear() + push
instead of vec![...], but this caused a 60s regression.

.clear() triggers Drop on inner Vec<u8> elements, which is expensive
in zkvm. fresh vec! allocation is faster due to compiler optimization
and no Drop overhead.

lesson: micro-optimizations that "save allocations" can backfire in
zkvm environments due to Drop costs.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
attempted to avoid 81-byte DELEGATED_PUZZLE_BYTECODE.to_vec() copy
by using borrowed slices with lifetime extension pattern.

results: no measurable improvement (383-407s vs 357-395s baseline)
analysis: 81-byte copy is negligible in ~25s proof

verdict: adds code complexity with zero benefit, reverted

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
almogdepaz and others added 22 commits January 21, 2026 23:27
exhaustively explored 10 iterations of optimization attempts:
- 2 successful implementations (23x + 10% speedups)
- 3 failed attempts documented (sp1, vec reuse, bytecode slice)
- 5 paths analyzed and deferred/rejected

final performance: 969s → 357-395s (2.5-2.7x overall improvement)

measurement variance (±30s) now exceeds remaining optimization
potential. all obvious no-tradeoff optimizations completed.

ralph loop objective satisfied.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
  settlement.rs:
  - guard ClvmValue import and 5 helper functions with #[cfg(feature = "risc0")]
    to eliminate dead code warnings when compiling without risc0

  test_ring_balance_enforcement.rs:
  - guard 3 rejection tests with #[cfg(not(feature = "mock"))] since mock
    backend doesn't do real ZK verification

  test_settlement_recursive.rs, test_conditional_spend.rs:
  - guard imports with #[cfg(feature = "mock")] since all tests in these
    files are mock-only
- MintData struct with TAIL source/params and output coin details
- GenesisSpend: links mint to a genesis coin, prevents infinite re-minting
  via genesis nullifier (same mechanism as spend nullifiers)
- TAIL-on-delta: spend path consults TAIL when sum(inputs) != sum(outputs)
  for melt/burn authorization (CAT2-style supply change control)
- ProofType::Mint (type=3) for mint proofs
- CLI `sim mint` command with local TAIL verification
- guest mint logic (risc0 + sp1): compile TAIL, verify genesis,
  execute TAIL, compute coin commitment, output proof
- tests: genesis-linked mint, nullifier determinism, multi-CAT minting,
  failing TAIL rejection

Generated with AI

Co-Authored-By: AI <ai@example.com>
covers: XCH spend + double-spend, CAT mint+spend, genesis-linked
mint anti-replay, CAT ring spend, offer settlement flow, TAIL-on-delta melt

Generated with AI

Co-Authored-By: AI <ai@example.com>
nonces stored as 80-byte encrypted blobs (ephemeral_pub || ciphertext)
instead of plaintext [u8; 32]. only recipient can decrypt and scan.
- replace rs_merkle with SparseMerkleTree in simulator (host now matches guest proof format)
- scan dedup by serial_commitment instead of puzzle_hash (stealth coins share puzzle_hash)
- fix test_cat_minting: use proof_output.X access pattern, add tail_source field
- fix signature_integration_tests: add cfg(feature = "testing") gate
- remove rs_merkle dependency from Cargo.toml
NM-001 (HIGH): settlement outputs were persisted with placeholder program
and XCH-default tail_hash, causing post-settlement coins to be non-spendable.
Fixed by using correct tail_hash per output and real program sources.

NM-002 (LOW): added maker_pubkey equality assertion before settlement
state mutation, enforcing the guard that was documented but never checked.
…tion

test 7: reconstructs settlement goods coin with correct tail_hash and program,
then spends it through the normal prove+verify flow. would have failed before
NM-001 fix (placeholder program → program_hash mismatch in guest).

test 8: proves the bug mechanism — reconstructing a settlement coin with wrong
tail_hash produces a different commitment that can't be found in the merkle tree.
fix: settlement wallet desync + maker pubkey guard (NM-001, NM-002)
… authorization

- stealth nonce counter: per-recipient counter prevents identical shared_secret
  when sending to same recipient multiple times (was hardcoded to 0)
- taker mark-as-spent: taker's consumed coin now marked spent after settlement
- CAT conditional spend: thread tail_source through prove pipeline so TAIL can
  authorize balance delta during offer-create (was hardcoded None, causing panic)
- faucet --tail-source: store TAIL program on coin for later use in offer-create
- demo.sh: combined stealth + CAT offer e2e demo, uses --tail-source
…thorization

prevents substitution attack where attacker provides permissive tail_source
(e.g. "(mod () 1)") to bypass restrictive TAIL during CAT burn/melt.
the compiled TAIL hash is now asserted equal to the coin's tail_hash
before execution, in both risc0 and sp1 guests.
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