Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions FULL_HELP_DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,16 @@ To view the commands that will be executed, without executing them, use the --pr

- `--print-commands-only` — Print commands to build without executing them

###### **Verifiable:**

- `--verifiable` — Build inside a trusted Docker container and record SEP-58 metadata (`bldimg`, `source_rev`, `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-repo <SOURCE_REPO>` — SEP-58 source identification: HTTPS URL (or `github:user/repo`) of the source repository. Must be passed together with `--source-rev`
- `--source-rev <SOURCE_REV>` — SEP-58 source identification: 40-char SHA-1 of the source commit. The local workspace must be a git repo at this exact SHA with a clean working tree. Must be passed together with `--source-repo`
- `--tarball-url <TARBALL_URL>` — SEP-58 source identification: URL where the source tarball can be downloaded
- `--tarball-sha256 <TARBALL_SHA256>` — SEP-58 source identification: SHA-256 of the source tarball bytes
- `-d`, `--docker-host <DOCKER_HOST>` — Override the default docker host used by `--verifiable`

## `stellar contract extend`

Extend the time to live ledger of a contract-data ledger entry.
Expand Down
202 changes: 202 additions & 0 deletions cmd/crates/soroban-test/tests/it/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -988,3 +988,205 @@ fn build_always_injects_cli_version() {
"CLI version should not be empty"
);
}

const ZERO_DIGEST: &str =
"docker.io/stellar/stellar-cli@sha256:0000000000000000000000000000000000000000000000000000000000000000";

// Convenience: drive a git command in a fixture directory.
fn git_in(dir: &Path, args: &[&str]) {
std::process::Command::new("git")
.args(args)
.current_dir(dir)
.env("GIT_AUTHOR_NAME", "Test")
.env("GIT_AUTHOR_EMAIL", "test@example.com")
.env("GIT_COMMITTER_NAME", "Test")
.env("GIT_COMMITTER_EMAIL", "test@example.com")
.status()
.unwrap();
}

// Init a tempdir copy of the workspace fixture and return the workspace path.
fn fresh_workspace() -> (TempDir, PathBuf) {
let cargo_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let fixture_path = cargo_dir.join("tests/fixtures/workspace");
let temp = TempDir::new().unwrap();
fs_extra::dir::copy(&fixture_path, temp.path(), &CopyOptions::new()).unwrap();
let workspace = temp.path().join("workspace");
(temp, workspace)
}

fn git_head(dir: &Path) -> String {
let out = std::process::Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(dir)
.output()
.unwrap();
String::from_utf8(out.stdout).unwrap().trim().to_string()
}

// `--verifiable` cannot accept reserved `--meta` keys that the cli writes itself.
#[test]
fn verifiable_meta_conflict_errors() {
let sandbox = TestEnv::default();
let cargo_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let fixture_path = cargo_dir.join("tests/fixtures/workspace/contracts/add");

sandbox
.new_assert_cmd("contract")
.current_dir(fixture_path)
.arg("build")
.arg("--verifiable")
.arg("--image")
.arg(ZERO_DIGEST)
.arg("--tarball-url")
.arg("https://example.com/foo.tar.gz")
.arg("--meta")
.arg("bldimg=not-allowed")
.assert()
.failure()
.stderr(predicate::str::contains("reserved key: bldimg"));
}

// `--image` is validated against the SEP-58 bldimg regex; tag-only refs fail.
#[test]
fn verifiable_image_must_be_digest_pinned() {
let sandbox = TestEnv::default();
let cargo_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let fixture_path = cargo_dir.join("tests/fixtures/workspace/contracts/add");

sandbox
.new_assert_cmd("contract")
.current_dir(fixture_path)
.arg("build")
.arg("--verifiable")
.arg("--image")
.arg("docker.io/stellar/stellar-cli:latest")
.arg("--tarball-url")
.arg("https://example.com/foo.tar.gz")
.assert()
.failure()
.stderr(predicate::str::contains("bldimg format"));
}

// SEP-58 bldimg requires an explicit registry host (e.g. `docker.io/...`).
// Implicit Docker-Hub-style short refs are rejected.
#[test]
fn verifiable_image_requires_explicit_registry_host() {
let sandbox = TestEnv::default();
let cargo_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let fixture_path = cargo_dir.join("tests/fixtures/workspace/contracts/add");

let short_ref = format!("stellar/stellar-cli@sha256:{}", "0".repeat(64));

sandbox
.new_assert_cmd("contract")
.current_dir(fixture_path)
.arg("build")
.arg("--verifiable")
.arg("--image")
.arg(short_ref)
.arg("--tarball-url")
.arg("https://example.com/foo.tar.gz")
.assert()
.failure()
.stderr(predicate::str::contains("bldimg format"));
}

// `--verifiable` without any source-identification flag must error.
#[test]
fn verifiable_requires_source_id() {
let sandbox = TestEnv::default();
let cargo_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let fixture_path = cargo_dir.join("tests/fixtures/workspace/contracts/add");

sandbox
.new_assert_cmd("contract")
.current_dir(fixture_path)
.arg("build")
.arg("--verifiable")
.arg("--image")
.arg(ZERO_DIGEST)
.assert()
.failure()
.stderr(predicate::str::contains("source-identification"));
}

// `--source-rev` value must match the 40-hex regex.
#[test]
fn verifiable_source_rev_format_errors() {
let sandbox = TestEnv::default();
let cargo_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let fixture_path = cargo_dir.join("tests/fixtures/workspace/contracts/add");

sandbox
.new_assert_cmd("contract")
.current_dir(fixture_path)
.arg("build")
.arg("--verifiable")
.arg("--image")
.arg(ZERO_DIGEST)
.arg("--source-repo")
.arg("https://github.com/foo/bar")
.arg("--source-rev")
.arg("not-a-sha")
.assert()
.failure()
.stderr(predicate::str::contains("source_rev format"));
}

