StealthOSAll current reports

Groth16 core, WASM & Base contracts

Adversarial follow-up

Base / EVMGroth16Browser WASM

Review date: 10 September 2026

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.

ADV-01 — Untrusted relayer gas spend

High · Remediated in source

Construct fee fields on the server; enforce gas, fee and durable global/per-signer spending limits, including mandatory simulation.

Evidence / limits: Current spending-policy and handler regression checks; operator budgets and shared durable storage remain required.

Remediation commits: stealthos-relayer bef98e7

ADV-02 — Incompatible Web Locks options

High · Remediated in source

Use ifAvailable without combining it with signal; check cancellation while retaining the lease until work settles.

Evidence / limits: Current wallet-lock regressions.

Remediation commits: privacy-frontend bfc82d9

ADV-03 — Proxy requires redundant public-wallet field

Medium · Remediated in source

Authenticate the minimal V2 envelope by JWT; reject any supplied wallet alias that conflicts with the authenticated account.

Evidence / limits: September actual-proxy and loopback regression record.

Remediation commits: privacy-frontend bfc82d9

ADV-04 — Mismatched HMAC protocols

Medium · Remediated in source

Use the same versioned method/path/query/body authentication transcript at the proxy and relayer, including route-alias checks.

Evidence / limits: September loopback record and current HMAC regressions.

Remediation commits: privacy-frontend bfc82d9 · stealthos-relayer bef98e7

ADV-05 — Cancelled operations exhaust the active journal

Medium · Remediated in source

Archive proven pre-submit cancellations outside the active limit while preserving encrypted recovery material; keep ambiguous submissions recoverable.

Evidence / limits: Current cancellation, pending-journal and archive regressions, including more than 64 historical records.

Remediation commits: privacy-frontend bfc82d9

ADV-06 — Bridge transaction hash-loss recovery

Medium · Remediated in source

Recover the bridge event only after canonical receipt, calldata, nullifier and order-ID validation.

Evidence / limits: Current bridge recovery and private-bridge journal regressions.

Remediation commits: privacy-frontend bfc82d9

ADV-07 — SDK snapshot reads use latest state

Medium · Remediated in source

Pin reads using the ethers v6 transaction blockTag and update the packaged SDK.

Evidence / limits: September SDK record and current pinned-pool-read regressions.

Remediation commits: privacy-frontend bfc82d9 · privacy-protocol 491fe12

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)

ZK staging adversarial review — 2026-09-10

Follow-up: these seven findings were remediated and pushed to staging. See the remediation and verification record. The findings below describe the original reviewed commits, not the corrected heads.

Outcome

Seven additional issues are confirmed: two high-priority issues and five medium-priority issues. The highest security risk is uncontrolled spending from the Node relayer's signing wallet. The other findings concern operation availability, cross-service compatibility, durable recovery, and SDK snapshot correctness.

No new proof-forgery, double-spend, or unfunded-mint counterexample was found in the exercised circuit and settlement checks. That is a bounded test result, not a guarantee that the system is secure. These findings should be addressed before treating this version as production-ready.

This was a review-only pass. No production source, contracts, keys, configuration, or deployments were changed. Reproduction harnesses and this report are outside the three Git repositories, whose tracked working trees remained clean.

Reviewed versions and scope

RepositoryReviewed staging commit
privacy-frontendac2dc2a329c4587d55bf58744f1dd9324a792e85
privacy-protocol4860c121c9ad089df3ee7e3bd755b838d775ca8d
stealthos-relayer34d35df705e566886435ef07e2d0ce39cbcbefe6

The pass covered the Flat JoinSplit V2 circuits and verifier/release boundary, pool and adapter settlement, SDK, frontend transaction and recovery lifecycle, frontend proxy, and checked-out Node relayer. It followed the previous remediation rather than repeating the entire application's unrelated functionality review.

