Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Internal audit report — Soroban on-chain verification seam

  • Scope: the code this repository controls around on-chain UltraHonk verification: the wire payload and calldata encoders (adapters/soroban/src/payload.rs, crates/ultrahonk/src/calldata.rs), the contract boundary and local double (adapters/soroban/src/contract.rs), the live network client (adapters/soroban/src/live.rs), the bb process layer (crates/ultrahonk/src/exec.rs), the verification-key store (crates/ultrahonk/src/store.rs), and the witness-material handling (crates/witness, adapters/simulator/src/oracle.rs, crates/ultrahonk/src/provider.rs).
  • Out of scope: the cryptographic core of the deployed verifier contract, which is Nethermind’s audited rs-soroban-ultrahonk (their audit, their VK parsing, their BN254 host-function usage). This report treats that contract as a trusted boundary and audits our seam to it.
  • Methodology: source review of every owned module, static dependency audit (cargo audit), and live on-chain invariant verification against the deployed testnet contract CCS6Z3VVCKV4F5BCH7VXJLKKWMDROUWOTZYROJ4T26CM7R45SE4IFYI2.
  • Date: 2026-09-08. Result: no critical or high findings.

1. Dependency audit

cargo audit against the RustSec advisory database (1242 advisories, 144 locked dependencies):

CategoryCount
Vulnerabilities0
Unmaintained0
Unsound0
Yanked0
Informational0

The pinned toolchains are additionally pinned by exact version (nargo 1.0.0-beta.26, bb 6.0.0-nightly.20260903, Rust 1.98) and checked at runtime, so a supply-chain substitution is detected by the version gates before any proving work.

2. Findings

F-1 (Low, fixed in this audit) — RPC client had no request timeout

LiveSorobanClient::post issued ureq::post with no .timeout(). ureq 2.x defaults to no timeout, so a hung RPC endpoint would block the caller indefinitely — relevant for an interactive verification path. Fixed: a 30s deadline covering the whole request (connect + write + read + body) is now set, and a timeout surfaces as LiveError::Rpc like any transport failure.

F-2 (Low, fixed in this audit) — provenance version drift in public_inputs.json

write_public_inputs_json hardcoded TESTED_BB_VERSION in the document’s bb_version field, while proof.json and vk.json carry the resolved key’s bb_version. With the current single toolchain the values coincide, but a store holding a key produced by a different bb version would emit three artifact files with mutually inconsistent provenance — exactly the drift class a verifier exists to catch. Fixed: the document now carries the resolved key’s bb_version, so the three files always agree.

F-3 (Info, accepted) — set_permissions failure is ignored in the oracle

adapters/simulator/src/oracle.rs writes the private Prover.toml and applies mode 0600 with let _ = on the set_permissions result. The enclosing scratch directory is created by tempfile::tempdir (0700, owner-only), so the file is unreachable by other users even if the chmod fails; the failure is only relevant on exotic filesystems. Accepted as-is with the tempdir boundary as the actual control.

F-4 (Info, by design) — error surface is deliberately small

The deployed contract exposes exactly six error codes; the off-chain layers surface ~70 typed variants. See docs/error-surface.md for the inventory and the 1:1-code-to-failure-mode policy. No changes made — padding codes would be inventory inflation, not hardening.

3. What was verified (live, against the deployed contract)

All checks were run against the live testnet contract through the production client code (CRUCIBLE_SOROBAN_LIVE=1 cargo test -p crucible-soroban-adapter --test live):

InvariantResult
Legit transfer fixture proof verifies on-chainOk(())
Tampered proof (1 byte flipped) rejectedError(Contract, #4) = VerificationFailed
Wrong-length proof rejected before crypto✅ length gate (Error #3)
On-chain VK equals committed fixture, byte-for-byte (1760 B)
vk_bytes() returns the committed VK from instance storage
Constructor re-initialization rejected✅ by source (AlreadyInitialized, deploy-only fn)
Cost oracle returns the RPC’s resource estimate✅ (verify_proof_with_cost)
Network healthy at audit time (ledger 4,571,410)

The on-chain VK check is cryptographically meaningful: the legit fixture verifies only because the contract holds exactly the committed VK — a different key would fail verification.

4. Defense-in-depth summary (owned layers)

  • Payload (payload.rs): versioned (PAYLOAD_VERSION=1), bounded (255-byte names, 4 GiB proof cap), fully validated on decode (checked-add cursor, UTF-8 names, calldata well-formedness), deterministic.
  • Calldata (calldata.rs): ABI-ordered, count-prefixed, 32-byte field words, rejects truncation and out-of-width values; decode rejects non-field values (≥ BN254 modulus) — a malformed submission can never become a plausible one.
  • bb process layer (exec.rs): paths-only execution (witness never read into the module), scheme/version provenance validation on every artifact, VK-digest cross-check between proof and vk documents, redacted one-line stderr excerpts (no witness material, 200-char cap).
  • VK store (store.rs): id-scheme parsing (uhk/circuit/version/hash), foreign ids hashed into a separate namespace (no collisions, no escape), scheme guard on write, malformed stored keys reported distinctly from missing keys.
  • Privacy: witness material lives only in 0600 files inside tempfile scratch dirs that are deleted when generation returns; errors carry paths and counts, never values.
  • Live client (live.rs): one RPC call per verification (simulation — no submission, no fees, no secrets), strkey types validated on both addresses, every malformed-response shape maps to a typed error, and now a hard 30s deadline.

5. Residual risks (external by nature)

  1. Mainnet deployment requires a funded mainnet account and a key-management review; testnet is fully covered.
  2. Toolchain divergence: the audited verifier accepts bb v0.87.0 proofs; the local backend emits bb 6.0.0-nightly proofs. The on-chain fixtures pin the on-chain format, but proofs from the local toolchain must be re-proven with the on-chain pin before submission. Tracked in docs/soroban-verification.md.
  3. Future verifier upgrades should re-run this audit’s live suite against the new deployment before trusting it.

6. Verification commands

cargo audit
CRUCIBLE_SOROBAN_LIVE=1 cargo test -p crucible-soroban-adapter --test live
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings