StealthOSAll current reports

Atomic deposit merging

Cross-repository review

Groth16Browser WASMEVM + Solana

Review date: 7 October 2026

The 8 October source follow-up records frontend 9d8b376 (failed-preparation lifecycle) and relayer 11bba04 (merged-nullifier indexing/backfill), merged through frontend PR 66 and relayer PR 26. It inspected those fixes without rerunning the complete audit. The original 7 October findings below are preserved.

The original branch/report name uses “Nova-style deposits”; the reviewed circuit is Groth16 DepositMergeV1, not the retired recursive Nova verifier.

Current remediation status — 12 October 2026

Finding-status verification against staging source, dated remediation records and targeted regression tests. Not a full re-audit or blanket deployment attestation.

F1 — Interrupted preparation reserves notes indefinitely

High · Remediated in source

Track disclosure and archive demonstrably undisclosed preparation failures; preserve and reconcile any attempt that might have been submitted.

Evidence / limits: October 8 source follow-up and current deposit cancellation/merge regressions; later Solana recovery-resolution evidence.

Remediation commits: privacy-frontend 9d8b376

F2 — Merged-note nullifiers absent from the recovery index

Medium · Remediated in source

Ingest the scalar DepositMerged nullifier and rebuild the previous index generation without duplicating insertion leaves.

Evidence / limits: Current merged-event decoding regression passed; PostgreSQL index/backfill coverage is supported by the recorded October remediation run and was not rerun in this pass. October 9 DogeOS atomic-merge cutover includes funded recovery checks. This source status does not assert that every independently pinned recovery service was upgraded.

Remediation commits: stealthos-relayer 11bba04

Download current status for all 19 findings · Status data and checked revisions


Original dated report

The original text below is preserved as a historical record. Use the current-status addendum above for finding closure.

Download original dated source report (without current addendum)

NOVA-STYLE-DEPOSITS CROSS-REPOSITORY AUDIT
Date: 2026-10-07

DECISION

Fix the two confirmed integration regressions below before enabling this feature.
One is a high-priority wallet availability regression; the other is a medium-priority
recovery/index correctness regression. No unauthorized-spend, inflation, or fee-bypass
exploit was established in the inspected changes and executed tests. That conclusion
does not establish cryptographic soundness or qualify these artifacts for production.
Production key authorization and deployment qualification remain outstanding.

SCOPE AND METHOD

Compared each fetched nova-style-deposits head against its merge base with staging,
the actual PR target. Reviewed the cumulative branch changes, their interacting
callers, recovery consumers, proof boundaries, and deployment gates. Generated SDK
output and package changes were assessed with their source/build changes.

privacy-protocol, PR 21:
  https://github.com/lexie-crypto/privacy-protocol/pull/21
  head: b3f70c4bb59d0e3e4c6d8a26f72c0b37a6eb1b91
  base: 59225f7461596ca86c95f6b25549570a4c6c63c3
  49 commits; 274 changed files; +8344/-2897 lines.
privacy-frontend, PR 66:
  https://github.com/lexie-crypto/privacy-frontend/pull/66
  head: c7456bb5afaa9eb071bd786016b301debab07a0c
  base: d805dc86c788268f826759f80bbb37ef26dd12e3
  47 commits; 81 changed files; +9449/-411 lines.
stealthos-relayer, PR 26:
  https://github.com/lexie-crypto/stealthos-relayer/pull/26
  head: cb851fdaf040fb706809f0818e99e2185ad75f1a
  base: 92370a78279ae30cd23f0cfc72c9a7b2b39ecf6d
  3 commits; 26 changed files; +3422/-13 lines.

Total: 381 changed paths across the three repositories. The companion
*-changed-files.txt files give the complete per-repository line-count inventory.
Tests ran in separate snapshots of these heads. The original three checkouts were
left on their existing branches and remained clean. Audit-only reproductions,
dependencies, build output, and logs are in this audit directory; the Linux contract
test snapshot is under /home/markm/.cache/nova-style-deposits-audit-20261007 in WSL.

