Skip to content

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

Open
fnando wants to merge 1 commit into
container-pull-streamingfrom
contract-verifiable-build
Open

fnando wants to merge 1 commit into
container-pull-streamingfrom
contract-verifiable-build

Conversation

@fnando

@fnando fnando commented Aug 31, 2026

Copy link
Copy Markdown
Member

Stacked on #2734 (container image-pull streaming). Review/merge that first; this PR is diffed against its branch and will be retargeted to main once it lands.

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. The container connection/resource flags are the existing container-build arguments, reused here.

A --verifiable build always generates the reproducible source archive (the same generator exposed as stellar contract build 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 is built with its own --package, forwarded to the build and recorded as a bldopt, so every WASM is independently reproducible; multi-contract workspaces build in a single container to share the crates download and target/. Every bldopt is recorded as valid shell syntax so a verifier can replay the exact invocation.

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, building on the reproducible source archive from stellar contract build archive.

@fnando
fnando requested a review from a team as a code owner August 31, 2026 17:17
Copilot AI balanced review requested due to automatic review settings August 31, 2026 17:17
@github-project-automation github-project-automation Bot moved this to Backlog (Not Ready) in DevX Aug 31, 2026

Copilot AI 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
File Description
FULL_HELP_DOCS.md Documents new commands and flags.
cmd/soroban-cli/src/config/locator.rs Adds recursive permission hardening.
cmd/soroban-cli/src/config/data.rs Adds managed archive storage.
cmd/soroban-cli/src/commands/mod.rs Adds verifiable help heading.
cmd/soroban-cli/src/commands/contract/mod.rs Registers archive command.
cmd/soroban-cli/src/commands/contract/build/verifiable.rs Implements verifiable builds.
cmd/soroban-cli/src/commands/contract/build/source_archive.rs Implements reproducible archives.
cmd/soroban-cli/src/commands/contract/build/container.rs Shares container and artifact logic.
cmd/soroban-cli/src/commands/contract/build.rs Adds flags and dispatch.
cmd/soroban-cli/src/commands/contract/archive.rs Implements archive CLI.
cmd/soroban-cli/src/commands/container/shared.rs Adds streamed image pulling.
cmd/soroban-cli/Cargo.toml Adds archive dependencies.
cmd/crates/soroban-test/tests/it/build.rs Adds integration coverage.
Cargo.lock Locks dependency updates.

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

Comment thread cmd/soroban-cli/src/commands/contract/build/container.rs Outdated
Comment thread cmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment thread cmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment thread cmd/soroban-cli/src/commands/contract/build/verifiable.rs Outdated
Comment thread cmd/soroban-cli/src/commands/contract/build/source_archive.rs Outdated
Comment thread cmd/soroban-cli/src/commands/contract/build/container.rs
Comment thread cmd/soroban-cli/src/commands/contract/build/verifiable.rs Outdated
Comment thread cmd/soroban-cli/src/commands/contract/build/source_archive.rs Outdated
@fnando
fnando force-pushed the contract-verifiable-build branch from d58a8a7 to fce6107 Compare August 31, 2026 17:40
Copilot AI review requested due to automatic review settings August 31, 2026 17:40

Copilot AI 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 thread cmd/soroban-cli/src/config/locator.rs
Copilot AI review requested due to automatic review settings August 31, 2026 18:33
@fnando
fnando force-pushed the contract-verifiable-build branch from fce6107 to 4988080 Compare August 31, 2026 18:33

Copilot AI 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 thread cmd/soroban-cli/src/commands/contract/archive.rs Outdated
Comment thread cmd/soroban-cli/src/commands/contract/archive.rs Outdated
Comment thread cmd/soroban-cli/src/config/locator.rs Outdated
@fnando
fnando force-pushed the contract-verifiable-build branch from 4988080 to f1ab06b Compare August 31, 2026 18:59
Copilot AI review requested due to automatic review settings August 31, 2026 18:59

Copilot AI 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 thread cmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment thread cmd/soroban-cli/src/commands/contract/build/verifiable.rs
Comment thread cmd/soroban-cli/src/commands/contract/build/source_archive.rs Outdated
@fnando
fnando force-pushed the contract-verifiable-build branch from f1ab06b to 42db3e5 Compare September 1, 2026 15:26
Copilot AI 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 team September 1, 2026 15:31

Copilot AI 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")

Comment thread cmd/soroban-cli/src/commands/contract/build/source_archive.rs Outdated
@fnando fnando moved this from Backlog (Not Ready) to Needs Review in DevX Sep 4, 2026
@fnando fnando self-assigned this Sep 4, 2026
Copilot AI review requested due to automatic review settings September 17, 2026 18:54
@fnando
fnando force-pushed the contract-verifiable-build branch from 42db3e5 to 434b938 Compare September 17, 2026 18:54
@fnando
fnando changed the base branch from main to contract-archive September 17, 2026 18:54
@fnando
fnando added this pull request to stack #2732 September 17, 2026 18:56

Copilot AI 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.

🟡 Changes recommended

Recorded build options and failure reproduction commands currently cannot reliably reproduce the original build.

Get a fresh assessment by requesting another Copilot review.

Review details
  • Files reviewed: 9/9 changed files
  • Comments generated: 2
  • Review effort level: Balanced

if let Some(v) = value {
args.push(format!("{key}={v}"));
if record_bldopts {
bldopts.push(format!("{key}={}", shell_escape::escape(v.into())));
Comment on lines +195 to +199
container::run_in_container(
&image_ref,
&resolved.mount_root,
&container_cmds,
&env,
Base automatically changed from contract-archive to main September 18, 2026 18:22
fnando added a commit that referenced this pull request Sep 18, 2026
### What

Adds a `stellar contract build archive` subcommand that generates — or
inspects — a byte-reproducible source archive of the working tree. Split
out of #2709 so it can be reviewed on its own; it's the shared
foundation the `--verifiable` build is stacked on.

The archive walks the working directory honoring the project's own
`.gitignore`/`.ignore` files (`.git` is always skipped), so the same
tree hashes to the same `source_sha256`. It refuses a working tree it
can't verify against committed source (dirty, or files marked
assume-unchanged/skip-worktree), is written `0600`, and the `--dry-run`
listing is sanitized against hostile filenames.

### Why

SEP-58 verifies that a deployed WASM came from a specific source; this
archive is the exact artifact its `source_sha256` refers to.

### Known limitations

N/A
Copilot AI review requested due to automatic review settings September 18, 2026 18:34
@fnando
fnando force-pushed the contract-verifiable-build branch from 434b938 to 23d357e Compare September 18, 2026 18:34

Copilot AI 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.

🟡 Changes recommended

Workspace-member builds fail and shell-escaped bldopt metadata cannot be replayed verbatim as SEP-58 requires.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (1)

cmd/soroban-cli/src/commands/contract/build/container.rs:304

  • SEP-58 replays each bldopt verbatim as one argv argument, not as shell source. Escaping v stores literal quote characters (for example --features='a b'), so a verifier passes a different value and cannot reproduce the build. Record the raw {key}={v} argument and update the shell-oriented test accordingly.
                bldopts.push(format!("{key}={}", shell_escape::escape(v.into())));
  • Files reviewed: 9/9 changed files
  • Comments generated: 3
  • Review effort level: Balanced

// The source root is the current working directory: it's archived,
// bind-mounted into the container, and the `--manifest-path` bldopt is
// relativized against it. Run from the project/workspace root you want built.
let source_root = source_archive::resolve_source_root();
Comment on lines +157 to +162
#[arg(
long,
requires = "verifiable",
requires = "source_sha256",
help_heading = HEADING_VERIFIABLE
)]
Comment on lines +139 to +140
/// party can reproduce the exact bytes. Implies `--locked`. Requires a clean
/// git tree. Requires `--image` pinned by digest
Copilot AI review requested due to automatic review settings September 18, 2026 18:44
@fnando
fnando force-pushed the contract-verifiable-build branch from 23d357e to f6b8201 Compare September 18, 2026 18:44
@fnando
fnando force-pushed the contract-verifiable-build branch from f6b8201 to b87725e Compare September 18, 2026 18:52

Copilot AI 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.

🟡 Changes recommended

Recorded build options currently violate SEP-58 replay semantics, and valid source-URI usage is rejected.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (2)

cmd/soroban-cli/src/commands/contract/build/container.rs:304

  • SEP-58 defines each bldopt as one argument replayed verbatim, not as shell source. Escaping only the value inserts literal quotes into metadata—for example, --features=a b becomes --features='a b', so a conforming verifier passes a different feature value and cannot reproduce the build. Record the original argv string unchanged.
                bldopts.push(format!("{key}={}", shell_escape::escape(v.into())));

cmd/soroban-cli/src/commands/contract/build.rs:160

  • This makes --source-uri unusable unless the caller also supplies the optional hash pin, even though verifiable mode always computes and records source_sha256 itself. It unnecessarily forces users to pre-generate the archive before they can provide its URI and contradicts the documented standalone --source-uri option; require only --verifiable.
        requires = "source_sha256",
  • Files reviewed: 9/9 changed files
  • Comments generated: 1
  • Review effort level: Balanced

print.infoln(format!("Using Rust toolchain {}", probe.toolchain));
env.push(format!("RUSTUP_TOOLCHAIN={}", probe.toolchain));

container::run_in_container(
Copilot AI review requested due to automatic review settings September 18, 2026 18:54
@fnando
fnando removed this pull request from stack #2732 September 18, 2026 18:58
@fnando
fnando force-pushed the contract-verifiable-build branch from b87725e to f6b8201 Compare September 18, 2026 19:00
@fnando
fnando changed the base branch from main to container-pull-streaming September 18, 2026 19:00
@fnando
fnando added this pull request to stack #2735 September 18, 2026 19:00

Copilot AI 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.

🟡 Changes recommended

Verbatim metadata replay, workspace-member builds, Git enforcement, and source URI handling currently have blocking correctness issues.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (4)

cmd/soroban-cli/src/commands/contract/build/container.rs:304

  • SEP-58 defines each bldopt as one verbatim argv argument, not a shell fragment. Escaping the value changes the recorded argument—for example, the original --meta=note=added on build is recorded as --meta=note='added on build', so a verifier that correctly passes it verbatim includes literal quote characters and cannot reproduce the WASM. Record the forwarded argument unchanged.
                bldopts.push(format!("{key}={}", shell_escape::escape(v.into())));

cmd/soroban-cli/src/commands/contract/build.rs:162

  • source_sha256 is always computed and stamped by verifiable mode, so requiring the user to also pass the optional hash pin prevents the valid --verifiable --source-uri ... workflow. This contradicts the flag documentation and SEP-58's optional-URI model; remove this extra requirement.
    #[arg(
        long,
        requires = "verifiable",
        requires = "source_sha256",
        help_heading = HEADING_VERIFIABLE
    )]

