Reproducible builds explained
Reproducing an alkane's wasm byte-for-byte means reproducing everything the compiler baked into it. The wasm embeds panic paths (HOME, the registry src-dir hash, git-checkout hashes, the toolchain triple) and a producers section (rustc + clang vendor/version). Each of those is an axis you have to pin. This page explains the ones that actually bite.
You don't pin these by hand. The verifier reverses every axis below out of the on-chain bytecode itself – the embedded paths and
producersrecord are the ground truth. A/verifysubmission of just{ alkane, repo_url, commit, package }is enough for most contracts; the sections here explain what the reverser + builder reconstruct on your behalf, and what to override when a reversal is imperfect.
The environment axes
| Axis | Why it matters |
|---|---|
| Toolchain host triple | wasm32 rust-std is host-independent, but the toolchain's host triple leaks into the wasm two ways: (1) toolchain paths in panic strings, and (2) the rustc -vV host: line, which rustc folds into every crate's -C metadata / StableCrateId. Both must match the origin host. Since the arch is the same, you can run the origin toolchain on Linux and reproduce bit-for-bit – see the macOS and Windows recipes below. |
| clang vendor + version | Written verbatim into producers. Ubuntu clang 14.0.0 (apt) vs Homebrew clang 20.1.7 give different bytes. Homebrew clang is assembled on Linux from content-addressed GHCR bottle blobs (llvm + z3 + libedit) plus patchelf. |
HOME / remap_path_prefix | Registry, git-checkout, and toolchain paths all hang off HOME. With an empty remap-path-prefix, paths are raw and HOME must be reconstructed exactly (e.g. /home/lee, /Users/kevinyao). |
| Registry mode | A committed Cargo.lock ⇒ real crates.io + --locked. No lock ⇒ the time-machine: freeze the crates.io-index tree at the build date and serve it as index.crates.io so the registry src-dir hash is canonical. |
| Git-dep source-id | A bare git source-id (?rev=X# rewritten to #, then --locked) changes -C metadata and therefore monomorphization ordering – different bytes. Reproduce the exact spelling the origin lock used (bare vs rev). |
| secp256k1 C object | The one axis that doesn't fully reconstruct: a secp256k1 object built by a foreign host's clang has a slightly different static footprint, shifting the memory base a few bytes. Pure-Rust alkanes → byte-exact; alkanes that link a foreign-host C object → verified (~99.997%). |
The registry time-machine
When a repo commits no Cargo.lock, cargo would resolve against today's crates.io – different versions, different bytes. Instead we:
- Depth-1 fetch the
crates.io-index-archivecommit frozen at the build's date. - Serve it over local HTTPS as
index.crates.io(hosts entry + self-signed cert +CARGO_HTTP_CAINFO), and build with--add-host index.crates.io:127.0.0.1.
This makes the registry src-dir hash canonical and cargo-version-dependent: cargo 1.82 → index.crates.io-6f17d22bba15001f, cargo 1.86 → index.crates.io-1949cf8c6b5b557f.
Bare git source-id
Alkanes commonly depend on alkanes-rs (and metashrew) by git. The lockfile's spelling of that source-id changes the compiled metadata. To match a bare origin, resolve the dependency with its rev, then rewrite the lockfile source (?rev=X# → #) and build --locked. Reproducing rev form instead is a straight --locked build.
Multiple git deps, transitive pins, and mirrored repos
A real contract rarely pins one git dep. The builder handles the whole graph:
- Multi-dep pinning. It pins every git dependency it can identify (a pool commonly pins both
alkanes-rsandmetashrew), not just the singlealkanes-rsrev. The reverser derives the exact rev each dep was checked out at from the bytecode's.cargo/git/checkouts/<name>-<hash>/<rev>/paths and pins them by name. - Transitive pins via the lockfile. A dep pulled in through another git dep (e.g.
metashrewreached viaalkanes-rsunder the same URL) would otherwise drift to a movingHEAD, producing two revs of one crate → anE0599"multiple versions" mismatch. The builder uses cargo's own resolver (update --precise) to move the whole graph to the ground-truth rev consistently. - Mirrored-repo unify. When a contract and its deps reference the same crate through different repo URLs (e.g.
sandshrewmetaprotocols/metashrewvskungfuflex/metashrew), cargo builds two incompatible copies. The builder redirects one URL onto the other's exact source spec so both unify to a single source. - Date-freeze for unpinned deps. Any git dep still left unpinned (a
git = "url"with no rev and no committed lock) is frozen to that repo'sHEAD-as-of the deploy date – the same date used for the registry time-machine below. This is the universal fallback that needs no per-dep rev.
Over the API these map to the git_pins (explicit url rev [bare|rev] lines) and git_date fields, but both are auto-reversed from the bytecode and the deploy tx's block time, so you seldom set them.
Auto-freezing the crates.io index to the deploy date
The registry time-machine (below) normally needs an explicit freeze_commit. When none is supplied, the builder resolves it automatically from the deploy date (the deploy transaction's block time): it finds the crates.io-index-archive snapshot branch covering that date and takes the last commit at or before it. This is the piece that lifts a no-recipe, no-committed-lock build to byte fidelity without anyone hand-picking a freeze commit.
secp256k1 → wasm with Ubuntu clang
C deps like secp256k1-sys used to compile to wasm only under the self-contained Homebrew clang, because Ubuntu's apt clang leaks the host glibc include path (/usr/include/stdint.h → a missing bits/libc-header-start.h) when targeting wasm32-unknown-unknown. The builder now passes -nostdlibinc (dropping the system libc includes while keeping clang's own builtin headers; the crate's bundled wasm/wasm-sysroot supplies the rest), so apt clang-13/14/15 can build secp256k1→wasm. That lets a Linux-origin contract be matched at its actual apt clang version rather than being forced onto Homebrew clang. Extra flags can be added via the WASM_CFLAGS build knob.
Worked example: 4:797 → reproducible
free-mint commits no lock and links no C, so it reduces to three pins:
- registry: time-machine, index frozen at
2025-03-19, served asindex.crates.io(hash…6f17d22bba15001f). - clang: Ubuntu 14.0.0 via apt (
clang-14) – even though the artifact is pure Rust, theproducersrecord still names it. - git source-id:
alkanes-rsas a bare source-id (d787cdd1…).
Result: rebuilt sha256 8b51384a… == on-chain → reproducible, byte-exact, 100%.
Worked example: 2:0 → reproducible (macOS via a darwin toolchain path)
DIESEL's alkanes-std-genesis-alkane-upgraded-eoa was built on an Apple Silicon Mac (rustc 1.86.0, HOME=/Users/kevinyao). We reconstruct it on Linux by copying the 1.86.0 toolchain to $RUSTUP_HOME/toolchains/1.86.0-aarch64-apple-darwin (a symlink is canonicalized away by rustc), setting HOME=/Users/kevinyao, and cloning the repo to the origin build dir. Same architecture (aarch64/x86_64 rust-std for wasm32 is host-independent), so the toolchain path and the host: metadata line both come out identical → rebuilt sha256 == on-chain, byte-exact → reproducible (proven against minibot).
Worked example: 4:9200 → reproducible (Windows via Wine)
predicates pair-equality was built with a Windows rustc 1.86.0 (x86_64-pc-windows-msvc). The stubborn residual was the rustc -vV host: line – x86_64-pc-windows-msvc – which rustc folds into every crate's -C metadata / StableCrateId; nothing on Linux reproduces that except a genuine Windows host triple. So we run the Windows toolchain under Wine on Linux. The target arch (x86_64) is the same, so Wine executes the real rustc.exe and it reproduces the wasm bit-for-bit → reproducible. (An earlier golden fixture captured this alkane before deployment at partial; it is now deployed and byte-exact.)
Worked example: 4:76 (busd) → verified (foreign-host C object)
Some alkanes link secp256k1's C object built by a host we can't run on Linux the way we run macOS/Windows toolchains. 4:76 (busd) and the oyl implementations fall here: Rust code and the producers record reconstruct exactly, but the secp256k1 static footprint from the foreign host's clang shifts the memory base by ~6 bytes. That is the entire, understood residual → verified (~99.997%). These are the builds that use the admin attest path (or alkanes-cli upload without --verify), since the Linux verifier can't reach byte-exact on its own.