WHAT THE CHANGES DO

The main feature is a funded private-note top-up. Instead of creating another note
for every deposit, it consumes one existing note and creates one replacement owned
by the same identity. For example, an existing private 30 plus a new net deposit of
20 becomes one private 50 note. In the fee variant, fees apply to the new deposit,
not to the old balance. The deposit amount remains public; the old balance and the
replacement's total value remain private. This is a separate DepositMergeV1 proof
family, using Groth16, rather than recursive Nova proof aggregation.

privacy-protocol:
- Adds a seven-public-input circuit and Rust engine for one existing input plus
  public net credit, producing one fresh same-owner output. It checks ownership,
  Merkle membership, leaf-index/nullifier consistency, token domain, nonzero values,
  120-bit value bounds, and value conservation. The output can subsequently be spent
  through the ordinary JoinSplit proof path.
- Adds a funding transcript that binds chain/program domain, pool, current tree,
  token, payer, gross amount, net amount, fee commitments, deadline, and nonce.
- Adds baseline EVM native/ERC20 merged deposits, plus a separate private-fee pool
  and controller. The fee variant also implements fee-authorized ordinary deposits,
  private transfers, withdrawals, and migration. Shared authorization hooks prevent
  inherited legacy entrypoints from bypassing its fee policy; legacy batch entrypoints
  that cannot carry the authorization are disabled.
- Adds SOL/SPL merged deposits, account/PDA validation, event encoding, and verifying
  keys. Atomic execution couples the new funding, old nullifier consumption, and
  replacement insertion. Deadline and current-tree checks prevent stale merges.
- Adds SDK builders, event decoders, transcripts, browser worker/client support,
  development fixtures, CI scripts, and verification documentation. Browser proving
  stays local; witness/private keys are not part of the relayer API.
- Converts the SDK distribution to ESM/NodeNext and updates imports and exports.
  This is a compatibility change beyond deposit merging: CommonJS consumers need an
  appropriate dynamic-import/migration path.

privacy-frontend:
- Automatically selects an authenticated, confirmed, unspent, unreserved same-owner
  note for eligible self-deposits in the current pool/tree/asset. It falls back to an
  ordinary deposit when no compatible note or supported merge capability exists.
  EVM MAX deposits explicitly retain the ordinary path.
- Loads/proves in a dedicated browser worker, binds the funded transaction, handles
  native/token and private-fee variants, and checks final canonical event/leaf evidence.
- Persists encrypted recovery material before exposing a copyable spend proof. The
  private-fee flow backs up an immutable template before acquiring final authorization.
  It reserves inputs and reconciles successful/expired attempts across recovery flows.
- Adds bounded background consolidation: two compatible notes become one through an
  existing JoinSplit circuit, using a fully sponsored relay. It rechecks the quote,
  pauses for interaction, caps attempts, and avoids a wallet-paid fallback. EVM private-
  fee pools are explicitly excluded from this background optimization.
- Changes shared cancellation, wallet locking, journal conflict checks, and recovery
  logic. These shared changes also affect existing ordinary EVM transactions.
- Updates packaged SDK/prover artifacts, identity checks, configuration, UI hooks,
  API forwarding, and substantial test coverage.

stealthos-relayer:
- Adds prepare/backup/authorize handling for private-fee merged deposits, strict
  public-only request schemas, server-selected deployment/artifact policy, proof
  checks, exact funding/fee bindings, and transaction simulation. The user's wallet
  supplies the public funds.
- Adds a durable encrypted-recovery phase before authorization, content-bound backup
  acknowledgements, ledger restoration support, and merged-deposit receipt checks.
- Adds EVM/Solana sponsorship quotes for two-input/one-output consolidation, with
  deployment, balance, fee, gas, and budget checks appropriate to each path.
- Does not update the general EVM recovery ingestion event list for DepositMerged;
  this is finding F2 below.