cmd/soroban-cli/src/commands/contract/build/verifiable.rs:94

  • Using the current directory as the archive/mount root breaks the existing supported workflow of running contract build from a workspace member. Host metadata still discovers the workspace, but the extracted source omits the root Cargo.lock and sibling members; because verifiable mode forwards --locked, the member-only copy cannot build. Resolve metadata before archiving and use its workspace_root as the source root, while preserving the selected member via --package/--manifest-path.
    // The source root is the current working directory: it's archived,
    // bind-mounted into the container, and the `--manifest-path` bldopt is
    // relativized against it. Run from the project/workspace root you want built.
    let source_root = source_archive::resolve_source_root();

cmd/soroban-cli/src/commands/contract/build/verifiable.rs:199

  • No integration test exercises a successful verifiable build: every added case exits during validation or fails at the container engine, so archive mounting, probing, metadata stamping, multi-package invocation, and artifact copy-back remain untested. Add an end-to-end test using the repository's fake container-executable pattern (cmd/crates/soroban-test/tests/it/container.rs:1-7) that completes the build path and inspects the resulting invocation/WASM.
    container::run_in_container(
        &image_ref,
        &resolved.mount_root,
        &container_cmds,
        &env,
  • Files reviewed: 8/8 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment on lines +96 to +99
// The archive is the working tree, so refuse a dirty repo: a verifiable build
// should be deliberate, off a committed state, not whatever happens to be on
// disk. Skipped when the source root isn't a git repo.
source_archive::ensure_clean_tree(&source_root, None).map_err(Error::from)?;
Copilot AI review requested due to automatic review settings September 18, 2026 19:01

Copilot AI 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.

🟡 Changes recommended

Workspace-root handling and shell-escaped bldopt values currently break valid builds and SEP-58 replay.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (3)

cmd/soroban-cli/src/commands/contract/build/verifiable.rs:94

  • contract build is documented to work from a workspace-member subdirectory, but this archives that member directory while cargo metadata below still resolves the enclosing workspace. From contracts/add, /source therefore lacks the workspace Cargo.toml, Cargo.lock, and sibling packages even though workspace packages are inferred and forwarded, so the container build fails or cannot honor --locked. Resolve metadata before archiving and use its workspace_root, or reject non-root invocations before creating the archive.
    let source_root = source_archive::resolve_source_root();

cmd/soroban-cli/src/commands/contract/build/container.rs:304

  • SEP-58 defines each bldopt value as one argument replayed verbatim (as if the whole value were single-quoted), so embedding shell quoting changes the argument. For example, a build receives one --features=foo bar argument, but this records --features='foo bar'; a verifier replaying that value as argv passes literal quote characters and gets different or invalid features. Record the raw value instead.
                bldopts.push(format!("{key}={}", shell_escape::escape(v.into())));

cmd/soroban-cli/src/commands/contract/build.rs:160

  • source_sha256 is always computed from the archive and stamped into SourceIds, so requiring the user to also provide the optional hash pin prevents the documented --source-uri-only workflow for no functional reason. --verifiable --source-uri ... is rejected by clap even though the computed hash already satisfies SEP-58; remove this second requirement.
        requires = "source_sha256",
  • Files reviewed: 8/8 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment on lines +137 to +139
let supports_locked = at_least(container::LOCKED_MIN);
let supports_optimize_flag = at_least(container::OPTIMIZE_FLAG_MIN);
let supports_optimize_false = at_least(container::OPTIMIZE_NEW_SYNTAX_MIN);
// source_sha256) before the docker stage, so the "Wrote source archive" line
// appears even though the build then fails to reach a real image.
#[test]
fn verifiable_always_writes_source_archive() {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Needs Review

Development

Successfully merging this pull request may close these issues.

2 participants