IDPriorityConfirmed issueApplicability
ADV-01P1 / HighCaller-controlled gas pricing can consume relayer ETHEnabled, funded Node relayer; caller can submit otherwise-valid requests
ADV-02P1 / HighUnsupported Web Locks options prevent operations startingAffected UI operations in environments with Web Locks
ADV-03P2 / MediumProxy rejects the minimal V2 relay envelopeNormal frontend proxy path
ADV-04P2 / MediumProxy and Node relayer sign different HMAC transcriptsConditional on routing the proxy to this Node service with HMAC enabled
ADV-05P2 / MediumWallet cancellations exhaust the 64-record pending journalOperations reaching settlement and subsequently rejected by the user
ADV-06P2 / MediumExact-spend bridge lacks transaction-hash-loss recoverySuccessful zero-output bridge whose hash was not persisted
ADV-07P2 / MediumSDK snapshot silently reads latest with native ethers v6Direct SDK consumers; current frontend wrapper avoids this issue

Priorities distinguish urgency, not seven separate theft vulnerabilities. In particular, ADV-02 is a broad functional blocker, and ADV-04 is a conditional compatibility failure, not an authentication bypass.

Findings

ADV-01 — Untrusted gas pricing reaches the relayer signer

Evidence: transaction-handler.js (stealthos-relayer/src/relayer/transaction-handler.js:1470), fee selection (stealthos-relayer/src/relayer/transaction-handler.js:1540), balance check and signing (stealthos-relayer/src/relayer/transaction-handler.js:1800), and V2 validation (stealthos-relayer/src/relayer/base-pool-v2.js).

The handler accepts gasPrice, maxFeePerGas, maxPriorityFeePerGas, and gasLimit from the serialized client transaction. A positive caller-selected priority fee is retained; the maximum fee is raised to cover it. The legacy path selects the maximum of caller and network prices, without a server-owned upper bound. The eventual wallet-balance check only asks whether the signer can afford the requested transaction.

V2 validation binds the fee recipient and checks the proof-bound fee's shape and amount constraints, but does not require that it cover the relayer's gas spend. Transfer/migration sponsorship also requires a bounded server policy. Proof validity does not authorize an arbitrary gas expense.

Reproduction: The actual handler and actual V2 validator, with a mocked otherwise-successful chain and signer, retained a client gas price of 10^12 wei against a network quote of 10^6 wei. At the fixture's 300,000 gas, that represents 0.3 ETH execution cost against a proof-bound fee of 1 wei. A second case retained the oversized EIP-1559 priority fee. With SKIP_PREFLIGHT=true, a caller gas limit of 50,000 also reached signing even though the mandatory V2 simulation did not include that final gas limit.

Impact and limits: An authorized submitter with an otherwise-valid operation can cause excessive relayer expenditure. Repeated requests are not necessary if one transaction consumes a large enough balance fraction. Authentication and request-count limits do not themselves limit monetary loss. This is not a pool-fund theft or proof-verification bypass. The harness did not broadcast, use private keys, or establish whether a live deployment exposes this Node path; its chain mock deliberately isolates the spending-policy defect after operation acceptance.

Recommended change: Own the gas envelope on the server; reject or clamp client suggestions using explicit fee and total-cost ceilings. Enforce per-transaction, per-account, and aggregate sponsorship budgets. Require economic fee coverage where applicable. Estimate/simulate the final envelope and keep essential spend and gas-limit controls active regardless of optional preflight settings.

An independent second code review found no upstream expenditure cap that defeats this result. Exploitability and magnitude still depend on reaching the enabled authenticated route and on RPC/chain acceptance of the selected price. A high maxFeePerGas alone does not force overpayment; the demonstrated legacy gasPrice and retained EIP-1559 priority fee are the relevant paths. Existing proxy failures can block one route to the handler but do not constitute a reliable spending control.

ADV-02 — Invalid Web Locks options block normal UI operations

Evidence: walletLock.ts (privacy-frontend/src/joinsplit-v2/walletLock.ts:29), send caller (privacy-frontend/src/base-app/hooks/useBaseSendOperation.ts:160), batch-withdraw caller (privacy-frontend/src/base-app/hooks/useBaseSendOperation.ts:589), and shield caller (privacy-frontend/src/base-app/hooks/useBaseShieldOperation.ts:108).

