Add --verifiable flag to stellar contract build - #2709

Open
fnando wants to merge 1 commit into
mainfrom
contract-verifiable-build
Open

Add --verifiable flag to stellar contract build#2709
fnando wants to merge 1 commit into
mainfrom
contract-verifiable-build

Conversation

@fnando

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. It also adds a standalone stellar contract archive command. 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 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. Requires --image pinned by digest (<registry-host>/<repo>@sha256:<64-hex>); tag-only refs are rejected so the recorded bldimg names the exact bytes. --pull refreshes 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 --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).

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.

@fnando
fnando requested a review from a team as a code ownerAugust 31, 2026 17:17
CopilotAI balanced review requested due to automatic review settings August 31, 2026 17:17
@github-project-automationgithub-project-automationBot moved this to Backlog (Not Ready) in DevXAug 31, 2026
@socket-security

socket-securityBot commented Aug 31, 2026

Copy link
Copy Markdown

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

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Addedcargo/​tar@​0.4.4610010093100100
Updatedcargo/​ignore@​0.4.23 ⏵ 0.4.3398-110093100100

View full report

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds SEP-58 reproducible contract builds and source archive generation.

Changes:

  • Adds --verifiable build 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
FileDescription
FULL_HELP_DOCS.mdDocuments new commands and flags.
cmd/soroban-cli/src/config/locator.rsAdds recursive permission hardening.
cmd/soroban-cli/src/config/data.rsAdds managed archive storage.
cmd/soroban-cli/src/commands/mod.rsAdds verifiable help heading.
cmd/soroban-cli/src/commands/contract/mod.rsRegisters archive command.
cmd/soroban-cli/src/commands/contract/build/verifiable.rsImplements verifiable builds.
cmd/soroban-cli/src/commands/contract/build/source_archive.rsImplements reproducible archives.
cmd/soroban-cli/src/commands/contract/build/container.rsShares container and artifact logic.
cmd/soroban-cli/src/commands/contract/build.rsAdds flags and dispatch.
cmd/soroban-cli/src/commands/contract/archive.rsImplements archive CLI.
cmd/soroban-cli/src/commands/container/shared.rsAdds streamed image pulling.
cmd/soroban-cli/Cargo.tomlAdds archive dependencies.
cmd/crates/soroban-test/tests/it/build.rsAdds integration coverage.
Cargo.lockLocks dependency updates.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadcmd/soroban-cli/src/commands/contract/build/container.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/source_archive.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/build/container.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/build/source_archive.rs Outdated
@fnando
fnandoforce-pushed the contract-verifiable-build branch from d58a8a7 to fce6107CompareAugust 31, 2026 17:40
CopilotAI review requested due to automatic review settings August 31, 2026 17:40

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 stamped bldopt split 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/target via CARGO_TARGET_DIR, so using the host metadata target here breaks collection whenever the host has CARGO_TARGET_DIR or a configured target-dir. Plain builds then return a missing/stale path; for verifiable builds an absolute metadata path also causes join to discard the extracted root and can select an old host WASM instead of the newly built artifact. Collect from the forced target directory.
 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 at 0600.
 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 status result 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 --verifiable implies --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 --locked minimum 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.",
);
}

Comment threadcmd/soroban-cli/src/config/locator.rs
CopilotAI review requested due to automatic review settings August 31, 2026 18:33
@fnando
fnandoforce-pushed the contract-verifiable-build branch from fce6107 to 4988080CompareAugust 31, 2026 18:33

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 v does not make every recorded option valid shell syntax because parse_meta_arg allows 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 --locked instead 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.",
);

Comment threadcmd/soroban-cli/src/commands/contract/archive.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/archive.rs Outdated
Comment threadcmd/soroban-cli/src/config/locator.rs Outdated
@fnando
fnandoforce-pushed the contract-verifiable-build branch from 4988080 to f1ab06bCompareAugust 31, 2026 18:59
CopilotAI review requested due to automatic review settings August 31, 2026 18:59

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 bldopt as one value passed verbatim as an argv argument (“as if single-quoted”), not as shell source to evaluate. Escaping only v stores literal quote characters: an original --meta=note=added on build is 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())));

Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
@fnando
fnandoforce-pushed the contract-verifiable-build branch from f1ab06b to 42db3e5CompareSeptember 1, 2026 15:26
CopilotAI review requested due to automatic review settings September 1, 2026 15:26
@fnando
fnando requested review from a team and leighmcculloch and removed request for a teamSeptember 1, 2026 15:31

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 --porcelain hides 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 .env can 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 make source_sha256 machine-specific.
 .arg("status")
.arg("--porcelain")

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

Labels

None yet

Projects

Status: Backlog (Not Ready)

Development

Successfully merging this pull request may close these issues.

2 participants

