6. Creating Verifiable Contracts
When you deploy a contract, only its compiled Wasm bytecode lives on-chain. Anyone can read that bytecode, but it tells them nothing about which source code produced it. A verifiable contract closes that gap: it embeds enough information for an independent third party to take the published source, rebuild it, and confirm that the result is byte-for-byte identical to what's deployed.
This is what SEP-58 standardizes. It defines a shared vocabulary for the build environment information needed to reproduce a contract's Wasm bytes from source, so that independent tools can interoperate without prescribing a single workflow.
In this guide, we'll make the hello_world contract from the previous lessons verifiable by:
- Building it inside a pinned, reproducible container image.
- Publishing a source archive and recording its hash.
- Embedding SEP-58 metadata into the Wasm at build time.
- Inspecting that metadata and walking through how a verifier reproduces the build.
This tutorial assumes you've already completed Setup and Hello World, and that you have a hello_world contract you can build.
Why reproducible builds?
The whole scheme rests on one idea: if two people compile the same source code in the same environment, they should get the same bytecode. In practice, "the same environment" is surprisingly hard to pin down—the compiler version, build flags, and even the host architecture can all change the output.
SEP-58 makes the environment explicit by recording four pieces of metadata directly inside the contract's Wasm custom section:
| Field | Required | Description |
|---|---|---|
bldimg | Yes | Fully-qualified container image used for the build, pinned by digest. |
bldopt | No | A single shell-style flag passed verbatim as one argument to the build command. Repeat the field once per flag. |
source_sha256 | Yes | SHA-256 of the source archive's bytes. |
source_uri | No | URI from which the source archive can be downloaded. |
Because the metadata is embedded in the Wasm itself, it travels with the contract: anyone holding the deployed bytecode can read exactly how to rebuild it.
Step 1: Pin the build image
Reproducibility starts with the toolchain. Instead of relying on whatever version of Rust and the Stellar CLI happen to be installed locally, we build inside a container image pinned by its digest.
The digest must reference a single-architecture manifest, not a multi-arch manifest list. A manifest list resolves to different bytes on linux/amd64 versus linux/arm64, which defeats reproducibility. Pull the per-architecture digest from your registry (for example, with docker buildx imagetools inspect) and pin that.
Store the fully-qualified, digest-pinned image in a variable so we can reuse it consistently. The following example uses the Docker image for Stellar CLI 27.0.0 for arm64.
- macOS/Linux
- Windows (PowerShell)
IMAGE="docker.io/stellar/stellar-cli@sha256:c1297d0c2c6790dda6afaa3edd39a959ec12edd6ebe30282dd1d7a663e7c4109"
$IMAGE = "docker.io/stellar/stellar-cli@sha256:c1297d0c2c6790dda6afaa3edd39a959ec12edd6ebe30282dd1d7a663e7c4109"
To keep an in-source toolchain selector (such as a rust-toolchain.toml in your project) from silently swapping the toolchain mid-build, export RUSTUP_TOOLCHAIN to the image's default before building. We'll pass this variable into the container in Step 3.
- macOS/Linux
- Windows (PowerShell)
RUSTUP_TOOLCHAIN=$(docker run --rm --entrypoint rustup "$IMAGE" default | cut -d' ' -f1)
$RUSTUP_TOOLCHAIN = (docker run --rm --entrypoint rustup $IMAGE default).Split(" ")[0]
Step 2: Create and hash the source archive
Verifiers need the exact source that produced the contract. Package it into a deterministic archive whose files live within a single top-level directory, so extracting it yields exactly one directory holding the source tree.
git archive is a convenient way to produce such an archive from a specific commit:
git archive --format=tar.gz --prefix=source/ HEAD > hello-world-v1.0.0.tar.gz
Now compute the SHA-256 of the archive's bytes—this value goes into source_sha256:
- macOS/Linux
- Windows (PowerShell)
sha256sum hello-world-v1.0.0.tar.gz
Get-FileHash hello-world-v1.0.0.tar.gz -Algorithm SHA256
Upload the archive somewhere durable (your release page, a content-addressed store, etc.) and use that location as source_uri. The source_uri is optional—if you omit it, verifiers must obtain the archive out of band—but publishing it makes verification self-service.
Even if you don't publish the source publicly, store it somewhere safe. If you lose the source, you may not be able to reproduce the build or verify the contract later, depending on how your archive file was generated.
Step 3: Build with embedded metadata
Now build the contract inside the pinned image, passing each SEP-58 field with a --meta flag. Every build option that affects the output should also be recorded as a bldopt so a verifier can replay the exact same command.
- macOS/Linux
- Windows (PowerShell)
docker run --rm -v "$PWD:/source" -e RUSTUP_TOOLCHAIN "$IMAGE" \
contract build \
--manifest-path=contracts/hello_world/Cargo.toml \
--package=hello_world \
--optimize \
--locked \
--meta bldimg="$IMAGE" \
--meta bldopt=--manifest-path=contracts/hello_world/Cargo.toml \
--meta bldopt=--package=hello_world \
--meta bldopt=--optimize \
--meta bldopt=--locked \
--meta source_uri=https://example.com/hello-world-v1.0.0.tar.gz \
--meta source_sha256=7f9b6cee586f8d029699ac11b32d28daa1c67c3181f6f92537b419d380575d7f
docker run --rm -v "${PWD}:/source" -e RUSTUP_TOOLCHAIN $IMAGE `
contract build `
--manifest-path=contracts/hello_world/Cargo.toml `
--package=hello_world `
--optimize `
--locked `
--meta bldimg=$IMAGE `
--meta bldopt=--manifest-path=contracts/hello_world/Cargo.toml `
--meta bldopt=--package=hello_world `
--meta bldopt=--optimize `
--meta bldopt=--locked `
--meta source_uri=https://example.com/hello-world-v1.0.0.tar.gz `
--meta source_sha256=7f9b6cee586f8d029699ac11b32d28daa1c67c3181f6f92537b419d380575d7f
A few things to note:
- Each
--meta bldopt=...records one build flag verbatim. Repeat the flag once per option, mirroring exactly the flags you passed tocontract build. - The
--lockedflag matters for reproducibility. Without it, Cargo may re-resolve dependencies against the registry instead of using the versions pinned inCargo.lock, which can change the resulting bytecode. - Replace
source_uriandsource_sha256with the real values from Step 2.
The optimized, metadata-bearing Wasm is written to target/wasm32v1-none/release/hello_world.wasm.
Step 4: Inspect the embedded metadata
Confirm the metadata made it into the build with stellar contract info meta:
$ stellar contract info meta --wasm target/wasm32v1-none/release/hello_world.wasm
ℹ️ Loading contract spec from file...
Contract meta:
• rsver: 1.96.0 (Rust version)
• rssdkver: 26.1.0#175aa41306f383057a8cdfc84b68d931664fc34e (Soroban SDK version and its commit hash)
• cliver: 27.0.0#5a7c5fe76530bf4248477ac812fc757146b98cc4
• bldimg: docker.io/stellar/stellar-cli@sha256:c1297d0c2c6790dda6afaa3edd39a959ec12edd6ebe30282dd1d7a663e7c4109
• bldopt: --manifest-path=contracts/hello_world/Cargo.toml
• bldopt: --package=hello_world
• bldopt: --optimize
• bldopt: --locked
• source_uri: https://example.com/hello-world-v1.0.0.tar.gz
• source_sha256: 7f9b6cee586f8d029699ac11b32d28daa1c67c3181f6f92537b419d380575d7f
You should see the SEP-58 fields you supplied—bldimg, each bldopt, source_uri, and source_sha256—alongside the SDK metadata the Stellar CLI adds automatically. Because the metadata is part of the Wasm, it's preserved on-chain once the contract is deployed. This is the same information a verifier will read to verify your contract's source code.
Once you're satisfied, you can deploy the contract; just make sure you deploy with --wasm PATH. In this example, the full command would be stellar contract deploy --wasm target/wasm32v1-none/release/hello_world.wasm. Otherwise your contract will be rebuilt without the required metadata before being uploaded.
How verification works
You don't have to run verification yourself: the point of SEP-58 is that anyone can. But understanding the verifier's side shows why each step above matters. To verify a deployed contract, a third party:
- Reads the metadata from the deployed Wasm with
stellar contract info meta, recoveringbldimg, thebldoptflags,source_uri, andsource_sha256. - Downloads the source archive from
source_uriand confirms its bytes hash tosource_sha256. A mismatch means the published source isn't what was claimed. - Extracts the single top-level directory and rebuilds, replaying the recorded
docker runcommand—the samebldimg, the samebldoptflags, and exportingRUSTUP_TOOLCHAINto the image's default. - Compares the rebuilt Wasm hash against the deployed contract's bytecode. If they match, the deployed contract provably corresponds to the published source.
If every step lines up, the contract is verified: the source you can read is exactly the code that's running on-chain.
SEP-58 is intentionally narrow—it standardizes the vocabulary, not the tooling. That's what lets independent implementations interoperate: a verifier built by one team can check a contract built by another, as long as both speak the same metadata fields.
Summary
In this lesson, we learned how to:
- Embed SEP-58 build metadata (
bldimg,bldopt,source_uri,source_sha256) into a contract. - Produce a deterministic source archive and record its SHA-256 hash.
- Build inside a digest-pinned container image for reproducibility.
- Inspect embedded metadata with
stellar contract info meta. - Follow the verifier's workflow to confirm a deployed contract matches its source.
Making your contracts verifiable gives users and integrators a way to trust—not just hope—that the code they're interacting with is the code you published.