Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e182c7e
Scaffold stellar contract verify with metadata extraction.
fnando May 21, 2026
2df7217
Add trust gates to stellar contract verify.
fnando May 22, 2026
8ad2ff8
Materialize source for stellar contract verify.
fnando May 22, 2026
5164cf2
Rebuild and byte-compare in stellar contract verify.
fnando May 22, 2026
63d0d33
Use direct --docker-host field on contract verify.
fnando May 22, 2026
cb21dc9
Use question and warn emojis on trust prompt.
fnando May 22, 2026
ee11961
Respect --verbose and --quiet on contract verify.
fnando May 22, 2026
c0fd99e
Force trust prompts visible even under --quiet.
fnando May 22, 2026
525724a
Capitalize Verified result line.
fnando May 22, 2026
519f4aa
Capitalize info, warn, and check messages on contract verify.
fnando May 22, 2026
4894a02
Expand github:user/repo source_repo before git clone.
fnando May 22, 2026
3f012ee
Validate retrieval channel before trust prompts.
fnando May 22, 2026
f8e3960
Anchor verify rebuilt-wasm search at manifest-path parent.
fnando May 22, 2026
df0ad61
Add stellar contract verify integration tests.
fnando May 22, 2026
872e1c4
Add tarball-sha256 + local --tarball-url verify test.
fnando May 22, 2026
1d3902e
Move verify integration tests into integration tier.
fnando May 22, 2026
4e21de4
Restrict permissions on materialized verify source.
fnando May 22, 2026
de2fc15
Share build logic and fix contract verify rebuild.
fnando Jun 17, 2026
eb6ddd0
Add --wasm-hash to stellar contract verify.
fnando Jun 17, 2026
5ffd7ac
Stop requiring source hash when setting source URI.
fnando Jun 19, 2026
54291d5
Skip --locked when verifying against older images.
fnando Jul 8, 2026
1c8d8f7
Find rebuilt contract in workspace target folders.
fnando Jul 8, 2026
8a11c08
Strip shell quotes from build options when rebuilding.
fnando Jul 8, 2026
557918a
Add --keep to save build files for debugging.
fnando Jul 8, 2026
4d82b0a
Show the source file actually used when overriding.
fnando Jul 8, 2026
a65557a
Reject unknown source formats.
fnando Jul 9, 2026
0c23e77
Support zip files as verifiable build sources.
fnando Jul 9, 2026
d702bc0
Reject duplicate build metadata when verifying.
fnando Jul 9, 2026
d72a039
Replay recorded metadata in order when verifying.
fnando Jul 9, 2026
e71431f
Skip source metadata when verifying a contract.
fnando Jul 9, 2026
da0b601
Fall back to the last metadata block when verifying.
fnando Jul 9, 2026
7027bd9
Run contract verify through the docker CLI.
fnando Jul 16, 2026
3f94258
Support other container engines in contract verify.
fnando Jul 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ escape-bytes = "0.1.1"
hex = "0.4.3"
itertools = "0.10.0"
async-trait = "0.1.76"
tar = "0.4.46"
flate2 = "1.0.30"
zip = { version = "8.6.0", default-features = false, features = ["deflate"] }
serde-aux = "4.1.2"
serde_json = "1.0.82"
serde = "1.0.82"
Expand Down
41 changes: 40 additions & 1 deletion FULL_HELP_DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ Tools for smart contract developers
- `optimize` — ⚠️ Deprecated, use `build --optimize`. Optimize a WASM file
- `read` — Print the current value of a contract-data ledger entry
- `restore` — Restore an evicted value for a contract-data legder entry
- `verify` — Verify that a contract's WASM reproduces from the build metadata it records, per SEP-58. Either pass a contract id/alias via `--id` (the WASM is fetched from the network) or a local file via `--wasm`

## `stellar contract asset`