@fnando
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add --verifiable flag to stellar contract build - #2709

Open
fnando wants to merge 1 commit into
mainfrom
contract-verifiable-build
Open

Add --verifiable flag to stellar contract build#2709
fnando wants to merge 1 commit into
mainfrom
contract-verifiable-build

Conversation

@fnando

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. It also adds a standalone stellar contract archive command. 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 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. Requires --image pinned by digest (<registry-host>/<repo>@sha256:<64-hex>); tag-only refs are rejected so the recorded bldimg names the exact bytes. --pull refreshes 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 --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).

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.

@fnando
fnando requested a review from a team as a code ownerAugust 31, 2026 17:17
CopilotAI balanced review requested due to automatic review settings August 31, 2026 17:17
@github-project-automationgithub-project-automationBot moved this to Backlog (Not Ready) in DevXAug 31, 2026
@socket-security

socket-securityBot commented Aug 31, 2026

Copy link
Copy Markdown

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

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Addedcargo/​tar@​0.4.4610010093100100
Updatedcargo/​ignore@​0.4.23 ⏵ 0.4.3398-110093100100

View full report

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds SEP-58 reproducible contract builds and source archive generation.

Changes:

  • Adds --verifiable build 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
FileDescription
FULL_HELP_DOCS.mdDocuments new commands and flags.
cmd/soroban-cli/src/config/locator.rsAdds recursive permission hardening.
cmd/soroban-cli/src/config/data.rsAdds managed archive storage.
cmd/soroban-cli/src/commands/mod.rsAdds verifiable help heading.
cmd/soroban-cli/src/commands/contract/mod.rsRegisters archive command.
cmd/soroban-cli/src/commands/contract/build/verifiable.rsImplements verifiable builds.
cmd/soroban-cli/src/commands/contract/build/source_archive.rsImplements reproducible archives.
cmd/soroban-cli/src/commands/contract/build/container.rsShares container and artifact logic.
cmd/soroban-cli/src/commands/contract/build.rsAdds flags and dispatch.
cmd/soroban-cli/src/commands/contract/archive.rsImplements archive CLI.
cmd/soroban-cli/src/commands/container/shared.rsAdds streamed image pulling.
cmd/soroban-cli/Cargo.tomlAdds archive dependencies.
cmd/crates/soroban-test/tests/it/build.rsAdds integration coverage.
Cargo.lockLocks dependency updates.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadcmd/soroban-cli/src/commands/contract/build/container.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/source_archive.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/build/container.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/build/source_archive.rs Outdated
@fnando
fnandoforce-pushed the contract-verifiable-build branch from d58a8a7 to fce6107CompareAugust 31, 2026 17:40
CopilotAI review requested due to automatic review settings August 31, 2026 17:40

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 stamped bldopt split 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/target via CARGO_TARGET_DIR, so using the host metadata target here breaks collection whenever the host has CARGO_TARGET_DIR or a configured target-dir. Plain builds then return a missing/stale path; for verifiable builds an absolute metadata path also causes join to discard the extracted root and can select an old host WASM instead of the newly built artifact. Collect from the forced target directory.
 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 at 0600.
 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 status result 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 --verifiable implies --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 --locked minimum 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.",
);
}

Comment threadcmd/soroban-cli/src/config/locator.rs
CopilotAI review requested due to automatic review settings August 31, 2026 18:33
@fnando
fnandoforce-pushed the contract-verifiable-build branch from fce6107 to 4988080CompareAugust 31, 2026 18:33

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 v does not make every recorded option valid shell syntax because parse_meta_arg allows 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 --locked instead 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.",
);

Comment threadcmd/soroban-cli/src/commands/contract/archive.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/archive.rs Outdated
Comment threadcmd/soroban-cli/src/config/locator.rs Outdated
@fnando
fnandoforce-pushed the contract-verifiable-build branch from 4988080 to f1ab06bCompareAugust 31, 2026 18:59
CopilotAI review requested due to automatic review settings August 31, 2026 18:59

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 bldopt as one value passed verbatim as an argv argument (“as if single-quoted”), not as shell source to evaluate. Escaping only v stores literal quote characters: an original --meta=note=added on build is 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())));

Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
@fnando
fnandoforce-pushed the contract-verifiable-build branch from f1ab06b to 42db3e5CompareSeptember 1, 2026 15:26
CopilotAI review requested due to automatic review settings September 1, 2026 15:26
@fnando
fnando requested review from a team and leighmcculloch and removed request for a teamSeptember 1, 2026 15:31

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 --porcelain hides 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 .env can 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 make source_sha256 machine-specific.
 .arg("status")
.arg("--porcelain")

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

Labels

None yet

Projects

Status: Backlog (Not Ready)

Development

Successfully merging this pull request may close these issues.

2 participants

@fnando
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add --verifiable flag to stellar contract build - #2709