The helper requests an exclusive lock with both ifAvailable: true and the supplied AbortSignal. That combination must reject with NotSupportedError under the Web Locks request algorithm. The affected UI callers supply a signal on their normal path.

Reproduction: The actual transpiled helper, invoked against Node 24's native Web Locks implementation, rejected with NotSupportedError before its operation callback ran. The browser consequence follows the same standardized option restriction; a full interactive browser transaction was not executed.

Impact: Send, batch withdrawal, and shield workflows using this helper fail before starting where Web Locks is available. Environments taking the no-Web-Locks fallback are not affected. This finding does not assert that swap or bridge invokes this helper. Existing lower-level settlement tests did not exercise this native API boundary.

Recommended change: Preserve deliberate locking semantics: either keep fail-fast ifAvailable behavior and omit its incompatible request signal, checking cancellation inside the callback, or intentionally use supported abortable queue semantics. Add a browser/native-API regression that enters through the UI operation boundary.

ADV-03 — Frontend proxy requires a field deliberately absent from V2 requests

Evidence: proxy wallet binding (privacy-frontend/api/proxy/[service].js:954), authorization check (privacy-frontend/api/proxy/[service].js:1048), relay serialization (privacy-frontend/src/utils/relayer-client.ts:489), and V2 settlement (privacy-frontend/src/joinsplit-v2/settlement.ts).

The proxy requires user_address on /api/v1/relay/pool. V2 settlement intentionally omits that wallet field and legacy fee metadata; JSON serialization removes the undefined value. Thus the request fails in the frontend proxy before the Node relayer's newly corrected V2 middleware can accept it.

Reproduction: The actual proxy handler received a minimal V2 pool envelope and a valid synthetic local access JWT containing the wallet/recipient binding. It returned HTTP 400, user_address is required for this relay route, without reaching the upstream. No real credentials or external requests were used.

Impact: The normal proxied V2 relay path is blocked even for authenticated users. This failure is independent of which implementation sits behind the proxy.

Recommended change: Define a V2-specific authorization contract that preserves authentication, account/rate controls, and intended privacy without demanding a duplicate public-wallet field in this relay body. Do not solve it by disabling authorization or silently adding an identity link the V2 interface deliberately removed. Test the actual frontend serializer against the actual proxy handler.

ADV-04 — HMAC protocols disagree across proxy and Node service

Evidence: proxy signing transcript (privacy-frontend/api/proxy/[service].js:592) and Node verification transcript (stealthos-relayer/src/server.js:346).

The proxy signs a newline-separated base-relay-v2 transcript including method, path, timestamp, idempotency key, API-key hash, and body hash. The checked-out Node server verifies a colon-separated method/path/timestamp/serialized-body transcript. Matching the configured secret does not reconcile those inputs.

Reproduction: An actual proxy-generated signature was rejected by the actual Node verification function using the identical fixture secret, body, and timestamp. A Node-format control signature was accepted.

Impact and precondition: If BASE_ENGINE_URL targets this Node relayer with HMAC enabled, legitimate proxied requests fail authentication after ADV-03 is fixed. This is not an authentication bypass. The frontend's default target is an external engine hostname; its live implementation was not established, so this report does not claim that every deployed engine has this mismatch.

Recommended change: Share and version the authenticated canonical transcript across implementations, including path/body normalization and replay binding. Add a cross-repository contract test. Keep authentication enabled.

ADV-05 — Definitively canceled transactions permanently occupy journal slots

Evidence: journal preparation (privacy-frontend/src/joinsplit-v2/settlement.ts:209), settlement error path (privacy-frontend/src/joinsplit-v2/settlement.ts:427), 64-record limit (privacy-frontend/src/joinsplit-v2/pendingJournal.ts:793), removal restriction (privacy-frontend/src/joinsplit-v2/pendingJournal.ts:1161), and reconciliation (privacy-frontend/src/joinsplit-v2/journalReconciliation.ts:318).