CONFIRMED FINDINGS

F1 -- HIGH / P1 -- Failed or cancelled encrypted backup can indefinitely reserve
ordinary EVM spending inputs without a normal retry path.

Locations in privacy-frontend:
  src/joinsplit-v2/pendingJournal.ts:980-1000 (new reservation conflict check)
  src/joinsplit-v2/proveAndSend.ts:380-401 (write local record, then await backup)
  src/joinsplit-v2/settlement.ts:265 (preparation occurs before the try block)
  src/joinsplit-v2/journalReconciliation.ts:607-663 (unspent attempt stays pending)
  src/base-app/hooks/useBaseConsolidation.ts:172-175 (interaction cancellation)

Trigger: a send or background consolidation writes its local prepared record, then
the encrypted backup fails, times out, or is cancelled. The exception escapes before
the caller receives the journal ID or reaches simulation/submission. The new overlap
check treats the prepared record as an active reservation. A fresh attempt with the
same input is rejected. This is a new regression: the branch adds the conflict check
and expands cancellation into the backup path, although some surrounding persistence
code existed previously.

Reconciliation cannot complete an ordinary undisclosed JoinSplit: the input is still
unspent, the outputs do not exist, and the new safe-expiry logic is only for funded
deposit merges. Ordinary JoinSplits have no corresponding deadline/retry transition.
Background consolidation makes the cancellation race easier to encounter because a
pointer/key interaction aborts the active attempt.

Impact: wallet availability/denial of normal spending for the affected notes, including
ordinary EVM transactions when deposit merging is disabled. Funds are not proven lost
on chain; another properly recovered/manual path may still spend them. Do not describe
this as an on-chain freeze or irreversible theft.

Reproduction: auditDepositMergeGaps.test.ts invokes real settlement/journal code and
simulates a backup rejection immediately after the encrypted local write. It verifies
that simulation and wallet submission were never called, the record remains prepared,
the source is reserved, and an immediate new attempt fails with the reservation error.
Result: passed, confirming the failure mode. See audit-gap-reproduction.log.

Fix: explicitly track whether public proof disclosure has occurred. Safely retire an
undisclosed failed attempt while retaining necessary encrypted recovery material, or
resume the identical immutable intent idempotently. Provide a completion/resubmission
path for exposed attempts; do not simply release exposed proofs on a timer. Add tests
for backup rejection, abort, timeout, and restart/retry on an ordinary EVM send and
background consolidation.

F2 -- MEDIUM / P2 -- EVM DepositMerged nullifiers never enter the canonical recovery
index, so incremental sync can treat a consumed input as spendable.

Locations:
  privacy-protocol/contracts/src/ShieldedPoolDepositMerge.sol:112-120
  stealthos-relayer/src/backend/engine-api/recovery-ingestion.js:9-18,190-192,480-482,664
  privacy-frontend/src/joinsplit-v2/syncIndex.ts:309-310,394-398
  privacy-frontend/src/joinsplit-v2/syncIndex.ts:77,203-204 (24-hour full-check fallback)

The new contract marks its source nullifier used and emits DepositMerged; it does not
also emit JoinSplit. The specialized fee receipt code knows this new event. The general
recovery ingestion ABI does not. Its eth_getLogs filter is derived from that incomplete
ABI, so it never requests DepositMerged logs. The parser also assumes spend events
contain a nullifiers array; the new event carries a single nullifier.

The replacement's ordinary LeafInsertedInTree event is still indexed, so leaf-count
checks can succeed and the index can report ready while its spent-nullifier stream
is incomplete. Incremental client sync uses this stream to set spent:false when a
nullifier is absent. An already-synchronized second client, or another delta refresh,
can therefore retain/revalidate the consumed source, display excess available balance,
and select an input that later fails chain validation.

Limits: the chain's nullifier check still prevents double spending. Direct full note
validation and local transaction reconciliation can correct local state; the current
delta path requires a full validation after 24 hours. These mitigations reduce the
severity but do not repair the incomplete canonical server history.