Open
fnando wants to merge 1 commit into
mainfrom
contract-verifiable-build
Open

Add --verifiable flag to stellar contract build#2709
fnando wants to merge 1 commit into
mainfrom
contract-verifiable-build

Conversation

@fnando

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. It also adds a standalone stellar contract archive command. 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 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. Requires --image pinned by digest (<registry-host>/<repo>@sha256:<64-hex>); tag-only refs are rejected so the recorded bldimg names the exact bytes. --pull refreshes 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 --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).

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.

@fnando
fnando requested a review from a team as a code ownerAugust 31, 2026 17:17
CopilotAI balanced review requested due to automatic review settings August 31, 2026 17:17
@github-project-automationgithub-project-automationBot moved this to Backlog (Not Ready) in DevXAug 31, 2026
@socket-security

socket-securityBot commented Aug 31, 2026

Copy link
Copy Markdown

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

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Addedcargo/​tar@​0.4.4610010093100100
Updatedcargo/​ignore@​0.4.23 ⏵ 0.4.3398-110093100100

View full report

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds SEP-58 reproducible contract builds and source archive generation.

Changes:

  • Adds --verifiable build 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
FileDescription
FULL_HELP_DOCS.mdDocuments new commands and flags.
cmd/soroban-cli/src/config/locator.rsAdds recursive permission hardening.
cmd/soroban-cli/src/config/data.rsAdds managed archive storage.
cmd/soroban-cli/src/commands/mod.rsAdds verifiable help heading.
cmd/soroban-cli/src/commands/contract/mod.rsRegisters archive command.
cmd/soroban-cli/src/commands/contract/build/verifiable.rsImplements verifiable builds.
cmd/soroban-cli/src/commands/contract/build/source_archive.rsImplements reproducible archives.
cmd/soroban-cli/src/commands/contract/build/container.rsShares container and artifact logic.
cmd/soroban-cli/src/commands/contract/build.rsAdds flags and dispatch.
cmd/soroban-cli/src/commands/contract/archive.rsImplements archive CLI.
cmd/soroban-cli/src/commands/container/shared.rsAdds streamed image pulling.
cmd/soroban-cli/Cargo.tomlAdds archive dependencies.
cmd/crates/soroban-test/tests/it/build.rsAdds integration coverage.
Cargo.lockLocks dependency updates.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadcmd/soroban-cli/src/commands/contract/build/container.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/source_archive.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/build/container.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/build/source_archive.rs Outdated
@fnando
fnandoforce-pushed the contract-verifiable-build branch from d58a8a7 to fce6107CompareAugust 31, 2026 17:40
CopilotAI review requested due to automatic review settings August 31, 2026 17:40

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 stamped bldopt split 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/target via CARGO_TARGET_DIR, so using the host metadata target here breaks collection whenever the host has CARGO_TARGET_DIR or a configured target-dir. Plain builds then return a missing/stale path; for verifiable builds an absolute metadata path also causes join to discard the extracted root and can select an old host WASM instead of the newly built artifact. Collect from the forced target directory.
 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 at 0600.
 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 status result 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 --verifiable implies --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 --locked minimum 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.",
);
}

Comment threadcmd/soroban-cli/src/config/locator.rs
CopilotAI review requested due to automatic review settings August 31, 2026 18:33
@fnando
fnandoforce-pushed the contract-verifiable-build branch from fce6107 to 4988080CompareAugust 31, 2026 18:33

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 v does not make every recorded option valid shell syntax because parse_meta_arg allows 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 --locked instead 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.",
);

Comment threadcmd/soroban-cli/src/commands/contract/archive.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/archive.rs Outdated
Comment threadcmd/soroban-cli/src/config/locator.rs Outdated
@fnando
fnandoforce-pushed the contract-verifiable-build branch from 4988080 to f1ab06bCompareAugust 31, 2026 18:59
CopilotAI review requested due to automatic review settings August 31, 2026 18:59

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 bldopt as one value passed verbatim as an argv argument (“as if single-quoted”), not as shell source to evaluate. Escaping only v stores literal quote characters: an original --meta=note=added on build is 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())));

Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
@fnando
fnandoforce-pushed the contract-verifiable-build branch from f1ab06b to 42db3e5CompareSeptember 1, 2026 15:26
CopilotAI review requested due to automatic review settings September 1, 2026 15:26
@fnando
fnando requested review from a team and leighmcculloch and removed request for a teamSeptember 1, 2026 15:31

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 --porcelain hides 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 .env can 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 make source_sha256 machine-specific.
 .arg("status")
.arg("--porcelain")

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

Labels

None yet

Projects

Status: Backlog (Not Ready)

Development

Successfully merging this pull request may close these issues.

2 participants

@fnando
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add --verifiable flag to stellar contract build - #2709