Expand Down Expand Up @@ -424,7 +425,7 @@ To view the commands that will be executed, without executing them, use the --pr
- `--verifiable` — Build inside a trusted Docker container and record SEP-58 metadata (`bldimg`, `source_uri`, `source_sha256`, `bldopt`) so the resulting WASM can be reproduced and verified by third parties. Implies `--locked`. Requires a clean git working tree
- `--image <IMAGE>` — Override the auto-selected container image used by `--verifiable`. Must be digest-pinned, e.g. `docker.io/stellar/stellar-cli@sha256:...`. Tag-only refs are rejected because SEP-58 requires content addressing
- `--source-sha256 <SOURCE_SHA256>` — SEP-58 source identification: SHA-256 of the source archive (recorded as the `source_sha256` meta entry). Optional with `--verifiable`: the archive is always generated and its SHA-256 computed for you. When supplied it's treated as a pin — the build fails if it doesn't match the generated archive
- `--source-uri <SOURCE_URI>` — SEP-58 source identification: URI where the source can be obtained, e.g. `https://example.com/src.tar.gz` (recorded as the `source_uri` meta entry). Optional; when set it must accompany `--source-sha256`
- `--source-uri <SOURCE_URI>` — entry). Optional with `--verifiable`; the recorded `source_sha256` is computed from the generated archive, unless `--source-sha256` is explicitly set

## `stellar contract extend`

Expand Down Expand Up @@ -1162,6 +1163,44 @@ If no keys are specificed the contract itself is restored.
- `--inclusion-fee <INCLUSION_FEE>` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided
- `--build-only` — Build the transaction and only write the base64 xdr to stdout

## `stellar contract verify`

Verify that a contract's WASM reproduces from the build metadata it records, per SEP-58. Either pass a contract id/alias via `--id` (the WASM is fetched from the network) or a local file via `--wasm`

**Usage:** `stellar contract verify [OPTIONS]`

###### **Container Options:**

- `-d`, `--docker-host <DOCKER_HOST>` — Optional argument to override the default docker host. This is useful when you are using a non-standard docker host path for your Docker-compatible container runtime, e.g. Docker Desktop defaults to $HOME/.docker/run/docker.sock instead of /var/run/docker.sock
- `--engine <ENGINE>` — Container engine to use [default: docker]

Possible values:
- `docker`: Docker, or any Docker-compatible CLI
- `apple-container`: Apple's `container` CLI (macOS 26+, Apple silicon)

- `--cpus <CPUS>` — Limit the number of CPUs available to the container, e.g. `2`. A whole number: Apple's `container` engine does not accept fractional CPUs
- `--memory <MEMORY>` — Limit the memory available to the container, e.g. `2g` or `512m`

###### **Global Options:**

- `--config-dir <CONFIG_DIR>` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings

###### **Options:**

