⚠️ Testnet-only. NOT audited. This repo contains a Soroban smart contract (contracts/hello-soroban) used as a verification fixture. Soroban contracts here handle auth/funds and MUST be reviewed by Bleu's in-house Rust engineer before any mainnet use or "audited" claim. Do not present generated contract code as production-ready.
Open-source, SEP-0058-native source verification for Soroban contracts. SEP-58 ("Contract Build Reproducibility for Verification") defines the metadata a contract publishes so that anyone can re-run its build; Soroscan Verify is the service that does the re-running: it reads the SEP-58 fields from the deployed Wasm, rebuilds the source inside a pinned, network-isolated build image, and byte-compares the result against the on-chain bytecode — proving (or refuting) that this source produces this Wasm.
This repo is the working MVP of the service: the SEP-58 metadata reader, the
chain reader, the verdict and image-trust logic, the content-addressed tarball
flow, and a pinned build image — all tested against a live testnet fixture.
The full design — hosted multi-verifier service, /v1 public API, UI, and
explorer integrations — is specified in
docs/ARCHITECTURE.md (see Roadmap).
License: Apache-2.0.
Soroban contracts are deployed as opaque Wasm blobs. When a contract is
uploaded, the ledger stores the bytecode in a ContractCodeEntry keyed by the
SHA-256 of the executable, and every contract instance references its code
by that hash. So source verification reduces to one testable question:
Can we rebuild a candidate source into a Wasm whose SHA-256 equals the hash the ledger reports for this contract?
Two SEPs address contract trust from different angles, and they are complementary, not competing:
- SEP-0055 (Contract Build Verification) answers "did a trusted CI pipeline build this Wasm from this repo?" via GitHub artifact attestations — provenance from a named builder, available instantly, no rebuild cost.
- SEP-0058 (Contract Build Reproducibility for Verification) answers "does this source, in this environment, produce exactly these bytes?" via independent re-execution on neutral infrastructure — the Soroban analogue of Sourcify's bytecode-match model for Ethereum.
SEP-58's own text calls the two complementary, addressing the same trust question with different trade-offs. A contract can and ideally does carry both; the service exposes them as distinct trust levels (separate API fields, never collapsed into one badge). This repo implements the SEP-58 rebuild side.
Every SEP-58 field drives one step of the verification pipeline. The fields
normally travel in the Wasm's contractmetav0 custom section (SEP-0046 is
the underlying meta transport); the full service also accepts them as an
off-chain submission for contracts deployed before the tooling existed.
| SEP-58 field | Meaning | Pipeline step that consumes it |
|---|---|---|
bldimg |
Build container image, pinned by digest | Image-trust lookup (MVP): checked against docker/allowlist.json and reported as the imageTrust tier on every verify result. In the full service the rebuild worker also pulls this image by digest — never by tag. |
bldopt |
One build flag per entry (repeatable) | Rebuild invocation: each flag is appended, in order, to stellar contract build inside the container (hosted rebuild — roadmap; the MVP builds with the pinned --locked invocation and surfaces bldopt via read). |
source_repo |
HTTPS URL of the source repository | Source acquisition, public-repo mode: clone the repo (acquisition is roadmap; the field is parsed and surfaced by read today). |
source_rev |
Full 40-char commit SHA-1 | Source acquisition, public-repo mode: check out exactly this commit — branch and tag names are not accepted. |
tarball_url |
Where to download the source tarball | Source acquisition, hosted-tarball mode: download over HTTPS or IPFS (roadmap). |
tarball_sha256 |
SHA-256 of the source tarball | Integrity gate (MVP): verify --tarball refuses to unpack or build a tarball whose digest doesn't match. On its own, it is the lookup key for content-addressed private source. |
From these fields the reader infers the contract's source mode — one per conformant combination in SEP-58 §2:
| Source mode | SEP-58 fields present | Meaning |
|---|---|---|
public-repo |
source_repo + source_rev |
Source is a VCS checkout at a pinned revision. |
hosted-tarball |
tarball_url + tarball_sha256 |
Source is a hosted archive pinned by digest. |
hosted-tarball-unpinned |
tarball_url alone |
Verifier downloads and extracts, trusting the host (no digest pin). |
content-addressed |
tarball_sha256 alone |
Private source committed by digest only; the archive is handed to the verifier out of band. |
none |
— | No SEP-58 source identifiers found. |
The diagram uses the SDK method names as they actually exist on
@stellar/stellar-sdk's rpc.Server (getContractWasmByContractId,
getContractWasmByHash, getLedgerEntries) — confirmed present on
rpc.Server.prototype in the installed SDK (v15.1.0).
flowchart TD
Dev[Developer / Auditor] -->|contract ID or wasm hash + SEP-58 source claim| UI[Verification UI - React/Next - roadmap]
UI -->|POST /v1/verifications - roadmap| API[Public API - /v1 - roadmap]
API -->|enqueue job| Q[Build Queue - roadmap]
API -->|fetch on-chain wasm| Reader[Chain Reader - stellar-sdk RPC - MVP]
Reader -->|getContractWasmByContractId / getContractWasmByHash| RPC[(Stellar RPC - ContractCodeEntry - SHA-256 hash)]
Reader -->|SEP-58 fields from contractmetav0 - bldimg bldopt source_repo source_rev tarball_url tarball_sha256| API
Reader -->|bldimg digest| Allow{Allowlist lookup - docker/allowlist.json - MVP}
Allow -->|imageTrust tier| Match
Q --> Worker[Reproducible Build Worker - pinned Docker - MVP]
Worker -->|pull pinned image by digest| Docker[(Build image - sdf-trusted or fallback - MVP)]
Worker -->|repo@commit - roadmap - or digest-gated tarball - MVP| Git[(Source - repo / tarball / content-addressed)]
Worker -->|stellar contract build --locked - target wasm32v1-none| WASM[Rebuilt WASM + SHA-256 - MVP]
WASM -->|compare hash and section diff| Match{Match verdict + image trust - MVP}
Match -->|FULL_MATCH / METADATA_ONLY_MATCH / NO_MATCH / ERROR| DB[(Verification registry - ed25519-signed results - roadmap)]
DB -->|mirror artifacts| IPFS[(IPFS pin - roadmap)]
DB --> Badge[Badge endpoint - GET /v1/badge/id.svg - roadmap]
Badge --> Explorer[Explorers - Stellar Expert / Stellar Lab Contract Explorer - roadmap]
DB --> Query[GET /v1/wasm/hash - GET /v1/contract/id - roadmap]
Query --> UI
Pieces labeled MVP are implemented and tested in this repo. Pieces labeled roadmap are specified in docs/ARCHITECTURE.md but not built here.
- SEP-58 metadata reader (
reader/src/sep58.ts): extracts the six SEP-58 fields (bldimg,bldopt(repeatable),source_repo,source_rev,tarball_url,tarball_sha256) from the decoded meta entries and infers the source mode. - contractmeta reader (
reader/src/contractmeta.ts): parses the SEP-0046contractmetav0Wasm custom section — the transport the SEP-58 fields travel in — and decodes its XDRSCMetaEntryrecords. Also powers theMETADATA_ONLY_MATCHverdict (strip the section, re-compare). - Chain reader (
reader/src/chain-reader.ts): TypeScript over@stellar/stellar-sdkRPC. Fetches the on-chain Wasm + SHA-256 for a contract ID (getContractWasmByContractId) or a Wasm hash (getContractWasmByHash). - Image trust (
reader/src/image-trust.ts): derives theimageTrusttier for a contract'sbldimgfromdocker/allowlist.json. - Tarball flow (
reader/src/tarball.ts,reader/src/builder.ts): digest-gated unpacking of content-addressed source tarballs (path-traversal safe) and the rebuild step (local pinned toolchain or the pinned Docker image). - Verify CLI (
reader/src/cli.ts, binsoroscan-verify):readandverifysubcommands — see Quick start. - Pinned build image (
docker/Dockerfile): the repo's fallback toolchain image — see Build images. - Sample contract (
contracts/hello-soroban): a minimalsoroban-sdkcontract, deployed to testnet, that the pipeline rebuilds and hash-matches. Rust,wasm32v1-none, built withstellar contract build --locked.
shasum -a 256 of the WASM produced by stellar contract build equals (a) the
"Wasm Hash" the CLI prints, (b) the hash the ledger stores in the
ContractCodeEntry (confirmed by stellar contract fetch of the deployed
fixture), and (c) the hash the TypeScript reader computes from the WASM it pulls
over RPC. All are:
6fe7bd58e5a33dc27daefc74acfae6eb70f101fdbde860475cf18fde87288e4b
Every verification carries two orthogonal dimensions: the match verdict (did the rebuild reproduce the bytes?) and the image trust tier (how trustworthy is the environment that the contract claims built it?).
| Verdict | Meaning |
|---|---|
FULL_MATCH |
Rebuilt WASM is byte-identical to on-chain WASM (SHA-256 equal). The deployed blob is the source. |
METADATA_ONLY_MATCH |
WASM differs only in the contractmetav0 custom section (behaviorally identical) — Sourcify "partial match" analogue. |
NO_MATCH |
Hashes differ and the difference is not metadata-only. |
ERROR |
Could not fetch/compare (network, malformed ID, tarball digest mismatch, etc.). |
Reproducibility alone is not faithfulness to source: a hostile build image can
deterministically rewrite bytes and still pass byte-comparison. So every verify
result also carries an imageTrust tier, derived by looking up the contract's
SEP-58 bldimg in the checked-in docker/allowlist.json:
| Tier | Meaning |
|---|---|
sdf-trusted |
Image digest is on the SDF-trusted allowlist (official stellar-cli-docker releases). |
publicly-auditable |
Image is allowlisted as a publicly-auditable third-party image (e.g. this repo's pinned toolchain image). |
arbitrary |
A bldimg was declared but is not allowlisted. |
unknown |
No bldimg metadata available. |
A FULL_MATCH from an arbitrary image is weaker evidence of faithfulness to
source than one from an allowlisted image — which is why the two dimensions are
reported together but never merged.
The primary path is SDF-published build images: contracts declare a
bldimg pointing at a stellar/stellar-cli image from
stellar-cli-docker — which is explicitly SEP-58-compatible and itself
mandates digest pinning — and the verifier checks that digest against the
allowlist, granting the sdf-trusted tier. The allowlist file documents the
exact entry shape for these images; their published digests are recorded as
SDF releases them.
This repo's own pinned image (docker/Dockerfile, recorded in
docker/toolchain-manifest.json) is the
interim/fallback image: a publicly-auditable toolchain (pinned rustc,
stellar-cli, and Wasm target, built from a checked-in Dockerfile, run with
--network=none during compile) used for local verification and for contracts
that don't reference an SDF image. It is allowlisted at the
publicly-auditable tier, deliberately below sdf-trusted.
# Build the image (repo root as context: the fixture's locked dependency
# graph is prefetched into the image, so the actual compile can run with
# --network=none and never fetch — the staged source-acquisition model from
# docs/ARCHITECTURE.md §8).
docker build -f docker/Dockerfile -t soroscan-verify-builder:rust-1.91.1-cli-26.1.0 .
# Rebuild the contract network-isolated and check the hash
docker run --rm --network=none \
-v "$PWD/contracts":/work \
soroscan-verify-builder:rust-1.91.1-cli-26.1.0
shasum -a 256 contracts/target/wasm32v1-none/release/hello_soroban.wasmFor true reproducibility the image must be referenced by digest, like any
SEP-58 bldimg. This repo's image is currently built locally and pinned by
version tag only — its registry digest is recorded in
docker/toolchain-manifest.json and
docker/allowlist.json once the image is published
(both carry an explicit placeholder until then).
Allowlist eviction downgrades; it never deletes. If a digest is removed from the allowlist — say a vulnerability turns up in an image — past verifications that used it keep their records; only the trust tier reported for them is downgraded. Trust signals age; evidence does not.
Prereqs: rust 1.91.1, nodejs 24.11.0 (see .tool-versions), stellar CLI
26.1.0, pnpm. wasm32v1-none target installed.
# 1. Build + test the sample contract (produces the WASM)
cd contracts
cargo test --locked
stellar contract build --locked # -> target/wasm32v1-none/release/hello_soroban.wasm
# 2. Build the reader/CLI
cd ../reader
pnpm install
pnpm test # unit tests (no network)
pnpm run build
# 3. Read the on-chain WASM hash + SEP-58 source metadata for the deployed
# fixture: prints the contractmetav0 entries, the SEP-58 fields, and the
# inferred source mode (also accepts --wasm-hash <hex> instead of --id)
node dist/cli.js read \
--id CDVSGPL3HFBGJ6ZEYQUAVE3OH3XE2ZE5ZT2GWPA3LKOYVD4UBPQJ2VHB
# 4. Verify by ID: rebuild-compare against the chain. Prints the verdict and
# the imageTrust tier; exit 0 only on FULL_MATCH.
node dist/cli.js verify \
--id CDVSGPL3HFBGJ6ZEYQUAVE3OH3XE2ZE5ZT2GWPA3LKOYVD4UBPQJ2VHB \
--wasm ../contracts/target/wasm32v1-none/release/hello_soroban.wasm
# 5. Verify from a content-addressed source tarball (the SEP-58
# tarball_sha256 commitment model): the digest is checked FIRST — a tarball
# that doesn't match is never unpacked or built — then the source is
# unpacked to a fresh temp dir, rebuilt, and compared. --docker rebuilds
# inside the pinned image (build it first — see "Build images" above).
# Omitting --docker uses the local toolchain, which must be rust 1.91.1
# *outside* the repo tree too: the rebuild runs in a temp dir, where
# directory-scoped version managers (asdf, direnv) won't see .tool-versions.
tar -czf /tmp/hello-soroban-src.tar.gz -C ../contracts Cargo.toml Cargo.lock hello-soroban
node dist/cli.js verify \
--id CDVSGPL3HFBGJ6ZEYQUAVE3OH3XE2ZE5ZT2GWPA3LKOYVD4UBPQJ2VHB \
--tarball /tmp/hello-soroban-src.tar.gz \
--tarball-sha256 "$(shasum -a 256 /tmp/hello-soroban-src.tar.gz | cut -d' ' -f1)" \
--dockerAll subcommands take --json for machine-readable output. Or run the whole
thing via the driver:
scripts/verify.sh CDVSGPL3HFBGJ6ZEYQUAVE3OH3XE2ZE5ZT2GWPA3LKOYVD4UBPQJ2VHB
# add --docker to build inside the pinned image;
# add --tarball <path> --tarball-sha256 <digest> for the tarball flow| Field | Value |
|---|---|
| Network | Stellar testnet |
| Contract ID | CDVSGPL3HFBGJ6ZEYQUAVE3OH3XE2ZE5ZT2GWPA3LKOYVD4UBPQJ2VHB |
| WASM hash (SHA-256) | 6fe7bd58e5a33dc27daefc74acfae6eb70f101fdbde860475cf18fde87288e4b |
| WASM size | 1060 bytes |
| Toolchain | rust 1.91.1, stellar-cli 26.1.0, soroban-sdk 25.3.1, wasm32v1-none |
| Explorer | https://stellar.expert/explorer/testnet/contract/CDVSGPL3HFBGJ6ZEYQUAVE3OH3XE2ZE5ZT2GWPA3LKOYVD4UBPQJ2VHB |
The live chain-read test is opt-in: SOROSCAN_INTEGRATION=1 pnpm exec vitest run test/integration.testnet.test.ts.
- Determinism is shown, not assumed.
stellar contract build --lockedassertsCargo.lockis unchanged (the documented reproducible-build lever). Two clean rebuilds on this toolchain produce the identical hash above, matching the on-chain WASM. The Stellar docs do not guarantee byte-for-byte reproducibility orwasm-opt/--optimizedeterminism across machines — that is an open question handled by pinning the full toolchain via Docker image digest and by theMETADATA_ONLY_MATCHverdict for behaviorally-identical builds. - TESTNET ONLY. No mainnet config is wired up;
resolveNetworkrejects anything buttestnet. (The full service runs against both networks — roadmap phase 2.) - The fixture predates the SEP-58 stamping tooling, so its on-chain
metadata carries no SEP-58 fields (
readreports source modenoneandverifyreportsimageTrust: unknown). That is exactly the population the retroactive, off-chain submission path in docs/ARCHITECTURE.md §7 exists for; contracts built with the in-progressstellar contract build --verifiable(stellar-cli#2585) get the fields stamped in automatically. - Soroban SDK version. This fixture pins
soroban-sdk =25.3.1, the version OpenZeppelin'sstellar-contracts0.7.1requires (^25.3.0, verified via the crates.io sparse index) — Bleu's validated default base. OZ-composed contracts reproduce on this same SDK; real submissions select their build image by the SEP-58bldimgthey advertise.
This repo is the MVP core; the full plan, with an objective completion test per phase, is in docs/ARCHITECTURE.md §11. In summary:
| Milestone | Scope | Done when |
|---|---|---|
| MVP (this repo) | SEP-58 metadata reader, chain reader, verdict + image-trust logic, content-addressed tarball flow, pinned build image, deterministic hash-match proof on testnet. | Shipped — see Quick start. |
| 1 — Self-hostable verifier core | ed25519 result signing, allowlist enforcement with downgrade semantics, all three SEP-58 source modes with IPFS and the artifact store, sandboxed rebuild workers, written threat model. | A third party can stand up a full verifier from docs alone and verify the MVP fixture end to end. |
| 2 — Security audit and hosted deployment | Independent security audit; hosted service live on testnet + mainnet, including the retroactive (off-chain metadata) submission path. | Audit report and remediations public; GET /v1/contract/{id} answers for both networks in production. |
| 3 — Stable API, SDK, and docs | GET /v1/contract/{id}, GET /v1/wasm/{hash}, POST /v1/verifications, GET /v1/verifiers; client SDK; allowlist governance policy published. |
An integrator goes from docs to rendering verification state without contacting us; the under-15-minute walkthrough passes in CI. |
| 4 — Integrations | Badge endpoint (GET /v1/badge/{contractId}.svg), explorer embed, stellar-cli interaction in whichever shape the ecosystem standardizes, partner reference integration. |
A partner surface renders results in production for both networks. |
| 5 — Production operations | Runbook, monitoring, status page, on-call, peer-operator support. | SLO dashboards public; at least one external peer verifier running or in progress. |
- SEP-0058 Contract Build Reproducibility for Verification (
bldimg,bldopt,source_repo,source_rev,tarball_url,tarball_sha256) - SEP-0055 Contract Build Verification (GitHub-Attestation provenance — the complementary trust level; formalized from discussion #1573)
- SEP-0046 Contract Meta (
contractmetav0/ SCMetaEntry — the transport SEP-58 fields travel in) - stellar-cli-docker — SDF-published
stellar/stellar-cliimages (the primarybldimgpath) - stellar-cli#2585
stellar contract build --verifiableand stellar-cli#2586stellar contract verify— the in-progress local half of the SEP-58 story - Stellar CLI manual —
stellar contract build(--locked,--meta,--optimize) - "Sha256 hash of the executable" — Stellar upgrading-contracts docs
- Retrieve a contract code ledger entry (LedgerKeyContractCode / getLedgerEntries)
@stellar/stellar-sdkrpc.ServerAPI reference (method names)- Sourcify — Ethereum source verification (full vs partial match) prior art
- OpenZeppelin
stellar-contracts(Rust Soroban library) - Service design & roadmap: docs/ARCHITECTURE.md