// `--source-rev` is cross-checked against local git HEAD; a mismatch is a hard
// fail before docker is touched.
#[test]
fn verifiable_source_rev_must_match_head() {
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"]);

let bogus = "a".repeat(40);

sandbox
.new_assert_cmd("contract")
.current_dir(workspace.join("contracts").join("add"))
.arg("build")
.arg("--verifiable")
.arg("--image")
.arg(ZERO_DIGEST)
.arg("--source-repo")
.arg("https://github.com/foo/bar")
.arg("--source-rev")
.arg(bogus)
.assert()
.failure()
.stderr(predicate::str::contains("does not match local HEAD"));
}

// A dirty git tree under `--source-rev` is a hard fail (the recorded rev would
// not describe the bytes built).
#[test]
fn verifiable_dirty_tree_errors_with_source_rev() {
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"]);
let head = git_head(&workspace);
// Dirty the tree after committing so HEAD matches but status is non-empty.
std::fs::write(workspace.join("dirty.txt"), b"uncommitted").unwrap();

sandbox
.new_assert_cmd("contract")
.current_dir(workspace.join("contracts").join("add"))
.arg("build")
.arg("--verifiable")
.arg("--image")
.arg(ZERO_DIGEST)
.arg("--source-repo")
.arg("https://github.com/foo/bar")
.arg("--source-rev")
.arg(head)
.assert()
.failure()
.stderr(predicate::str::contains("dirty").or(predicate::str::contains("clean tree")));
}
2 changes: 1 addition & 1 deletion cmd/soroban-cli/src/commands/container/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::commands::global;

pub(crate) mod logs;
mod shared;
pub(crate) mod shared;
pub(crate) mod start;
pub(crate) mod stop;

Expand Down
78 changes: 77 additions & 1 deletion cmd/soroban-cli/src/commands/contract/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ use crate::{
wasm,
};

pub mod verifiable;

/// A built WASM artifact with its package name and file path.
#[derive(Debug, Clone)]
pub struct BuiltContract {
Expand Down Expand Up @@ -96,6 +98,65 @@ pub struct Cmd {
#[arg(long, conflicts_with = "out_dir", help_heading = "Other")]
pub print_commands_only: bool,

/// Build inside a trusted Docker container and record SEP-58 metadata
/// (`bldimg`, `source_rev`, `bldopt`) so the resulting WASM can be
/// reproduced and verified by third parties. Implies `--locked`.
/// Requires a clean git working tree.
#[arg(long, help_heading = "Verifiable")]
pub verifiable: bool,

/// 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.
#[arg(long, requires = "verifiable", help_heading = "Verifiable")]
pub image: Option<String>,

/// SEP-58 source identification: HTTPS URL (or `github:user/repo`) of the
/// source repository. Must be passed together with `--source-rev`.
#[arg(
long,
requires = "verifiable",
requires = "source_rev",
conflicts_with_all = ["tarball_url", "tarball_sha256"],
help_heading = "Verifiable"
)]
pub source_repo: Option<String>,

/// SEP-58 source identification: 40-char SHA-1 of the source commit. The
/// local workspace must be a git repo at this exact SHA with a clean
/// working tree. Must be passed together with `--source-repo`.
#[arg(
long,
requires = "verifiable",
requires = "source_repo",
conflicts_with_all = ["tarball_url", "tarball_sha256"],
help_heading = "Verifiable"
)]
pub source_rev: Option<String>,

/// SEP-58 source identification: URL where the source tarball can be
/// downloaded.
#[arg(
long,
requires = "verifiable",
conflicts_with_all = ["source_repo", "source_rev"],
help_heading = "Verifiable"
)]
pub tarball_url: Option<String>,

/// SEP-58 source identification: SHA-256 of the source tarball bytes.
#[arg(
long,
requires = "verifiable",
conflicts_with_all = ["source_repo", "source_rev"],
help_heading = "Verifiable"
)]
pub tarball_sha256: Option<String>,

/// Override the default docker host used by `--verifiable`.
#[arg(short = 'd', long, env = "DOCKER_HOST", help_heading = "Verifiable")]
pub docker_host: Option<String>,

#[command(flatten)]
pub build_args: BuildArgs,
}
Expand Down Expand Up @@ -204,6 +265,9 @@ pub enum Error {

#[error("wasm parsing error: {0}")]
WasmParsing(String),

#[error(transparent)]
Verifiable(#[from] verifiable::Error),
}

const WASM_TARGET: &str = "wasm32v1-none";
Expand All @@ -222,6 +286,13 @@ impl Default for Cmd {
out_dir: None,
locked: false,
print_commands_only: false,
verifiable: false,
image: None,
source_repo: None,
source_rev: None,
tarball_url: None,
tarball_sha256: None,
docker_host: None,
build_args: BuildArgs::default(),
}
}
Expand All @@ -230,8 +301,13 @@ impl Default for Cmd {
impl Cmd {
/// Builds the project and returns the built WASM artifacts.
#[allow(clippy::too_many_lines)]
pub fn run(&self, global_args: &global::Args) -> Result<Vec<BuiltContract>, Error> {
pub async fn run(&self, global_args: &global::Args) -> Result<Vec<BuiltContract>, Error> {
let print = Print::new(global_args.quiet);

if self.verifiable {
return verifiable::run(self, global_args, &print).await;
}

let working_dir = env::current_dir().map_err(Error::GettingCurrentDir)?;
let metadata = self.metadata()?;
let packages = self.packages(&metadata)?;
Expand Down
Loading
Loading