Contract build verification
Publish discoverable build provenance, inspect deployed code and interpret verification results.
1. Inspect a deployed contract
Open the contract in the Atlas explorer and choose Check build. To confirm a specific release, open Verify my build and enter the full commit and Wasm SHA-256. For automated checks, use the contract C-address with the provenance endpoint. This checks signed build provenance. Use the Reflector DAO Mainnet contract below as a reference with a published attestation.
curl -q --include --fail-with-body --connect-timeout 5 --max-time 60 \
'https://api.stellaratlas.io/v1/contracts/CBQSUF57OYX4RIMCZV62DKN6JFOTEKPHIZASMJYOUOCNHGNG2P3XQLSE/provenance'The Reflector DAO v1.1.0 release has Wasm SHA-256 f60d33ec57eecee04b52d048c1c079983ff1b3834c8195fea811b025fb889361 and an attestation for source commit ac9740f12a50a81c1b96d1e8d929a6066d2d00a8. Compare the response’s wasmSha256 before using that reference. For the same digest, a verified result should name that commit in build.record.commit. The contract can be upgraded and the lookup can return an unresolved result.
Replace the address with your contract. Save networkPassphrase, observedLedger, checkedAt, wasmSha256, build.status and ledgerEvidence (if present) together. The observed ledger may not be the current network head. An upgrade can change the executable and digest.
ledgerEvidence set to configured_rpc_observation identifies a configured RPC observation, not an independent ledger-state proof. Native responses may omit this field. An absent or unrecognized value does not establish independent proof. Read build.status separately for the attestation outcome.
2. Make a build discoverable
Publish a GitHub artifact attestation from a publicly readable repository. Atlas looks up the signed evidence by the repository embedded in the deployed Wasm and the deployed Wasm’s SHA-256.
- Embed
source_repo=github:YOUR_ORG/YOUR_REPOin the Wasm metadata before building the artifact you deploy. This checker accepts that repository form, without a branch or commit suffix. See the contract metadata specification.Examplestellar contract build --meta source_repo=github:YOUR_ORG/YOUR_REPO - Use the repository’s GitHub Actions build to publish artifact provenance for the final Wasm bytes, including any optimization and metadata. Grant the job
id-token: writeandattestations: write, and give the attestation step the final Wasm as itssubject-path. Deploy that same file. The Reflector DAO reference workflow run produced the published attestation used above.Example# Run with the final artifact from the release workflow gh attestation verify path/to/final.wasm -R YOUR_ORG/YOUR_REPO sha256sum path/to/final.wasm - The supported bundle uses in-toto Statement v1, SLSA provenance v1 and GitHub workflow build provenance. Repository, source commit, workflow identity, certificate and transparency evidence must match. The source commit comes from verified provenance, not a metadata claim alone.
- After publication, open Verify my build on your contract page. Paste the full commit from the workflow run and the SHA-256 printed above, then select Verify build. Atlas reports a match only if it validates signed evidence for both values. Use Check again after correcting or publishing evidence. Native contract observations can be retained for one minute, and build outcomes for up to thirty minutes. Repeated requests do not force a fresh build check.
3. Handle verified and unresolved results
HTTP 200 means an evidence response was returned. Read build.status before displaying a verification badge. The API has no separate mismatch or unverified status; preserve the specific result below.
| build.status | Meaning and next action |
|---|---|
verified | The signed build binds to this Wasm, repository, commit and workflow. Inspect build.record links. |
invalid | An available attestation failed a binding or verification check. Check the artifact digest, source commit, workflow and signature; no detailed mismatch reason is returned. |
not_published | No attestation was found for this repository and digest. Publish the matching artifact attestation, then allow cached results to expire. |
metadata_missing | The Wasm has no source_repo entry. Include supported metadata in a future build; changing the bytes changes the digest. |
metadata_invalid | Wasm metadata could not be read or validated. Check its encoding, repeated entries and artifact hash. |
unsupported | The repository metadata or attestation format is unsupported. Use the supported GitHub repository and workflow provenance format. |
unavailable | Build evidence or trust material could not be checked. Treat verification as unresolved; retry later with backoff. |
builtin | This is a built-in Stellar asset contract, with no Wasm build to attest. |
not_found_at_ledger | The contract instance was not found at observedLedger. Check the address and RPC coverage; this does not mean it was never deployed. |
HTTP 400 with invalid_contract requires a valid C-address. HTTP 503 with verification_busy or contract_evidence_unavailable is a request failure; honor Retry-After before a deliberate retry. It is different from a successful response containing build.status=unavailable. A client timeout is also unresolved, not evidence of an invalid contract.
4. Keep the trust boundaries visible
For verified, follow build.record.sourceUrl to the exact commit and workflowUrl to its build workflow. runUrl can be null; attestationUrl locates the repository’s evidence for the digest. Other build outcomes have a null record. Embedded repository and home-domain metadata remain developer-supplied claims.
Build attested means the provenance check accepted the signed build for the observed Wasm. For a source-to-Wasm match, rebuild with pinned inputs and compare the complete output bytes. The offline recipe below uses a separate historical fixture. Contract safety and ledger-state inclusion require separate evidence. securityAudit remains not_assessed.
5. Reproduce a controlled build offline
This historical example uses existing Cargo and Stellar tools. It requires no hosted executor or GitHub Actions. Build attested describes accepted signed provenance; independently reproduced describes a separate byte comparison. The provenance API does not run this recipe or return a reproduction status.
Prepare and identify the inputs
Obtain the Stellar CLI source at commit 067b5a7c6084fdcd7900f2bf5d8af85df3898cae, preserving its upstream Apache-2.0 licensing. Use package test_hello_world 21.3.0 at cmd/crates/soroban-test/tests/fixtures/test-wasms/hello_world. Keep the upstream contract, its manifest, the tracked .cargo/config.toml and the test-wasms profile unchanged. The SDK is pinned to soroban-sdk =21.2.0.
In this repository, examples/contract-build-reproduction/ supplies the exact derived root Cargo.toml, Cargo.lock and wasm-target.sha256. Replace only the upstream root manifest and lock with these files. The derived workspace selects this fixture alone; 154 external dependency identities retain their original versions, sources and checksums. It is a deliberately narrowed recipe, not an unchanged CLI workspace build.
# In the prepared upstream source root, after replacing the two files:
printf '%s Cargo.toml
%s Cargo.lock
' \
'cd8cc44a5c0b8cd814fe572db1ee2e13d5900576266091aa4fa8e35d8d329cb0' \
'a9322ef77cbd1b45e6aea5e71474dcdd02ef6058d605c4895f8fa1d2fec88954' | sha256sum --checkPrepare a standard Cargo registry cache against this lock before disconnecting. The measured normal/build graph used 59 registry packages, including native build scripts and procedural macros; another 79 packages supported offline resolution/metadata, not the compiled feature graph. Keep package archives, extracted sources and registry index records. Check archive checksums against the lock. Do not fetch the unrelated full CLI workspace or replace registry sources with a different vendor configuration.
Use Linux amd64 and the image digest below. Its native compiler is GCC 12.2 with GNU ld 2.40. The image does not include the Wasm target: supply the matching Rust 1.80.1 wasm32-unknown-unknown target separately and check all 27 files with sha256sum --check wasm-target.sha256 from its root. Supply the exact Stellar optimizer binary separately. These retained binary hashes identify the tested tools; rebuilding the same version string alone does not establish identical tool bytes.
docker.io/library/rust@sha256:883df89a3ed53fb0e0f6a67d79cfebf72906d97f9d816379fdf941af969be1b5| Tool | SHA-256 of executable |
|---|---|
| rustc 1.80.1 (3f5fd8dd4 2024-08-06) | 0989d3ded21082db4f63935ad73fefeee8e7881a8b1bcb19a84800e63e2049aa |
| cargo 1.80.1 (376290515 2024-07-16) | 9d42d3b2535ed528f59f537d01ecdfc821ef30562495a9341c08c756d2b31c40 |
| Stellar CLI 21.3.0, opt feature | 6bbb96f3cd230543fb5c19e8c5bda5a24bb6da8b9502659a07aa3af2e4441f26 |
The optimizer is Stellar CLI 21.3.0 with the opt feature, embedding wasm-opt 0.114.2, aggressive size optimization and convergence, with MVP, MutableGlobals and SignExt. This CLI build does not accept modern contract build --locked or --image flags. Use the frozen Cargo step followed by the final optimizer below.
Build only from prepared inputs
For each attempt, start a fresh isolated container from the pinned image, with an empty target directory and fixed paths. Mount source read-only at /source, the prepared registry read-only at /deps/registry, and the optimizer read-only at /tools/stellar. Mount the checked target read-only at /usr/local/rustup/toolchains/1.80.1-x86_64-unknown-linux-gnu/lib/rustlib/wasm32-unknown-unknown. Make /build a fresh writable, executable 4 GiB tmpfs; native build scripts and procedural macros execute there.
Run as UID/GID 1000:1000 with a read-only root filesystem, network disabled, private IPC, all capabilities dropped, no-new-privileges and the default seccomp policy. Limit each attempt to one CPU, 4 GiB memory with no additional swap, 128 PIDs and 15 minutes; cap logs at 8 MiB and the final artifact at 1 MiB. Mount no credentials, Docker socket, reference artifact or previous output. Review source before allowing its build scripts to execute.
The following are commands inside that prepared sandbox. Stop on any nonzero exit. Apply the displayed clean environment to both processes; unset ambient Rust flags, compiler wrappers and toolchain overrides. The tracked Cargo configuration remains active because the working directory is /source. Do not install tools, pull images or download dependencies during this phase.
set -eu
mkdir -p /build/home /build/tmp /build/cargo-home /build/final
ln -s /deps/registry /build/cargo-home/registry
cd /source
run_clean() { env -i \
PATH=/usr/local/rustup/toolchains/1.80.1-x86_64-unknown-linux-gnu/bin:/usr/bin:/bin \
HOME=/build/home TMPDIR=/build/tmp CARGO_HOME=/build/cargo-home \
CARGO_TARGET_DIR=/build/target CARGO_NET_OFFLINE=true CARGO_BUILD_JOBS=1 \
RUSTC=/usr/local/rustup/toolchains/1.80.1-x86_64-unknown-linux-gnu/bin/rustc \
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=/usr/bin/cc \
SOURCE_DATE_EPOCH=1723217212 TZ=UTC LANG=C "$@"; }
run_clean /usr/local/rustup/toolchains/1.80.1-x86_64-unknown-linux-gnu/bin/cargo rustc \
--frozen --offline \
--manifest-path=/source/cmd/crates/soroban-test/tests/fixtures/test-wasms/hello_world/Cargo.toml \
--package=test_hello_world --crate-type=cdylib \
--target=wasm32-unknown-unknown --profile=test-wasms
# Only after Cargo exits successfully, under the same clean environment:
run_clean /tools/stellar contract optimize \
--wasm=/build/target/wasm32-unknown-unknown/test-wasms/test_hello_world.wasm \
--wasm-out=/build/final/test_hello_world.optimized.wasmCompare the final bytes
Export the final optimized file before removing each container. Save the source, recipe, tool and dependency identities with the output. Compare the entire final files, including metadata and custom sections; a digest is a useful identity check, while cmp performs the full-byte comparison.
# Export final optimized bytes only after each successful build.
sha256sum reference.wasm rebuilt.wasm
cmp --silent reference.wasm rebuilt.wasm
# cmp: 0 = exact match; 1 = mismatch; >1 = comparison error.
# Check its exit status; do not strip or normalize either file.| Controlled output | Bytes | SHA-256 | Comparison |
|---|---|---|---|
| A: newly authored local reference | 2837 | 03cf774f7ddddd0e07dcff76321da10fe735b87bf15bbfa200c0de7e986eb19d | Reference only |
| B: clean rebuild of A source | 2837 | 03cf774f7ddddd0e07dcff76321da10fe735b87bf15bbfa200c0de7e986eb19d | Full-byte match with A |
| C: first Hello literal changed to Hallo | 2986 | 34ba5ea14258bf786d3f475b34e09e4a1a1ab6aebf812019e6c137667c7573d4 | Successful build; mismatch with A |
| A plus empty custom section (00 01 00) | 2840 | afd28eba60f8729ab347bcebfaa16f3d39b263f8313dd1566462bc468dd54446 | Valid Wasm; mismatch with B |
A and B were actual separate clean builds on the same operator’s host. C changed only the first symbol_short!("Hello") literal to symbol_short!("Hallo") before another successful build. The last row appended an empty custom section to A without another build. All four outputs passed WebAssembly structural/type validation; no Soroban host loading or contract invocation was tested.
A is a newly authored local reference, not a publisher artifact or a deployed contract. B matching A demonstrates repeatability under these controlled inputs. It does not establish independent operators, source authenticity, compiler correctness, safe behavior or a security audit. Fresh-machine bootstrap, current-network compatibility, publisher attestation and deployed identity were not tested.
Record not_run when a required input is missing, the recipe is invalid, a build or optimizer fails, or a timeout prevents a final comparison. Only successful final outputs can yield a match or mismatch; a mismatch alone does not establish malicious behavior. If exact historical inputs cannot be obtained, retain not_run for this recipe and qualify a different pinned recipe with its own reference.