Reproduction: audit-event-gap.mjs encodes a genuine DepositMerged event using the
branch's fee-receipt ABI. The fee interface recognizes it, the canonical recovery
interface returns no event, and its topic is absent from the ingestion filter.
See audit-event-gap.log.

Fix: add the event to the canonical ABI/filter, ingest its scalar nullifier, and
rebuild/backfill already-indexed ranges with an appropriate generation change. Add
an end-to-end test from funded merge receipt through recovery delta to spent state,
covering baseline/private-fee and native/token paths. Adding receipt support alone is
insufficient.

REMAINING RELEASE GAPS AND BEHAVIORAL LIMITS

1. Production key authorization is outstanding. The newly included setup fixtures
   have public deterministic test entropy. They are development artifacts, not a
   ceremony-backed production setup. EVM production-domain rejection and Solana
   mainnet build rejection are present; no bypass was established. Preserve those
   gates and produce authorized production keys/artifact manifests before real-value
   enablement. The branch explicitly documents this unresolved gate.
2. Real EVM merge-to-withdraw proofs were exercised for the baseline pool, but the
   seven private-fee contract tests use mock merge/deposit/router/allocation verifiers.
   They test flow/accounting, not a complete real-proof private-fee merged deposit.
   Add real native/ERC20 fee-bearing round trips with authorized matching keys.
3. Solana tests here used native program execution, not compiled SBF. They do not
   establish deployed stack limits, compute budget, or target-cluster behavior.
   Physical iOS/Android browser/WASM qualification and a DogeOS target deployment/RPC
   check are also still outstanding, as the branch documentation states.
4. This is a coordinated deployment across pools/programs, keys, browser packages,
   server pins, and enabled capabilities. Existing deployed contracts do not acquire
   new selectors from an SDK upgrade. The fee pool is a distinct variant; its support
   for ordinary transfers/exits/migration does not establish private swap/bridge parity.
   Capability/rollout mismatches require explicit integration qualification.
5. Privacy tradeoff: the public payer/deposit is in the same transaction as the old
   nullifier and replacement commitment. The circuit hides the old note's balance
   and leaf selection, but does not hide this public transaction-level association.
   This is a property of the feature, not a demonstrated accidental key/witness leak.
6. The observed GitHub Actions jobs at the audited heads did not start because of
   account billing/spending-limit restrictions. They are not evidence of code-test
   failures, but they also provide no independent passing CI result at these heads.

VALIDATION EXECUTED

- Frontend: 187 suites / 1,459 existing tests passed.
- Frontend TypeScript check passed.
- Frontend deposit-merge package identity check passed.
- Frontend optimized production build passed, with warnings (including bundle size).
  CI=false was used for that local build; it does not certify a warning-free CI build.
- Relayer focused deposit-merge suite: 55 tests passed.
- Rust deposit-merge circuit/engine suite: 9 tests passed, including real proof and
  altered-input rejection cases.
- EVM: 16 tests passed, including 3 real-proof merge-to-ordinary-withdrawal cases
  (native, ERC20, and DogeOS-domain native). The remaining tests include 7 private-fee
  mock-verifier cases and 6 baseline contract cases.
- Solana: 2 real-proof SOL/SPL native-runtime integration tests passed, including
  expected rejection/atomic rollback cases. compiled SBF=false.
- Audit-only regression test: 1 passed, reproducing F1.
- Audit-only event compatibility reproduction confirmed F2.

The logs beside this report record commands' output. No live deployment, transaction,
fund movement, source fix, PR comment, or PR merge was performed during this audit.

ORDER OF FOLLOW-UP

First fix F1's failed-preparation lifecycle and F2's event ingestion/backfill together
with regression tests. Then complete the real private-fee proof lane, restore CI,
and satisfy production-key and target-runtime/deployment qualification before enabling
the new funded merge capability for real-value users.