- `--id <CONTRACT_ID>` — Contract id or alias to fetch the WASM from the network
- `--wasm <WASM>` — Local WASM file to verify, instead of fetching from the network
- `--wasm-hash <WASM_HASH>` — WASM hash (hex) to fetch the WASM from the network
- `--source-uri <SOURCE_URI>` — Local source code file or http(s) URL to use as the source when the WASM's recorded SEP-58 metadata has only `source_sha256` (no `source_uri`). Accepts http(s) URLs or local file paths
- `--trust` — Bypass interactive confirmation when the WASM's bldimg is not in the default trust list, or when the source is provided as an archive (source archives are never default-trusted)
- `--keep` — Keep the materialized source and rebuild output instead of deleting them on exit, and print the path. Useful for debugging a byte mismatch (e.g. diffing the rebuilt WASM's metadata against the original)

###### **RPC Options:**

- `--rpc-url <RPC_URL>` — RPC server endpoint
- `--rpc-header <RPC_HEADERS>` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times
- `--network-passphrase <NETWORK_PASSPHRASE>` — Network passphrase to sign the transaction sent to the rpc server
- `-n`, `--network <NETWORK>` — Name of network to use from config

## `stellar doctor`

Diagnose and troubleshoot CLI and network issues
Expand Down
30 changes: 28 additions & 2 deletions cmd/crates/soroban-test/tests/it/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1319,15 +1319,41 @@ fn verifiable_source_uri_format_errors() {
.arg("--verifiable")
.arg("--image")
.arg(ZERO_DIGEST)
.arg("--source-sha256")
.arg("a".repeat(64))
.arg("--source-uri")
.arg("not a uri")
.assert()
.failure()
.stderr(predicate::str::contains("source_uri format"));
}

// `--source-uri` does not require `--source-sha256`: the archive is generated
// and its source_sha256 computed regardless, so a source-uri-only build gets
// past validation and writes the archive (then fails reaching a real image).
#[test]
fn verifiable_source_uri_without_sha256_is_allowed() {
let sandbox = TestEnv::default();
let (_temp, workspace) = fresh_workspace();
git_in(&workspace, &["init", "-q", "-b", "main"]);
git_in(&workspace, &["add", "-A"]);
git_in(&workspace, &["commit", "-q", "-m", "init"]);

sandbox
.new_assert_cmd("contract")
.current_dir(workspace.join("contracts").join("add"))
.arg("build")
.arg("--verifiable")
.arg("--image")
.arg(ZERO_DIGEST)
.arg("--source-uri")
.arg("https://example.com/src.tar.gz")
.assert()
.failure()
.stderr(
predicate::str::contains("Wrote source archive")
.and(predicate::str::contains("source_sha256")),
);
}

// A dirty git tree is a hard fail under `--verifiable` (the recorded
// source_sha256 would not describe the bytes built).
#[test]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
mod fetch;
mod info_hash;
mod verify;
167 changes: 167 additions & 0 deletions cmd/crates/soroban-test/tests/it/integration/contract/verify.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
//! End-to-end tests for `stellar contract verify`.
//!
//! Pipeline, entirely through the cli (no git/network clone needed):
//! 1. `contract init` scaffolds a workspace + `hello-world` contract.
//! 2. `contract build --verifiable` builds it against a pinned bldimg and
//! records `source_sha256` in the wasm's SEP-58 metadata.
//! 3. `contract archive` regenerates the *same* source tarball (same
//! `build_source_archive` the verifiable build used), so its sha256 matches
//! the recorded `source_sha256`.
//! 4. `contract verify --source-uri <that archive>` materializes the source,
//! rebuilds in the bldimg, and byte-compares.
//!
//! The happy-path tests require docker + the pinned bldimg pullable from Docker
//! Hub. They are always-run by convention (per the project's "no #[ignore]"
//! rule) — failures there flag a regression or pinned-resource drift loudly.
//!
//! Fixture pin:
//! - bldimg: `docker.io/fnando/stellar-cli-experimental@sha256:85e76e…`.
//! TODO: swap to `docker.io/stellar/stellar-cli@sha256:<…>` once
//! `stellar/stellar-cli-docker` publishes a canonical tag matching the
//! cli version under test.

use predicates::prelude::{predicate, PredicateBooleanExt};
use soroban_test::TestEnv;
use std::path::{Path, PathBuf};

const PINNED_BLDIMG: &str =
"docker.io/fnando/stellar-cli-experimental@sha256:85e76eae8bf9f47ba94391214b76f8fa2b9d7b28171774dfafaf5b8d613a74d3";

/// Scaffold a workspace with the default `hello-world` contract under
/// `<sandbox>/proj`. The scaffolded tree is not a git repo, so the verifiable
/// build archives the working directory directly.
fn init_project(sandbox: &TestEnv) -> PathBuf {
let proj = sandbox.dir().join("proj");
sandbox
.new_assert_cmd("contract")
.arg("init")
.arg(&proj)
.assert()
.success();
proj
}

/// Build the scaffolded contract verifiably and generate the matching source
/// archive. Returns `(wasm_path, archive_path)`.
///
/// The archive is produced *after* the verifiable build on purpose: the build's
/// host-side `cargo metadata` writes `Cargo.lock` into the workspace, and
/// `contract archive` then captures that same tree — so the archive's sha256
/// equals the `source_sha256` the build recorded into the wasm.
fn build_and_archive(sandbox: &TestEnv, proj: &Path) -> (PathBuf, PathBuf) {
let out_dir = sandbox.dir().join("out");
std::fs::create_dir_all(&out_dir).unwrap();
sandbox
.new_assert_cmd("contract")
.arg("build")
.arg("--verifiable")
.arg("--image")
.arg(PINNED_BLDIMG)
.arg("--out-dir")
.arg(&out_dir)
.current_dir(proj)
.assert()
.success();

let archive = sandbox.dir().join("source.tar.gz");
sandbox
.new_assert_cmd("contract")
.arg("archive")
.arg("--out-file")
.arg(&archive)
.current_dir(proj)
.assert()
.success();

(out_dir.join("hello_world.wasm"), archive)
}

/// Happy path: build a verifiable wasm, then verify it from the local file,
/// handing the cli the matching source archive via `--source-uri`. Asserts the
/// cli prints `Verified:` on stderr.
#[test]
fn verify_wasm_succeeds_for_freshly_built_verifiable_wasm() {
let sandbox = TestEnv::default();
let proj = init_project(&sandbox);
let (wasm, archive) = build_and_archive(&sandbox, &proj);

sandbox
.new_assert_cmd("contract")
.arg("verify")
.arg("--wasm")
.arg(&wasm)
.arg("--source-uri")
.arg(&archive)
.arg("--trust")
.assert()
.success()
.stderr(predicate::str::contains("Verified:"));
}

/// Build verifiable → upload to local network → verify by `--id`. Exercises the
/// `wasm::fetch_from_contract` path through the verify command.
#[tokio::test]
async fn verify_id_succeeds_after_upload() {
let sandbox = TestEnv::new();
let proj = init_project(&sandbox);
let (wasm, archive) = build_and_archive(&sandbox, &proj);
let wasm_str = wasm.to_string_lossy().to_string();

// Deploy gives us a contract id `--id` can resolve to the on-ledger wasm.
let id = sandbox
.new_assert_cmd("contract")
.arg("deploy")
.arg("--wasm")
.arg(&wasm_str)
.arg("--alias")
.arg("verify_e2e")
.arg("--ignore-checks")
.assert()
.success()
.stdout(predicate::str::is_empty().not())
.get_output()
.stdout
.clone();
let id = String::from_utf8(id).unwrap().trim().to_string();

sandbox
.new_assert_cmd("contract")
.arg("verify")
.arg("--id")
.arg(&id)
.arg("--source-uri")
.arg(&archive)
.arg("--trust")
.assert()
.success()
.stderr(predicate::str::contains("Verified:"));
}

/// Flip a byte in a verifiable wasm and confirm `contract verify` reports the
/// mismatch. The flipped byte is in the middle (code) so the trailing
/// `contractmetav0` section still parses; the rebuild reproduces the original
/// bytes, and the byte comparison fails.
#[test]
fn verify_wasm_fails_on_tampered_bytes() {
let sandbox = TestEnv::default();
let proj = init_project(&sandbox);
let (wasm, archive) = build_and_archive(&sandbox, &proj);

let mut bytes = std::fs::read(&wasm).unwrap();
let mid = bytes.len() / 2;
bytes[mid] = bytes[mid].wrapping_add(1);
let tampered = sandbox.dir().join("tampered.wasm");
std::fs::write(&tampered, &bytes).unwrap();

sandbox
.new_assert_cmd("contract")
.arg("verify")
.arg("--wasm")
.arg(&tampered)
.arg("--source-uri")
.arg(&archive)
.arg("--trust")
.assert()
.failure()
.stderr(predicate::str::contains("verification failed"));
}
Loading
Loading