Add container build backend and build verify command#2525
Add container build backend and build verify command#2525leighmcculloch wants to merge 54 commits intomainfrom
Conversation
…pt_package, bldopt_profile, bldopt_optimize
|
Each meta field needs a very detailed specification of exactly what valid values are expected, and the format(s) those values can take. For example, depending on how you install the Homebrew: Cargo Cargo git install: Without a precise spec for each field, downstream tooling (verifiers, registries, indexers) can't reliably parse or validate these values. |
|
Currently only the That works, and the host-side stellar-cli version is captured in the wasm via the A way to get the best of both: Embed a Dockerfile in the stellar-cli that, at build time, layers on top of whatever rust base image is requested:
Both the rust version and the stellar-cli version are specified at build (and verify) time, the image gets built locally, and the entire build pipeline runs inside it. The user on the host doesn't need to install a matching stellar-cli to verify a build produced by a different version — that version is installed into the image instead. This avoids the supply-chain cost of us owning and publishing a bespoke stellar build image, keeps the rust base image flexible, and still lets the image digest capture the whole pipeline. It also resolves the It also keeps the door open for SDF or someone else to host prebuilt images later as a convenience — but we don't have to figure that out for the first iteration. The embedded-Dockerfile approach defers that decision without foreclosing it. |
|
Opened an issue about the cliver inconsistency here: |
|
Heads up on a devx tradeoff worth documenting: building inside an Small contracts won't notice it. Workspaces with many contracts, or contracts with heavy dep trees, will see noticeably slower builds inside docker (sometimes a multiple of the native build time). This is a fundamental cost of pinning the build to a single arch for reproducibility, not something the It's also not only a perf concern: users on container runtimes that don't ship qemu/binfmt support (some minimal Linux setups, certain rootless or stripped-down podman configs, etc.) won't be able to run amd64 containers on arm64 hosts at all — they'll see Worth surfacing in docs so users aren't caught off guard, and worth keeping |
|
Another scaling tradeoff worth flagging between the two backends:
In practice this is an edge case — most users build with a small set of (rust, cli) combinations and the layered image gets cached the first time. |
What
Add two container build backends to
stellar contract build(anddeploy/upload) —--backend dockerand--backend docker-all— that produce reproducible wasms by running the build inside alinux/amd64container, recording the resolved image digest plus the source git remote/sha, the per-package build options, and which backend produced the wasm in contract metadata. Add astellar contract build verifysubcommand that reads everything it needs from the wasm's metadata, rebuilds with the same toolchain, image, and build options, and reports which (if any) rebuilt artifact is byte-identical to the original. Add a mainnet warning onstellar contract deploywhen the wasm is missing the meta entries needed for independent verification.Why
Contract builds vary across host OS, architecture, and toolchain, preventing third parties from independently confirming a deployed contract was built from given source. Pinning the build to an image digest plus the rust toolchain version makes builds reproducible, recording the source repo + commit + per-package build options lets verifiers rebuild the exact same artifact, and the new
verifysubcommand automates the rebuild-and-compare check.Closes #2506.
Two backends:
dockervsdocker-allTwo container-based backends are introduced. They differ in where in the pipeline the host CLI runs vs. the in-container CLI runs.
--backend docker--backend docker-allrust:latest)FROM <user-chosen rust> + rustup target add wasm32v1-none + cargo install --locked --git https://github.com/stellar/stellar-cli --rev <host's commit sha> stellar-clicargo rustconlystellar contract buildpipeline (cargo + meta injection + spec filtering + optionalwasm-opt)wasm-opt)--out-dirif requested)bldimgrecordsbldimg + cliver)bldbkdrecordsdockerdocker-allcargo install --git --rev <sha> stellar-clito build the layered image (cached locally afterward)Both backends embed the same set of build-recording meta entries (see table below), so a wasm built with either backend can be deployed identically and verified by any user with docker. Choosing between them is a pragmatic trade:
dockeris cheaper to run but couples reproducibility to the host CLI version;docker-allcaptures the whole pipeline at the cost of an extra layered-image build.Discussion of the design choices: #2525 (comment).
How it works
Three parts: build-time recording, deploy-time warning, and verify-time reproduction.
Build
For all backends (including
local), the build:bldbkd(one oflocal,docker,docker-all) so anyone inspecting the wasm can see which build path produced it.originremote, embedssource_repo(URL canonicalized tohttps://…),source_rev(full HEAD SHA), and per-package build options (bldopt_manifest_pathrelative to git root,bldopt_package,bldopt_profile, optionalbldopt_optimize). The manifest path is auto-inserted whether or not--manifest-pathwas passed on the CLI.source_repo/source_rev/bldopt_*.For
--backend docker, additionally:docker.io/library/rust:latest) with--platform=linux/amd64over the Docker daemon's HTTP API (viabollard— nodockerbinary required), and resolves a fully-qualified<registry>/<path>@sha256:<digest>reference. The pull runs only once per build invocation; multi-contract workspaces reuse the cached digest.<git_root or workspace_root>→/workspace(rw, source)<target_dir>→/target(rw, build output, host reads the wasm afterward)~/.cargo/registry→/usr/local/cargo/registry(rw, cached crate downloads)~/.rustupis not mounted — host toolchain binaries are platform-specific (e.g. Mach-O on macOS) and can't run inside the linux/amd64 container. The image's pre-installed rustup state is used; the wasm target is installed at the start of each container run.rustup --quiet target add [--toolchain <pin>] wasm32v1-none && exec cargo [+<pin>] rustc …inside the container — the wasm target is always installed up-front, so the build doesn't depend on the workspace'srust-toolchain.toml.--locked. SetsSOURCE_DATE_EPOCHfromgit log -1 --format=%ct. SetsCARGO_TERM_COLOR=always.--remap-path-prefixuses container paths so wasms don't leak host paths.Compiling …,Finished …).inject_meta, spec shaking, optionalwasm-opt) runs on the host as before.For
--backend docker-all, instead of (4)–(7) above:docker.STELLAR_CLI_REVis the full 40-char commit sha of the host stellar-cli (extracted fromversion::git()); the layered image therefore contains the same CLI version as the host. The image is taggedstellar-cli-build:<short-hash-of(base_digest, cli_rev)>so docker's layer cache hits across runs.stellar contract build --backend local --bldimg <base-digest> --bldbkd docker-all …against the same bind mounts asdocker. The in-container CLI does cargo + meta injection + spec filtering + optionalwasm-optitself; the host only copies outputs to--out-dirif requested.Verification on a different machine reconstructs the layered image from
bldimg(base) +cliver(CLI version) — the layered image digest itself doesn't need to be recorded because the recipe is deterministic.The wasm's
contractmetav0custom section is populated with up to nine entries:cliver26.0.0#abc1234(CLI version + git rev)bldbkdlocal/docker/docker-allbldimgdocker.io/library/rust@sha256:…(fully-qualified)--backend docker[-all])rsver1.83.0(resolved rustc version)source_repohttps://github.com/user/repo(clean repo's origin)source_revbldopt_manifest_pathcontracts/foo/Cargo.toml(relative to git)bldopt_packagebldopt_profilerelease)bldopt_optimizetrue(only present when--optimizewas used)For full reproducibility from day one, pin the image with
--backend docker[-all]=<name>@sha256:…and commit before building.--backendand--docker-hostare also exposed onstellar contract deployandstellar contract upload(which auto-build when no--wasm/--wasm-hashis given), so the same flags work end-to-end.Deploy
stellar contract deployagainst mainnet now warns when the wasm is missing any ofcliver,bldimg,rsver,source_repo,source_rev,bldopt_manifest_path,bldopt_package,bldopt_profile:The check is mainnet-only (matches network passphrase against
Public Global Stellar Network ; September 2015); on testnet/futurenet/local the wasm deploys silently.Verify
verifyis a subcommand ofbuild— it lives atstellar contract build verify, and works on multi-contract workspaces by rebuilding and finding the match.contract info).cliver,bldimg,bldbkd,rsver,bldopt_manifest_path,bldopt_package,bldopt_profile, optionalbldopt_optimizefrom the wasm's meta.bldbkdis treated asdockerif absent (legacy wasms predating this PR). Forbldbkd: local, errors out (local builds aren't reproducible).bldbkd:docker: usesBackend::Docker { image: bldimg }. Errors if the running CLI'scliverdoesn't match the wasm's (host post-processing makes this mismatch fatal).docker-all: usesBackend::DockerAll { image: bldimg }. Skips theclivermismatch check — the in-container CLI is what matters, and it's installed at the wasm'scliverregardless of the host CLI.bldopt_manifest_pathagainst the cwd's git top-level (viagit rev-parse --show-toplevel) so verify works from anywhere inside the checkout.<rsver>(i.e. cargo invoked ascargo +<rsver> rustc …), and the recorded--manifest-path/--package/--profile/--optimizeflags. rustup uses that exact rust version regardless ofrust-toolchain.toml's channel.The user is responsible for checking out the matching commit before running verify; verify rebuilds from the working tree. (
source_repoandsource_revare embedded in meta to help users find the right commit, but verify itself doesn't clone — that would add a separate trust path.)End-to-end example (
dockerbackend)End-to-end example (
docker-allbackend)Notes
/var/run/docker.sock, or whatever--docker-host/DOCKER_HOSTpoints at). Sameconnect_to_dockerhelper used bystellar container start/stop/logs, with the same Docker Desktop fallback ($HOME/.docker/run/docker.sock). No shell-out to thedockerCLI. A podman socket exposing the Docker API would also work (untested).~/.cargo/registrylets the container reuse crate downloads the host already has. Fordocker-all, docker's layer cache also keeps thecargo install stellar-clilayer warm across runs as long as(base_digest, cli_rev)is unchanged.rust-toolchain.tomldependency: every container build runsrustup target add wasm32v1-none(with--toolchain <pin>when verifying) before cargo, so the workspace'srust-toolchain.tomldirectives are not relied on.cargo +<rsver>(rustup's explicit toolchain selector) — this overridesrust-toolchain.toml'schanneland ensures the same exact rust version is used across machines and time.bldimgis normalized to<registry>/<path>@sha256:<digest>(e.g.rust:latest→docker.io/library/rust@sha256:…) so verify can resolve it without relying on the local registry config.docker-allrequires a host CLI built from a commit: the layered image installs the stellar-cli at the host's full 40-char commit sha. Homebrew/crates.io/cargo-git installs all produce a usable sha. Local-cargo-run builds work as long as HEAD has been pushed to origin (socargo install --git --rev <sha>can fetch it). See Normalize stellar-cli version rendering instellar versionandclivermeta #2535 for the in-progress normalization of thecliverrendering.source_repois normalized tohttps://…form (e.g.[email protected]:user/repo.git→https://github.com/user/repo).bldopt_manifest_pathis recorded relative to the git repo root regardless of whether--manifest-pathwas passed on the CLI. Verify resolves it against the cwd's git top-level so the command works from anywhere inside the checkout.Build Completelines.docker container prune.Status
This is an experiment in validating the ideas in #2506. May or may not be destined for merging — at this moment it is an experiment in validating the approach. We have not yet decided which of
dockeranddocker-allto keep; both are exercising real tradeoffs and the PR ships both so they can be compared in practice.