Settlement durably prepares a journal record before requesting the wallet signature. A definite wallet ACTION_REJECTED does not transition that record to a reclaimable terminal state. Public removal rejects unfinalized records, while reconciliation retains records whose nullifiers remain unused.

Reproduction: The actual settlement path and actual encrypted journal accumulated 64 prepared records from 64 user-rejected wallet prompts. The 65th attempt failed with a full journal before prompting. Supported removal rejected every record; reconciliation retained all 64 as ambiguous.

Impact: Ordinary repeated cancellations eventually disable journal-dependent operations for that storage context. This is not a malicious proof or on-chain loss. The reproduction enters settlement directly; ADV-02 can mask it in affected browsers until the locking issue is resolved.

Recommended change: Track and safely reclaim definitively unsubmitted cancellations or pre-broadcast failures. Preserve records for ambiguous submission outcomes. An unused nullifier at one block is not proof that a signed/submitted transaction cannot settle later; do not make blanket deletion of pending entries the remedy.

ADV-06 — Zero-output bridge recovery cannot rediscover the transaction

Evidence: event discovery filter (privacy-frontend/src/joinsplit-v2/journalReconciliation.ts:115), transaction-hash discovery (privacy-frontend/src/joinsplit-v2/journalReconciliation.ts:329), bridge error result (privacy-frontend/src/joinsplit-v2/adapterOps.ts:321), and bridge UI error path (privacy-frontend/src/base-app/hooks/useBaseBridgeOperation.ts:228).

An exact-spend bridge has no private change note. If it broadcasts successfully but the response is lost before its transaction hash is persisted, recovery has neither an output-note insertion nor a saved hash. Its event fallback includes withdrawals and swaps, but not PrivateBridge. The bridge adapter/UI also discard settlement error context that can contain the journal ID and transaction hash.

Reproduction: With a prepared zero-output bridge record, used source nullifiers, and no saved hash, the actual reconciler retained the record as ambiguous and performed no bridge-log or transaction-evidence lookup.

Impact: The application cannot automatically establish completion, recover tracking context, or finalize/clean up the journal entry for this interruption scenario. The test does not establish loss of bridged assets or unauthorized bridge execution.

Recommended change: Add receipt-backed bridge discovery tied to the exact source nullifiers and expected bridge/order intent, with existing canonicality and finality checks. Preserve txHash and journalId through the adapter and UI. Test interrupted submission both with and without change notes.

ADV-07 — SDK block-pinned snapshots silently use latest on ethers v6

Evidence: SDK snapshot call (privacy-protocol/packages/sdk/src/joinsplit-v2/snapshot.ts:57), provider interface (privacy-protocol/packages/sdk/src/joinsplit-v2/types.ts:350), and frontend compatibility wrapper (privacy-frontend/src/joinsplit-v2/poolAccess.ts:99).

The SDK uses provider.call(transaction, blockTag). Its structural interface accepts a native ethers v6 provider, but that provider takes the block tag inside the transaction request and ignores the extra argument. The returned snapshot still reports the selected block, and rechecking that block's hash does not pin the independently executed calls.

Reproduction: The built exported SDK was invoked with a native ethers AbstractProvider subclass. Its actual ethers call normalization sent all five reads to latest while the snapshot claimed block 100. Only the provider's RPC-performing hook was mocked.

Impact and boundary: Direct SDK consumers can get mixed-block tree/root/token/fee state, causing inconsistent proof inputs or failed operations. The current frontend's explicit pinnedReadProvider wrapper correctly moves the block tag into the request and avoids this defect. No proof-forgery consequence was established.

Recommended change: Use the ethers v6 call shape and align the exported interface, or require an explicit, non-ambiguous adapter. Test with the native ethers provider normalization rather than only a custom two-argument mock.

Checks that passed

