Skip to content

Add --verifiable flag to stellar contract build#2585

Draft
fnando wants to merge 21 commits into
mainfrom
contract-build-verifiable
Draft

Add --verifiable flag to stellar contract build#2585
fnando wants to merge 21 commits into
mainfrom
contract-build-verifiable

Conversation

@fnando

@fnando fnando commented May 22, 2026

Copy link
Copy Markdown
Member

What

Adds a --verifiable flag to stellar contract build that performs a reproducible build inside a digest-pinned Docker container and stamps SEP-58 metadata (bldimg, source_uri, source_sha256, bldopt) into the resulting WASM so third parties can re-run the build and verify the output byte-for-byte.

New flags, all grouped under a Verifiable help section:

  • --verifiable — opt in to the reproducible build mode; implies --locked, infers --package (the default-member cdylib contracts) when omitted, and requires a clean git working tree.
  • --image — override the auto-selected container image. Must be digest-pinned (...@sha256:...); tag-only refs are rejected so bldimg stays content-addressed.
  • --source-sha256 — SEP-58 source identification: 64-char SHA-256 of the source. Optional — the archive is always generated and its SHA-256 computed for you. When supplied it acts as a pin: the build fails if it doesn't match the generated archive.
  • --source-uri — SEP-58 source identification: URI where the source can be obtained (any scheme, e.g. https://example.com/src.tar.gz). Optional.
  • -d/--docker-host (also reads DOCKER_HOST) — override the docker daemon endpoint.

A --verifiable build always generates the reproducible source archive, records its SHA-256 as source_sha256, writes a content-addressed copy to the data dir's archives/<sha256>.tar.gz, and builds from the extracted (permission-hardened) copy so the WASM comes from exactly the bytes that were hashed.

Each contract — explicit --package or inferred — is built with its own --package, which is forwarded to the build and recorded as a bldopt, so every WASM is independently reproducible and stable even if the workspace's default members change later. Multi-contract workspaces build every contract in a single container so the crates download, compiled dependencies, and target/ are shared.

Every bldopt is recorded as valid shell syntax: each build option is shell-escaped once at the source, quoting only the value side, so a verifier can join the recorded bldopts and replay the exact invocation through a shell. A flag like --env B='this is very nice' is stored as --env=B='this is very nice' (flag and key outside the quotes), which round-trips back to the original argument. Run with --verbose to print the full docker run … command the build executes (the same command surfaced in the error message if the container build fails).

--env NAME=VALUE

Some contracts read environment variables at build time (build scripts, proc-macros, env!). --env (repeatable) sets them for the build and lives on the shared build options, so contract build, deploy, and upload all accept it:

  • Without --verifiable it's set on the local build process.
  • With --verifiable it's passed to the container (docker -e) and recorded as its own bldopt (shell-escaped, e.g. --env=B='this is very nice') so a verifier replays the same build. Because it's embedded in the WASM meta, avoid secrets here.

Names are validated ([A-Za-z_][A-Za-z0-9_]*); values are kept verbatim.

Source archives

The source archive is built by walking the working directory and tarring it, honoring the project's own .gitignore/.ignore files; the .git directory itself is always skipped. The archive is rooted at the current working directory — run contract build --verifiable / contract archive from the project (or workspace) root you want archived, so a workspace member's build still gets the whole workspace (its root Cargo.toml/Cargo.lock).

Selection depends only on the in-tree files plus the .gitignore/.ignore files inside the archived tree — never on machine-specific state (the global gitignore, .git/info/exclude, and parent-directory ignore files are not consulted) — so the same tree always hashes to the same source_sha256 across machines. Paths that usually shouldn't ship but weren't ignored (.env, target/, .idea, …) are listed in a warning so you can add an ignore rule.

Because the archive is the working tree rather than a committed snapshot, both contract build --verifiable and contract archive refuse a dirty git tree (with a warning explaining why), so a recorded source_sha256 always corresponds to a committed state. When the source isn't a git repo, there's nothing to check and they proceed.

stellar contract archive

A standalone command to generate — or inspect — the same reproducible archive a verifiable build uses, sharing the exact byte-generation logic so the output is identical:

  • -o/--out-file <PATH> — write the gzipped tarball. Must end in .tar.gz or .tgz. Required unless --dry-run.
  • --dry-run — list the entries that would be archived plus the computed source_sha256, without writing anything. Handy for confirming contents before a verifiable build, or for producing the artifact to host at a --source-uri.

Why

SEP-58 defines how to verify that a deployed contract WASM came from a specific source built with a specific toolchain image. Until now the CLI had no built-in way to produce such a build — users had to assemble the docker invocation, run cargo inside it, and stamp the custom sections by hand. This makes it a first-class option on stellar contract build, and the source archive (auto-generated on build, or produced/inspected via stellar contract archive) closes the loop by being the exact artifact that source_sha256 refers to.

Known limitations

  • The verify side (download source_uri, check source_sha256, rebuild, byte-compare) is not part of this PR.
  • A purely local docker image can't be used directly; reference it by digest (e.g. via a local registry).

@github-project-automation github-project-automation Bot moved this to Backlog (Not Ready) in DevX May 22, 2026
@fnando fnando moved this from Backlog (Not Ready) to In Progress in DevX May 22, 2026
@fnando fnando self-assigned this May 22, 2026
@fnando fnando force-pushed the contract-build-verifiable branch from bdca27b to 629046f Compare June 17, 2026 02:55
@socket-security

socket-security Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatedcargo/​assert_cmd@​2.0.17 ⏵ 2.2.27910093100100
Updatedcargo/​reqwest@​0.12.23 ⏵ 0.12.2879 +210094 +1100100
Addedcargo/​stellar-strkey@​0.0.1889100100100100
Addedcargo/​tar@​0.4.4610010093100100
Addedcargo/​zeroize@​1.9.010010093100100
Addedcargo/​toml_edit@​0.25.12%2Bspec-1.1.010010093100100
Updatedcargo/​chrono@​0.4.42 ⏵ 0.4.459910093100100
Updatedcargo/​indexmap@​2.11.0 ⏵ 2.14.010010093100100
Updatedcargo/​log@​0.4.28 ⏵ 0.4.3210010093100100
Updatedcargo/​once_cell@​1.21.3 ⏵ 1.21.410010093100100
Updatedcargo/​rand@​0.9.2 ⏵ 0.8.6100100 +193100100
Updatedcargo/​semver@​1.0.26 ⏵ 1.0.28100 +1910093100100
Updatedcargo/​tempfile@​3.21.0 ⏵ 3.27.098 -110093100100
Updatedcargo/​assert_fs@​1.1.3 ⏵ 1.1.410010093 +4100100
Updatedcargo/​async-compression@​0.4.30 ⏵ 0.4.4210010093100100
Updatedcargo/​clap_complete@​4.5.57 ⏵ 4.6.59710093100100
Updatedcargo/​csv@​1.3.1 ⏵ 1.4.09310093100100
Updatedcargo/​env_logger@​0.11.8 ⏵ 0.11.1099 -110093 +4100100
Updatedcargo/​flate2@​1.1.2 ⏵ 1.1.910010093100100
Updatedcargo/​futures@​0.3.31 ⏵ 0.3.3210010093100100
Updatedcargo/​futures-util@​0.3.31 ⏵ 0.3.3210010093100100
Updatedcargo/​home@​0.5.11 ⏵ 0.5.129910093100100
Updatedcargo/​humantime@​2.2.0 ⏵ 2.3.010010093100100
Updatedcargo/​ignore@​0.4.23 ⏵ 0.4.269910093100100
Updatedcargo/​mockito@​1.7.0 ⏵ 1.7.29810093100100
Updatedcargo/​open@​5.3.2 ⏵ 5.3.59910093100100
Updatedcargo/​predicates@​3.1.3 ⏵ 3.1.410010093100100
Updatedcargo/​regex@​1.11.2 ⏵ 1.12.410010093 -1100100
Updatedcargo/​rpassword@​7.4.0 ⏵ 7.5.4100100 +193100100
Updatedcargo/​serial_test@​3.2.0 ⏵ 3.5.010010093 +4100100
Updatedcargo/​shell-words@​1.1.0 ⏵ 1.1.110010093100100
Updatedcargo/​testcontainers@​0.27.2 ⏵ 0.27.39610093100100
Updatedcargo/​tokio-util@​0.7.16 ⏵ 0.7.1810010093100100
See 5 more rows in the dashboard

View full report

@fnando fnando force-pushed the contract-build-verifiable branch 2 times, most recently from 557647b to 6e7c125 Compare June 17, 2026 19:31
@fnando fnando force-pushed the contract-build-verifiable branch from 6e7c125 to 1780110 Compare June 18, 2026 23:47
@socket-security

socket-security Bot commented Jun 18, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
License policy violation: cargo colored under MPL-2.0

Location: Package overview

From: ?cargo/mockito@1.7.2cargo/colored@3.1.1

ℹ Read more on: This package | This alert | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore cargo/colored@3.1.1. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: cargo hyper-util is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?cargo/reqwest@0.12.28cargo/mockito@1.7.2cargo/bollard@0.20.2cargo/hyper-util@0.1.20

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore cargo/hyper-util@0.1.20. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
License policy violation: cargo libdbus-sys under GPL-2.0-only

License: GPL-2.0-only - The applicable license policy does not permit this license (5) (libdbus-sys-0.2.7/vendor/dbus/m4/pkg.m4)

License: BSD-3-Clause-HP - The applicable license policy does not permit this license (5) (libdbus-sys-0.2.7/vendor/dbus/cmake/modules/COPYING-CMAKE-SCRIPTS)

License: GPL-2.0+ - The applicable license policy does not permit this license (5) (libdbus-sys-0.2.7/vendor/dbus/COPYING)

From: ?cargo/keyring@3.6.3cargo/libdbus-sys@0.2.7

ℹ Read more on: This package | This alert | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore cargo/libdbus-sys@0.2.7. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: cargo openssl is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?cargo/reqwest@0.12.28cargo/openssl@0.10.81

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore cargo/openssl@0.10.81. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: cargo writeable is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?cargo/url@2.5.8cargo/writeable@0.6.3

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore cargo/writeable@0.6.3. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: cargo zerocopy is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?cargo/reqwest@0.12.28cargo/mockito@1.7.2cargo/bollard@0.20.2cargo/sep5@0.1.0cargo/stellar-xdr@27.0.0cargo/soroban-sdk@27.0.0-rc.1cargo/soroban-ledger-snapshot@27.0.0-rc.1cargo/ulid@1.2.1cargo/zerocopy@0.8.52

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore cargo/zerocopy@0.8.52. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@saimeunt

Copy link
Copy Markdown

@fnando Thanks for updating this PR to the latest SEP-58 revision, this is really helping prototyping a verification service.

While toying around with stellar contract build --verifiable I found it works great when overriding the auto-selected container image, but it failed at finding a correct default image for me.
The ideal path where things just work out of the box is preferred so we should be careful to have sane defaults.

The current approach to select the default image uses CARGO_PKG_VERSION from stellar-cli itself which will always be the stellar-cli version executing the stellar contract build --verifiable command, and it uses rustc_version which basically returns rustc --version which will be the local rust compiler version when building the contract.

I think we should try to parse the contract being built Cargo.toml instead and try to find appropriate values for the CLI version and rust version, for example in the increment soroban example it would return 25.1.0 (pick soroban-sdk version under [dependencies]) and 1.89.0 (read rust-version under [package]).

When these values are not available or we can't find an appropriate image we can default to the latest image (and warn the user).

After fetching the correct values from the package being built Cargo.toml, we can select the appropriate image by rebuilding the tag and trying to find a corresponding image on Docker Hub (at the moment it will fail if the tag isn't a perfect match).

When listing images from Docker Hub using the list_published_tags function, there's an issue with the RegEx used to capture potential tag names (https://github.com/stellar/stellar-cli/pull/2585/changes#diff-2b0cca9e11d4682a5fb7c67d5a12fc8ec010305a77fce2cf5147b11e74bc112bR641): problematic regex is ^(\d+\.\d+\.\d+)-rust(\d+\.\d+\.\d+)$ (anchored $), so it won't match the latest available image which is 27.0.0-rust1.96.0-slim-trixie.

My suggestion: wire the published tags search from Docker Hub not only when the wanted image isn't found, use it to find the correct image having the built package Cargo.toml CLI version and rust version found in its tag name using a fixed RegEx (to account for tags being suffixed eg. -slim-trixie).

This will fix the current behavior where not passing --image always results in a failure (failing to fetch 27.0.0-rust1.96.0 as it doesn't exist) and will improve the sane defaults of trying to find an appropriate image matching the built package CLI/rust versions when not specifying an image.

@fnando

fnando commented Jun 19, 2026

Copy link
Copy Markdown
Member Author

@saimeunt the auto image selection still needs to be worked on. I'm focusing on explicit setting the image for now on my tests. We didn't have the images ready while I started doing this draft.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

2 participants