Reach compiler and Docker runtime: who should own image version pinning for reproducible builds?
0 reputation · 24 Jan 2022, 22:20 UTC
0 reputation · 24 Jan 2022, 22:20 UTC
We are adopting Reach (reach.sh) for a DApp project and want a repeatable development environment across laptops and CI. Reach distributes its compiler and standard library as Docker images invoked through the ./reach wrapper script, so the host only needs Docker. That makes the boundary between the Reach toolchain and the Docker runtime the place where reproducibility either holds or silently breaks.
Our goal: two machines running the same .rsh source should produce identical compilation output and behave identically against the local devnet. The uncertainty is that the wrapper appears to select image versions, but it is unclear whether it pins mutable tags or content-addressed digests. Tags can drift when re-pulled, so "same Reach release" may not mean "same images." Compilation output also depends on the chosen connector (Algorand vs Ethereum-style), so pinning must cover both compiler and connector. Some of our machines sit behind a corporate proxy, and one developer uses rootless Docker, which may change devnet networking assumptions.
35275 reputation · 25 Jan 2022, 00:01 UTC
The Reach toolchain side — the wrapper script and your project's pinned Reach release — should own version selection, and your repository should own recording the resolved digests. The Docker runtime should be treated as a dumb executor: it runs whatever image reference it is handed and should never decide which version that is. If you let the runtime (or a developer's local image cache) influence which image runs, you have moved the reproducibility boundary to the least controllable place.
Whether the ./reach wrapper resolves images by mutable tag or by digest is a property of the specific Reach release you installed, and it has changed across versions of similar toolchain wrappers, so treat it as unverified until you check. Two quick checks on one machine settle it:
# See which images the wrapper actually pulls/uses
REACH_DEBUG=1 ./reach compile --help 2>&1 | grep -i 'docker\|image'
# After a compile, inspect what is in the local cache
docker images --digests | grep -i reachIf the wrapper references tags like reachsh/reach:x.y.z, then "same release" does not guarantee "same bytes" — a re-published tag or a stale local cache on one laptop will silently diverge. If it references @sha256:... digests, the release pin is already content-addressed and your remaining risk is much smaller.
A practical pattern: after first setup on a clean machine, record digests into a checked-in file:
docker images --digests --format '{{.Repository}}@{{.Digest}}' \
| grep -i reach > reach-images.lockThen add a CI step that fails if the resolved digests differ from the lockfile. That gives you drift detection without fighting the wrapper. Only if you observe actual drift should you override image references yourself (e.g., via the wrapper's environment-variable overrides, if your release exposes them) — overriding prematurely couples you to internals that change between releases.
Neither rootless Docker nor a corporate proxy changes image reproducibility — digests are digests regardless of how the bytes arrived. The risk is at the devnet networking layer, not the compiler layer:
host.docker.internal or specific localhost bindings, test that path explicitly on the rootless machine.HTTP(S)_PROXY in the Docker daemon config (not just your shell), but a transparent caching proxy that serves correct content does not alter digests — verify with docker inspect on the pulled image if you suspect rewriting.Pin the Reach release plus connector in the repo, record resolved digests in a lockfile, and let Docker be a generic executor. Verify the wrapper's tag/digest behavior on your exact release before deciding whether you need to override image references — that one check determines how much of this you actually have to build.
Use comments to ask for clarification. Post a solution as an answer.
35,275 reputation · 25 Jan 2022, 05:23 UTC
One thing worth adding to the ownership split: the digest lockfile only protects you end-to-end if the reference you recorded is the reference Docker actually pulls. A corporate registry mirror or pull-through cache that re-tags or serves from its own namespace can silently substitute content, so the digest recorded on a developer laptop may not match what CI receives. Verify on both sides of the proxy:
docker inspect --format='{{index .RepoDigests 0}}' reachsh/reach:x.y.zand compare the digest per machine, not just per release.
On the connector point: because connector selection changes codegen, treat the connector image pin as part of the same lockfile entry as the compiler — a single record of {reach release, connector, compiler digest, connector digest} per project. That way a CI check can fail loudly if any one of the four drifts, instead of producing a subtly different artifact that only diverges at devnet time.
For rootless Docker, the version-selection story is unchanged, but devnet port binding and host-networking assumptions are the more likely breakage point than image resolution — worth a separate smoke test on that one machine rather than blocking the pinning work on it.