Open
fnando wants to merge 1 commit into
mainfrom
contract-verifiable-build
Open

Add --verifiable flag to stellar contract build#2709
fnando wants to merge 1 commit into
mainfrom
contract-verifiable-build

Conversation

@fnando

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. It also adds a standalone stellar contract archive command. 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 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. Requires --image pinned by digest (<registry-host>/<repo>@sha256:<64-hex>); tag-only refs are rejected so the recorded bldimg names the exact bytes. --pull refreshes 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 --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).

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.

@fnando
fnando requested a review from a team as a code ownerAugust 31, 2026 17:17
CopilotAI balanced review requested due to automatic review settings August 31, 2026 17:17
@github-project-automationgithub-project-automationBot moved this to Backlog (Not Ready) in DevXAug 31, 2026
@socket-security

socket-securityBot commented Aug 31, 2026

Copy link
Copy Markdown

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

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Addedcargo/​tar@​0.4.4610010093100100
Updatedcargo/​ignore@​0.4.23 ⏵ 0.4.3398-110093100100

View full report

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds SEP-58 reproducible contract builds and source archive generation.

Changes:

  • Adds --verifiable build 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
FileDescription
FULL_HELP_DOCS.mdDocuments new commands and flags.
cmd/soroban-cli/src/config/locator.rsAdds recursive permission hardening.
cmd/soroban-cli/src/config/data.rsAdds managed archive storage.
cmd/soroban-cli/src/commands/mod.rsAdds verifiable help heading.
cmd/soroban-cli/src/commands/contract/mod.rsRegisters archive command.
cmd/soroban-cli/src/commands/contract/build/verifiable.rsImplements verifiable builds.
cmd/soroban-cli/src/commands/contract/build/source_archive.rsImplements reproducible archives.
cmd/soroban-cli/src/commands/contract/build/container.rsShares container and artifact logic.
cmd/soroban-cli/src/commands/contract/build.rsAdds flags and dispatch.
cmd/soroban-cli/src/commands/contract/archive.rsImplements archive CLI.
cmd/soroban-cli/src/commands/container/shared.rsAdds streamed image pulling.
cmd/soroban-cli/Cargo.tomlAdds archive dependencies.
cmd/crates/soroban-test/tests/it/build.rsAdds integration coverage.
Cargo.lockLocks dependency updates.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadcmd/soroban-cli/src/commands/contract/build/container.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/source_archive.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/build/container.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/build/source_archive.rs Outdated
@fnando
fnandoforce-pushed the contract-verifiable-build branch from d58a8a7 to fce6107CompareAugust 31, 2026 17:40
CopilotAI review requested due to automatic review settings August 31, 2026 17:40

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 stamped bldopt split 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/target via CARGO_TARGET_DIR, so using the host metadata target here breaks collection whenever the host has CARGO_TARGET_DIR or a configured target-dir. Plain builds then return a missing/stale path; for verifiable builds an absolute metadata path also causes join to discard the extracted root and can select an old host WASM instead of the newly built artifact. Collect from the forced target directory.
 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 at 0600.
 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 status result 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 --verifiable implies --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 --locked minimum 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.",
);
}

Comment threadcmd/soroban-cli/src/config/locator.rs
CopilotAI review requested due to automatic review settings August 31, 2026 18:33
@fnando
fnandoforce-pushed the contract-verifiable-build branch from fce6107 to 4988080CompareAugust 31, 2026 18:33

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 v does not make every recorded option valid shell syntax because parse_meta_arg allows 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 --locked instead 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.",
);

Comment threadcmd/soroban-cli/src/commands/contract/archive.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/archive.rs Outdated
Comment threadcmd/soroban-cli/src/config/locator.rs Outdated
@fnando
fnandoforce-pushed the contract-verifiable-build branch from 4988080 to f1ab06bCompareAugust 31, 2026 18:59
CopilotAI review requested due to automatic review settings August 31, 2026 18:59

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 bldopt as one value passed verbatim as an argv argument (“as if single-quoted”), not as shell source to evaluate. Escaping only v stores literal quote characters: an original --meta=note=added on build is 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())));

Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
@fnando
fnandoforce-pushed the contract-verifiable-build branch from f1ab06b to 42db3e5CompareSeptember 1, 2026 15:26
CopilotAI review requested due to automatic review settings September 1, 2026 15:26
@fnando
fnando requested review from a team and leighmcculloch and removed request for a teamSeptember 1, 2026 15:31

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 --porcelain hides 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 .env can 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 make source_sha256 machine-specific.
 .arg("status")
.arg("--porcelain")

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

Labels

None yet

Projects

Status: Backlog (Not Ready)

Development

Successfully merging this pull request may close these issues.

2 participants

@fnando
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add --verifiable flag to stellar contract build - #2709

