Uh oh!
There was an error while loading. Please reload this page.
Add --verifiable flag to stellar contract build - #2709
Conversation
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Adds SEP-58 reproducible contract builds and source archive generation.
Changes:
- Adds
--verifiablebuild flags and provenance metadata. - Adds deterministic source archiving and
contract archive. - Extends container execution, artifact handling, tests, and documentation.
Reviewed changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
FULL_HELP_DOCS.md | Documents new commands and flags. |
cmd/soroban-cli/src/config/locator.rs | Adds recursive permission hardening. |
cmd/soroban-cli/src/config/data.rs | Adds managed archive storage. |
cmd/soroban-cli/src/commands/mod.rs | Adds verifiable help heading. |
cmd/soroban-cli/src/commands/contract/mod.rs | Registers archive command. |
cmd/soroban-cli/src/commands/contract/build/verifiable.rs | Implements verifiable builds. |
cmd/soroban-cli/src/commands/contract/build/source_archive.rs | Implements reproducible archives. |
cmd/soroban-cli/src/commands/contract/build/container.rs | Shares container and artifact logic. |
cmd/soroban-cli/src/commands/contract/build.rs | Adds flags and dispatch. |
cmd/soroban-cli/src/commands/contract/archive.rs | Implements archive CLI. |
cmd/soroban-cli/src/commands/container/shared.rs | Adds streamed image pulling. |
cmd/soroban-cli/Cargo.toml | Adds archive dependencies. |
cmd/crates/soroban-test/tests/it/build.rs | Adds integration coverage. |
Cargo.lock | Locks dependency updates. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
d58a8a7 to
fce6107CompareThere was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 14 changed files in this pull request and generated 1 comment.
Suppressed comments (6)
cmd/soroban-cli/src/commands/contract/build/container.rs:304
- Metadata keys are accepted as arbitrary strings by
parse_meta_arg, but this records the key unescaped. A key containing whitespace makes the stampedbldoptsplit into multiple shell words, and shell metacharacters such as;can execute commands when a verifier replays the joined options. Escape the key portion as well as the value (or reject non-shell-safe metadata keys).
bldopts.push(format!("{key}={}", shell_escape::escape(v.into())));
cmd/soroban-cli/src/commands/contract/build/container.rs:760
- The container is explicitly forced to write to
/source/targetviaCARGO_TARGET_DIR, so using the host metadata target here breaks collection whenever the host hasCARGO_TARGET_DIRor a configuredtarget-dir. Plain builds then return a missing/stale path; for verifiable builds an absolute metadata path also causesjointo discard the extracted root and can select an old host WASM instead of the newly built artifact. Collect from the forcedtargetdirectory.
let host_target = md.target_directory.as_std_path();
cmd/soroban-cli/src/commands/contract/build/source_archive.rs:207
- This filter silently omits every symlink, including Git-tracked symlinked files and directories. Such links are part of the source tree and may be required by path dependencies or build scripts, so the archived source can fail to build or differ from the committed source. Preserve safe symlink entries deterministically, or reject them explicitly instead of dropping them.
if entry.file_type().is_some_and(|t| t.is_file()) {
files.push(entry.path().to_path_buf());
}
cmd/soroban-cli/src/commands/contract/build/verifiable.rs:309
- Hardening the extracted tree through this helper changes every file to mode
0600, removing executable bits from Git-tracked helper scripts. A contract whose build script invokes an executable from the repository will build normally but fail only in verifiable mode. Use source-specific hardening that preserves the owner execute bit while removing group/other access; keep config files at0600.
enforce_hardened_tree(tmp.path()).map_err(source_archive::Error::ArchiveExtract)?;
cmd/soroban-cli/src/commands/contract/build/source_archive.rs:129
- Every nonzero
git statusresult is treated as “not a repository.” Failures in a real repository (for example corrupt metadata, ownership checks, or configuration errors) therefore bypass the clean-tree requirement and allow an unverified working tree to be archived. First determine whether this is a work tree, and propagate status failures for repositories; only the explicit non-repository case should proceed.
// Not a git repo (or git refused): can't verify cleanliness, proceed.
if !status.status.success() {
return Ok(false);
}
cmd/soroban-cli/src/commands/contract/build/verifiable.rs:147
- The documented contract says
--verifiableimplies--locked, but this branch knowingly performs an unlocked build. That can update dependency resolution relative to the archived lockfile, so the stamped source/image inputs no longer guarantee the advertised reproducibility. Reject images older than the--lockedminimum for verifiable builds instead of degrading to an unlocked build.
} else {
print.warnln(
"The build image's `contract build` does not support --locked; \
building without it. Dependency drift may affect reproducibility.",
);
}
Uh oh!
There was an error while loading. Please reload this page.
fce6107 to
4988080CompareThere was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 14 changed files in this pull request and generated 3 comments.
Suppressed comments (2)
cmd/soroban-cli/src/commands/contract/build/container.rs:304
- Only escaping
vdoes not make every recorded option valid shell syntax becauseparse_meta_argallows metadata keys containing spaces or shell metacharacters. For example,--meta 'my key=value'is forwarded as one argv item but recorded as--meta=my key=value, which splits into two arguments during replay. Escape the key segment as well so the recorded bldopt round-trips.
bldopts.push(format!("{key}={}", shell_escape::escape(v.into())));
cmd/soroban-cli/src/commands/contract/build/verifiable.rs:149
- A verifiable build is documented to imply
--locked, but an older pinned image reaches this branch and the build continues without it. That permits dependency resolution to drift between the original build and a verifier's replay, defeating the reproducibility guarantee. Please reject images whose CLI does not support--lockedinstead of producing a “verifiable” artifact without it.
} else {
print.warnln(
"The build image's `contract build` does not support --locked; \
building without it. Dependency drift may affect reproducibility.",
);
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
4988080 to
f1ab06bCompareThere was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 14 changed files in this pull request and generated 3 comments.
Suppressed comments (1)
cmd/soroban-cli/src/commands/contract/build/container.rs:304
- SEP-58 defines each
bldoptas one value passed verbatim as an argv argument (“as if single-quoted”), not as shell source to evaluate. Escaping onlyvstores literal quote characters: an original--meta=note=added on buildis recorded as--meta=note='added on build', so a conforming verifier passes the apostrophes into the metadata value and cannot reproduce the WASM. Record the raw{key}={v}argument instead, and update the shell-roundtrip test/documentation accordingly.
bldopts.push(format!("{key}={}", shell_escape::escape(v.into())));
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
f1ab06b to
42db3e5CompareThere was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 14 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
cmd/soroban-cli/src/commands/contract/build/source_archive.rs:127
git status --porcelainhides files ignored by global excludes,.git/info/exclude, and parent ignore files. The archive walker explicitly disables those sources, so a machine-local file such as a globally ignored.envcan pass this “clean tree” gate and then be included and persisted in the archive. Validate cleanliness against the actual selected archive entries (for example, reject selected files that are not tracked) so local ignored files cannot leak or makesource_sha256machine-specific.
.arg("status")
.arg("--porcelain")
Uh oh!
There was an error while loading. Please reload this page.
What
Adds a
--verifiableflag tostellar contract buildthat 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. It also adds a standalonestellar contract archivecommand. The container connection/resource flags (--image,--pull,--docker-host,--engine,--cpus/--memory) are the existing container-build arguments, reused here.New flags, grouped under a
Verifiablehelp 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. Requires--imagepinned by digest (<registry-host>/<repo>@sha256:<64-hex>); tag-only refs are rejected so the recordedbldimgnames the exact bytes.--pullrefreshes the image up front; otherwise a digest-pinned ref is used as-is (a missing image is fetched by the run itself, matching the plain container build).--source-sha256— SEP-58 source identification: 64-char lower-case hex 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 (with a scheme, e.g.https://example.com/src.tar.gz) where the source can be obtained. Requires--source-sha256.A
--verifiablebuild always generates the reproducible source archive, records its SHA-256 assource_sha256, writes a content-addressed copy to the data dir'sarchives/<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
--packageor inferred — is built with its own--package, which is forwarded to the build and recorded as abldopt, 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, andtarget/are shared.Every
bldoptis 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 recordedbldopts 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--verboseto print the fulldocker run …command the build executes (the same command surfaced in the error message if the container build fails).Source archives
The source archive is built by walking the working directory and tarring it, honoring the project's own
.gitignore/.ignorefiles; the.gitdirectory itself is always skipped. The archive is rooted at the current working directory — runcontract build --verifiable/contract archivefrom the project (or workspace) root you want archived, so a workspace member's build still gets the whole workspace (its rootCargo.toml/Cargo.lock).Selection depends only on the in-tree files plus the
.gitignore/.ignorefiles 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 samesource_sha256across 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 --verifiableandcontract archiverefuse a dirty git tree (with a warning explaining why), so a recordedsource_sha256always corresponds to a committed state. When the source isn't a git repo, there's nothing to check and they proceed.stellar contract archiveA 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.gzor.tgz. Required unless--dry-run.--dry-run— list the entries that would be archived plus the computedsource_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 viastellar contract archive) closes the loop by being the exact artifact thatsource_sha256refers to.