CheckResultImportant qualification
Targeted pool, verifier, adapter, deployment, and release-gate Foundry suites194 passed; 3 skippedBase mainnet fork cases skipped without BASE_MAINNET_RPC_URL
Additional adversarial settlement harness5 passedIncludes two fuzz properties at 64 runs each; proof-accepting mocks isolate settlement behavior
Rust base-decider library suite161 passed; 9 ignoredLocked, offline run; ignored cases were not exercised
Setup-versus-proving circuit matrix comparisons12 passedExact coefficient matrices, six action types with sparse/full 2x2 witnesses
SDK suite61 passedIncludes SDK TypeScript build
Targeted frontend suite76 passed across 11 suitesCRA-discovered frontend/proxy-auth tests; not a claim that all files under api/ ran
Node relayer suitePassedIncludes 13 V2 groups and local HTTP smoke checks; not a funded live-chain test

The new contract checks exercised rollback after a late native batch payout failure, rollback of earlier nullifier burns when a later input is invalid, native-callback reentrancy across six entry points, rejection of short-paying deposits without unfunded commitments, and exact-spend swaps from a retired source tree into the current output domain. The last case also checked that settlement uses measured output rather than an adapter's claimed return amount.

Passing unit suites did not detect the seven findings because the missing coverage lies in native API behavior, inter-service contracts, signing-cost policy, or interrupted operation lifecycle. The local reproductions therefore remain necessary regression candidates.

Reproduction artifacts

All paths below are local review artifacts outside the Git repositories. Run commands from workspace. The JavaScript reproductions use synthetic credentials, in-memory storage, and mocked chain/signing boundaries; they do not broadcast transactions.

FindingsArtifactCommand
ADV-01Relayer gas harness (.codex-tmp/zk-adversarial-2026-09-10/relayer-gas-envelope.mjs)node --experimental-vm-modules .codex-tmp/zk-adversarial-2026-09-10/relayer-gas-envelope.mjs
ADV-03, ADV-04Proxy contract harness (.codex-tmp/zk-adversarial-2026-09-10/proxy-pool-envelope.mjs)node --experimental-vm-modules .codex-tmp/zk-adversarial-2026-09-10/proxy-pool-envelope.mjs
ADV-02, ADV-05, ADV-06Frontend lifecycle harness (.codex-tmp/zk-review-2026-09-10/frontend-lifecycle-repro.cjs)node .codex-tmp/zk-review-2026-09-10/frontend-lifecycle-repro.cjs
ADV-07SDK snapshot harness (.codex-tmp/zk-review-2026-09-10/sdk-snapshot-blocktag-repro.cjs)node .codex-tmp/zk-review-2026-09-10/sdk-snapshot-blocktag-repro.cjs
Settlement propertiesFoundry harness (.codex-tmp/zk-adversarial-contracts/test/SettlementAdversarial.t.sol)Run forge test --match-contract SettlementAdversarialTest --summary in the sibling harness project using WSL
Circuit consistencyMatrix comparison source (.codex-tmp/zk-adversarial-2026-09-10/matrix-probe.rs)Built against the reviewed local Rust dependencies

Limits and remediation order

This is not an exhaustive verification of the whole StealthOS product, live deployments, infrastructure, dependencies, or supply chain. No live Base fork was available for the skipped tests; no funded relayer abuse was attempted. The pass did not independently rebuild and attest every distributed WASM/proving artifact, re-run a production ceremony, establish external Phase 2 lineage, or verify the implementation currently behind the external engine hostname. Passing matrix comparisons and release-gate tests do not substitute for those release/provenance checks.

Recommended order:

  1. Constrain the relayer's signing expenditure and correct the browser locking failure.
  2. Make the frontend/proxy/backend V2 contract work end-to-end, retaining privacy and authentication guarantees; resolve the HMAC mismatch if the Node service is the intended upstream.
  3. Repair definitively canceled journal cleanup and interrupted bridge recovery without weakening ambiguous-submission safety.
  4. Correct and regression-test the public SDK's native-provider snapshot semantics.
  5. Re-run this suite plus real-browser transaction flows and a configured Base fork, then validate the actual deployed service/artifact topology.

No remediation, commit, push, or deployment was performed in this review pass.