Open
fnando wants to merge 1 commit into
mainfrom
contract-verifiable-build
Open

Add --verifiable flag to stellar contract build#2709
fnando wants to merge 1 commit into
mainfrom
contract-verifiable-build

Conversation

@fnando

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. It also adds a standalone stellar contract archive command. 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 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. Requires --image pinned by digest (<registry-host>/<repo>@sha256:<64-hex>); tag-only refs are rejected so the recorded bldimg names the exact bytes. --pull refreshes 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 --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).

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.

@fnando
fnando requested a review from a team as a code ownerAugust 31, 2026 17:17
CopilotAI balanced review requested due to automatic review settings August 31, 2026 17:17
@github-project-automationgithub-project-automationBot moved this to Backlog (Not Ready) in DevXAug 31, 2026
@socket-security

socket-securityBot commented Aug 31, 2026

Copy link
Copy Markdown

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

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Addedcargo/​tar@​0.4.4610010093100100
Updatedcargo/​ignore@​0.4.23 ⏵ 0.4.3398-110093100100

View full report

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds SEP-58 reproducible contract builds and source archive generation.

Changes:

  • Adds --verifiable build 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
FileDescription
FULL_HELP_DOCS.mdDocuments new commands and flags.
cmd/soroban-cli/src/config/locator.rsAdds recursive permission hardening.
cmd/soroban-cli/src/config/data.rsAdds managed archive storage.
cmd/soroban-cli/src/commands/mod.rsAdds verifiable help heading.
cmd/soroban-cli/src/commands/contract/mod.rsRegisters archive command.
cmd/soroban-cli/src/commands/contract/build/verifiable.rsImplements verifiable builds.
cmd/soroban-cli/src/commands/contract/build/source_archive.rsImplements reproducible archives.
cmd/soroban-cli/src/commands/contract/build/container.rsShares container and artifact logic.
cmd/soroban-cli/src/commands/contract/build.rsAdds flags and dispatch.
cmd/soroban-cli/src/commands/contract/archive.rsImplements archive CLI.
cmd/soroban-cli/src/commands/container/shared.rsAdds streamed image pulling.
cmd/soroban-cli/Cargo.tomlAdds archive dependencies.
cmd/crates/soroban-test/tests/it/build.rsAdds integration coverage.
Cargo.lockLocks dependency updates.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadcmd/soroban-cli/src/commands/contract/build/container.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/source_archive.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/build/container.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/build/source_archive.rs Outdated
@fnando
fnandoforce-pushed the contract-verifiable-build branch from d58a8a7 to fce6107CompareAugust 31, 2026 17:40
CopilotAI review requested due to automatic review settings August 31, 2026 17:40

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 stamped bldopt split 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/target via CARGO_TARGET_DIR, so using the host metadata target here breaks collection whenever the host has CARGO_TARGET_DIR or a configured target-dir. Plain builds then return a missing/stale path; for verifiable builds an absolute metadata path also causes join to discard the extracted root and can select an old host WASM instead of the newly built artifact. Collect from the forced target directory.
 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 at 0600.
 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 status result 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 --verifiable implies --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 --locked minimum 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.",
);
}

Comment threadcmd/soroban-cli/src/config/locator.rs
CopilotAI review requested due to automatic review settings August 31, 2026 18:33
@fnando
fnandoforce-pushed the contract-verifiable-build branch from fce6107 to 4988080CompareAugust 31, 2026 18:33

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 v does not make every recorded option valid shell syntax because parse_meta_arg allows 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 --locked instead 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.",
);

Comment threadcmd/soroban-cli/src/commands/contract/archive.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/archive.rs Outdated
Comment threadcmd/soroban-cli/src/config/locator.rs Outdated
@fnando
fnandoforce-pushed the contract-verifiable-build branch from 4988080 to f1ab06bCompareAugust 31, 2026 18:59
CopilotAI review requested due to automatic review settings August 31, 2026 18:59

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 bldopt as one value passed verbatim as an argv argument (“as if single-quoted”), not as shell source to evaluate. Escaping only v stores literal quote characters: an original --meta=note=added on build is 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())));

Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
@fnando
fnandoforce-pushed the contract-verifiable-build branch from f1ab06b to 42db3e5CompareSeptember 1, 2026 15:26
CopilotAI review requested due to automatic review settings September 1, 2026 15:26
@fnando
fnando requested review from a team and leighmcculloch and removed request for a teamSeptember 1, 2026 15:31

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 --porcelain hides 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 .env can 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 make source_sha256 machine-specific.
 .arg("status")
.arg("--porcelain")

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

Labels

None yet

Projects

Status: Backlog (Not Ready)

Development

Successfully merging this pull request may close these issues.

2 participants

@fnando
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add --verifiable flag to stellar contract build - #2709

