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 contractCCS6Z3VVCKV4F5BCH7VXJLKKWMDROUWOTZYROJ4T26CM7R45SE4IFYI2. - 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):
| Category | Count |
|---|---|
| Vulnerabilities | 0 |
| Unmaintained | 0 |
| Unsound | 0 |
| Yanked | 0 |
| Informational | 0 |
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):
| Invariant | Result |
|---|---|
| Legit transfer fixture proof verifies on-chain | ✅ Ok(()) |
| Tampered proof (1 byte flipped) rejected | ✅ Error(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
tempfilescratch 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)
- Mainnet deployment requires a funded mainnet account and a key-management review; testnet is fully covered.
- Toolchain divergence: the audited verifier accepts bb
v0.87.0proofs; the local backend emits bb6.0.0-nightlyproofs. 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 indocs/soroban-verification.md. - 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