Open
fnando wants to merge 1 commit into
mainfrom
contract-verifiable-build
Open

Add --verifiable flag to stellar contract build#2709
fnando wants to merge 1 commit into
mainfrom
contract-verifiable-build

Conversation

@fnando

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. It also adds a standalone stellar contract archive command. 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 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. Requires --image pinned by digest (<registry-host>/<repo>@sha256:<64-hex>); tag-only refs are rejected so the recorded bldimg names the exact bytes. --pull refreshes 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 --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).

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.

@fnando
fnando requested a review from a team as a code ownerAugust 31, 2026 17:17
CopilotAI balanced review requested due to automatic review settings August 31, 2026 17:17
@github-project-automationgithub-project-automationBot moved this to Backlog (Not Ready) in DevXAug 31, 2026
@socket-security

socket-securityBot commented Aug 31, 2026

Copy link
Copy Markdown

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

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Addedcargo/​tar@​0.4.4610010093100100
Updatedcargo/​ignore@​0.4.23 ⏵ 0.4.3398-110093100100

View full report

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds SEP-58 reproducible contract builds and source archive generation.

Changes:

  • Adds --verifiable build 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
FileDescription
FULL_HELP_DOCS.mdDocuments new commands and flags.
cmd/soroban-cli/src/config/locator.rsAdds recursive permission hardening.
cmd/soroban-cli/src/config/data.rsAdds managed archive storage.
cmd/soroban-cli/src/commands/mod.rsAdds verifiable help heading.
cmd/soroban-cli/src/commands/contract/mod.rsRegisters archive command.
cmd/soroban-cli/src/commands/contract/build/verifiable.rsImplements verifiable builds.
cmd/soroban-cli/src/commands/contract/build/source_archive.rsImplements reproducible archives.
cmd/soroban-cli/src/commands/contract/build/container.rsShares container and artifact logic.
cmd/soroban-cli/src/commands/contract/build.rsAdds flags and dispatch.
cmd/soroban-cli/src/commands/contract/archive.rsImplements archive CLI.
cmd/soroban-cli/src/commands/container/shared.rsAdds streamed image pulling.
cmd/soroban-cli/Cargo.tomlAdds archive dependencies.
cmd/crates/soroban-test/tests/it/build.rsAdds integration coverage.
Cargo.lockLocks dependency updates.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadcmd/soroban-cli/src/commands/contract/build/container.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/source_archive.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/build/container.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/build/source_archive.rs Outdated
@fnando
fnandoforce-pushed the contract-verifiable-build branch from d58a8a7 to fce6107CompareAugust 31, 2026 17:40
CopilotAI review requested due to automatic review settings August 31, 2026 17:40

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 stamped bldopt split 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/target via CARGO_TARGET_DIR, so using the host metadata target here breaks collection whenever the host has CARGO_TARGET_DIR or a configured target-dir. Plain builds then return a missing/stale path; for verifiable builds an absolute metadata path also causes join to discard the extracted root and can select an old host WASM instead of the newly built artifact. Collect from the forced target directory.
 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 at 0600.
 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 status result 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 --verifiable implies --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 --locked minimum 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.",
);
}

Comment threadcmd/soroban-cli/src/config/locator.rs
CopilotAI review requested due to automatic review settings August 31, 2026 18:33
@fnando
fnandoforce-pushed the contract-verifiable-build branch from fce6107 to 4988080CompareAugust 31, 2026 18:33

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 v does not make every recorded option valid shell syntax because parse_meta_arg allows 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 --locked instead 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.",
);

Comment threadcmd/soroban-cli/src/commands/contract/archive.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/archive.rs Outdated
Comment threadcmd/soroban-cli/src/config/locator.rs Outdated
@fnando
fnandoforce-pushed the contract-verifiable-build branch from 4988080 to f1ab06bCompareAugust 31, 2026 18:59
CopilotAI review requested due to automatic review settings August 31, 2026 18:59

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 bldopt as one value passed verbatim as an argv argument (“as if single-quoted”), not as shell source to evaluate. Escaping only v stores literal quote characters: an original --meta=note=added on build is 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())));

Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
@fnando
fnandoforce-pushed the contract-verifiable-build branch from f1ab06b to 42db3e5CompareSeptember 1, 2026 15:26
CopilotAI review requested due to automatic review settings September 1, 2026 15:26
@fnando
fnando requested review from a team and leighmcculloch and removed request for a teamSeptember 1, 2026 15:31

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 --porcelain hides 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 .env can 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 make source_sha256 machine-specific.
 .arg("status")
.arg("--porcelain")

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

Labels

None yet

Projects

Status: Backlog (Not Ready)

Development

Successfully merging this pull request may close these issues.

2 participants

@fnando
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add --verifiable flag to stellar contract build - #2709

Open
fnando wants to merge 1 commit into
mainfrom
contract-verifiable-build
Open

Add --verifiable flag to stellar contract build#2709
fnando wants to merge 1 commit into
mainfrom
contract-verifiable-build

Conversation

@fnando

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. It also adds a standalone stellar contract archive command. 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 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. Requires --image pinned by digest (<registry-host>/<repo>@sha256:<64-hex>); tag-only refs are rejected so the recorded bldimg names the exact bytes. --pull refreshes 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 --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).

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.

@fnando
fnando requested a review from a team as a code ownerAugust 31, 2026 17:17
CopilotAI balanced review requested due to automatic review settings August 31, 2026 17:17
@github-project-automationgithub-project-automationBot moved this to Backlog (Not Ready) in DevXAug 31, 2026
@socket-security

socket-securityBot commented Aug 31, 2026

Copy link
Copy Markdown

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

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Addedcargo/​tar@​0.4.4610010093100100
Updatedcargo/​ignore@​0.4.23 ⏵ 0.4.3398-110093100100

View full report

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds SEP-58 reproducible contract builds and source archive generation.

Changes:

  • Adds --verifiable build 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
FileDescription
FULL_HELP_DOCS.mdDocuments new commands and flags.
cmd/soroban-cli/src/config/locator.rsAdds recursive permission hardening.
cmd/soroban-cli/src/config/data.rsAdds managed archive storage.
cmd/soroban-cli/src/commands/mod.rsAdds verifiable help heading.
cmd/soroban-cli/src/commands/contract/mod.rsRegisters archive command.
cmd/soroban-cli/src/commands/contract/build/verifiable.rsImplements verifiable builds.
cmd/soroban-cli/src/commands/contract/build/source_archive.rsImplements reproducible archives.
cmd/soroban-cli/src/commands/contract/build/container.rsShares container and artifact logic.
cmd/soroban-cli/src/commands/contract/build.rsAdds flags and dispatch.
cmd/soroban-cli/src/commands/contract/archive.rsImplements archive CLI.
cmd/soroban-cli/src/commands/container/shared.rsAdds streamed image pulling.
cmd/soroban-cli/Cargo.tomlAdds archive dependencies.
cmd/crates/soroban-test/tests/it/build.rsAdds integration coverage.
Cargo.lockLocks dependency updates.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadcmd/soroban-cli/src/commands/contract/build/container.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/source_archive.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/build/container.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/build/source_archive.rs Outdated
@fnando
fnandoforce-pushed the contract-verifiable-build branch from d58a8a7 to fce6107CompareAugust 31, 2026 17:40
CopilotAI review requested due to automatic review settings August 31, 2026 17:40

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 stamped bldopt split 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/target via CARGO_TARGET_DIR, so using the host metadata target here breaks collection whenever the host has CARGO_TARGET_DIR or a configured target-dir. Plain builds then return a missing/stale path; for verifiable builds an absolute metadata path also causes join to discard the extracted root and can select an old host WASM instead of the newly built artifact. Collect from the forced target directory.
 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 at 0600.
 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 status result 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 --verifiable implies --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 --locked minimum 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.",
);
}

Comment threadcmd/soroban-cli/src/config/locator.rs
CopilotAI review requested due to automatic review settings August 31, 2026 18:33
@fnando
fnandoforce-pushed the contract-verifiable-build branch from fce6107 to 4988080CompareAugust 31, 2026 18:33

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 v does not make every recorded option valid shell syntax because parse_meta_arg allows 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 --locked instead 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.",
);

Comment threadcmd/soroban-cli/src/commands/contract/archive.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/archive.rs Outdated
Comment threadcmd/soroban-cli/src/config/locator.rs Outdated
@fnando
fnandoforce-pushed the contract-verifiable-build branch from 4988080 to f1ab06bCompareAugust 31, 2026 18:59
CopilotAI review requested due to automatic review settings August 31, 2026 18:59

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 bldopt as one value passed verbatim as an argv argument (“as if single-quoted”), not as shell source to evaluate. Escaping only v stores literal quote characters: an original --meta=note=added on build is 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())));

Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
@fnando
fnandoforce-pushed the contract-verifiable-build branch from f1ab06b to 42db3e5CompareSeptember 1, 2026 15:26
CopilotAI review requested due to automatic review settings September 1, 2026 15:26
@fnando
fnando requested review from a team and leighmcculloch and removed request for a teamSeptember 1, 2026 15:31

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 --porcelain hides 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 .env can 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 make source_sha256 machine-specific.
 .arg("status")
.arg("--porcelain")

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

Labels

None yet

Projects

Status: Backlog (Not Ready)

Development

Successfully merging this pull request may close these issues.

2 participants

@fnando
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add --verifiable flag to stellar contract build - #2709

Open
fnando wants to merge 1 commit into
mainfrom
contract-verifiable-build
Open

Add --verifiable flag to stellar contract build#2709
fnando wants to merge 1 commit into
mainfrom
contract-verifiable-build

Conversation

@fnando

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. It also adds a standalone stellar contract archive command. 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 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. Requires --image pinned by digest (<registry-host>/<repo>@sha256:<64-hex>); tag-only refs are rejected so the recorded bldimg names the exact bytes. --pull refreshes 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 --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).

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.

@fnando
fnando requested a review from a team as a code ownerAugust 31, 2026 17:17
CopilotAI balanced review requested due to automatic review settings August 31, 2026 17:17
@github-project-automationgithub-project-automationBot moved this to Backlog (Not Ready) in DevXAug 31, 2026
@socket-security

socket-securityBot commented Aug 31, 2026

Copy link
Copy Markdown

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

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Addedcargo/​tar@​0.4.4610010093100100
Updatedcargo/​ignore@​0.4.23 ⏵ 0.4.3398-110093100100

View full report

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds SEP-58 reproducible contract builds and source archive generation.

Changes:

  • Adds --verifiable build 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
FileDescription
FULL_HELP_DOCS.mdDocuments new commands and flags.
cmd/soroban-cli/src/config/locator.rsAdds recursive permission hardening.
cmd/soroban-cli/src/config/data.rsAdds managed archive storage.
cmd/soroban-cli/src/commands/mod.rsAdds verifiable help heading.
cmd/soroban-cli/src/commands/contract/mod.rsRegisters archive command.
cmd/soroban-cli/src/commands/contract/build/verifiable.rsImplements verifiable builds.
cmd/soroban-cli/src/commands/contract/build/source_archive.rsImplements reproducible archives.
cmd/soroban-cli/src/commands/contract/build/container.rsShares container and artifact logic.
cmd/soroban-cli/src/commands/contract/build.rsAdds flags and dispatch.
cmd/soroban-cli/src/commands/contract/archive.rsImplements archive CLI.
cmd/soroban-cli/src/commands/container/shared.rsAdds streamed image pulling.
cmd/soroban-cli/Cargo.tomlAdds archive dependencies.
cmd/crates/soroban-test/tests/it/build.rsAdds integration coverage.
Cargo.lockLocks dependency updates.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadcmd/soroban-cli/src/commands/contract/build/container.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/source_archive.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/build/container.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/build/source_archive.rs Outdated
@fnando
fnandoforce-pushed the contract-verifiable-build branch from d58a8a7 to fce6107CompareAugust 31, 2026 17:40
CopilotAI review requested due to automatic review settings August 31, 2026 17:40

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 stamped bldopt split 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/target via CARGO_TARGET_DIR, so using the host metadata target here breaks collection whenever the host has CARGO_TARGET_DIR or a configured target-dir. Plain builds then return a missing/stale path; for verifiable builds an absolute metadata path also causes join to discard the extracted root and can select an old host WASM instead of the newly built artifact. Collect from the forced target directory.
 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 at 0600.
 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 status result 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 --verifiable implies --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 --locked minimum 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.",
);
}

Comment threadcmd/soroban-cli/src/config/locator.rs
CopilotAI review requested due to automatic review settings August 31, 2026 18:33
@fnando
fnandoforce-pushed the contract-verifiable-build branch from fce6107 to 4988080CompareAugust 31, 2026 18:33

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 v does not make every recorded option valid shell syntax because parse_meta_arg allows 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 --locked instead 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.",
);

Comment threadcmd/soroban-cli/src/commands/contract/archive.rs Outdated
Comment threadcmd/soroban-cli/src/commands/contract/archive.rs Outdated
Comment threadcmd/soroban-cli/src/config/locator.rs Outdated
@fnando
fnandoforce-pushed the contract-verifiable-build branch from 4988080 to f1ab06bCompareAugust 31, 2026 18:59
CopilotAI review requested due to automatic review settings August 31, 2026 18:59

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 bldopt as one value passed verbatim as an argv argument (“as if single-quoted”), not as shell source to evaluate. Escaping only v stores literal quote characters: an original --meta=note=added on build is 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())));

Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment threadcmd/soroban-cli/src/commands/contract/build/verifiable.rs
@fnando
fnandoforce-pushed the contract-verifiable-build branch from f1ab06b to 42db3e5CompareSeptember 1, 2026 15:26
CopilotAI review requested due to automatic review settings September 1, 2026 15:26
@fnando
fnando requested review from a team and leighmcculloch and removed request for a teamSeptember 1, 2026 15:31

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 --porcelain hides 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 .env can 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 make source_sha256 machine-specific.
 .arg("status")
.arg("--porcelain")

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

Labels

None yet

Projects

Status: Backlog (Not Ready)

Development

Successfully merging this pull request may close these issues.

2 participants

@fnando