From 72e3ad918711de454049e98e2f454003618570f3 Mon Sep 17 00:00:00 2001 From: void* Date: Wed, 16 Sep 2026 23:28:52 +0100 Subject: [PATCH] Software upgrade v0.15.1 --- .dockerignore | 12 ++ .github/workflows/build_and_test.yml | 29 +++++ .github/workflows/build_push.yml | 9 ++ .gitignore | 1 + Dockerfile | 25 ++++- Makefile | 92 ++++++++++++++- app/upgrades.go | 155 +++++++++++++++++-------- build-production-image-with-push.sh | 50 +++++++++ build-production-image.sh | 154 +++++++++++++++++++++++++ ci.Dockerfile | 8 +- docker-bake.hcl | 162 +++++++++++++++++++++++++++ go.mod | 8 +- go.sum | 8 +- musl.Dockerfile | 161 ++++++++++++++++++++++++++ 14 files changed, 817 insertions(+), 57 deletions(-) create mode 100644 .dockerignore create mode 100755 build-production-image-with-push.sh create mode 100755 build-production-image.sh create mode 100644 docker-bake.hcl create mode 100644 musl.Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..c16e4344f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +# Excluded from the Docker build context for ALL Dockerfiles in this repo. +# +# .modcache is the Go proxy-layout module cache consumed by musl.Dockerfile +# (see its header). It is ~5 GB, so it must never be part of `COPY . .` image +# layers; instead it is passed to the build as a *named build context* +# (--build-context modcache=.modcache, or automatically via the musl-hub +# target in docker-bake.hcl) and bind-mounted only where the build needs it. +# It is git-ignored (see .gitignore) and populated by: +# ./build-production-image.sh musl +# or manually: +# cp -R "$(go env GOMODCACHE)/cache/download/." .modcache/ +.modcache diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 06d639318..5cbb95f52 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -11,14 +11,43 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v2 + with: + # Fetch full history including tags so that `git describe --tags` + # (used for VERSION in the Makefile) works inside the docker build. + fetch-depth: 0 - name: Build Docker container + env: + # Token with read access to the private go module dependencies + # (e.g. github.com/fetchai/priv_wasmd_sec). Must be set as a repo/org + # secret; the default GITHUB_TOKEN cannot read other private repos. + GH_PRIVATE_DEP_TOKEN: ${{ secrets.GH_PRIVATE_DEP_TOKEN }} run: | + if [ -z "$GH_PRIVATE_DEP_TOKEN" ]; then + echo "::error::GH_PRIVATE_DEP_TOKEN secret is not set. Add a fine-grained PAT with read access to the private go module deps as a repo/org secret." >&2 + exit 1 + fi + # Fail fast with a clear diagnosis if the token cannot read the + # private dep repos (bad scope, expired, not SSO-authorized, ...). + for repo in priv_wasmd_sec priv_wasmvm_sec; do + code=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: Bearer $GH_PRIVATE_DEP_TOKEN" \ + "https://api.github.com/repos/fetchai/$repo") || code="curl_error" + if [ "$code" != "200" ]; then + echo "::error::GH_PRIVATE_DEP_TOKEN cannot access fetchai/$repo (API status: $code). Check the token's repository access/permissions (fine-grained: Contents=Read-only on the repo; classic: 'repo' scope + SSO authorization)." >&2 + exit 1 + fi + done + umask 077 + printf 'machine github.com\nlogin x-access-token\npassword %s\n' "$GH_PRIVATE_DEP_TOKEN" \ + > "$RUNNER_TEMP/netrc" docker build \ --no-cache \ --progress plain \ + --secret id=netrc,src="$RUNNER_TEMP/netrc" \ --tag fetch_cosmos_wasmd:$GITHUB_SHA \ --file ./ci.Dockerfile \ ./ + rm -f "$RUNNER_TEMP/netrc" - name: Run make test run: | docker run --rm fetch_cosmos_wasmd:$GITHUB_SHA \ diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml index 8140b7fe4..904d11f82 100644 --- a/.github/workflows/build_push.yml +++ b/.github/workflows/build_push.yml @@ -2,6 +2,15 @@ name: Build and push images on: push: tags: + # Run for every tag push EXCEPT the v0.15.1 release tag (that release's + # images are built/pushed manually via + # build-production-image-with-push-and-pull.sh). + # Tag patterns are anchored full-string matches, so 'v0.15.1' matches + # ONLY the exact tag (v0.15.1-rc0 etc. still trigger this workflow). + # NOTE: for future releases, extend the negated pattern (e.g. a + # '!v[0-9]*.[0-9]*.[0-9]*' pattern would exclude ALL plain semver + # release tags, but also any tag with a suffix like -rc0). + - '!v0.15.1' - '*' jobs: diff --git a/.gitignore b/.gitignore index a6aa651c9..e61b3ee8d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # OS .DS_Store +.modcache/ *.swp *.swo *.swl diff --git a/Dockerfile b/Dockerfile index e53c8cc69..603f7180f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,11 +10,30 @@ WORKDIR /cosmwasm COPY . . -RUN make install - +# Optional Go runtime setting, empty by default. Set via --build-arg only where +# needed, e.g. GODEBUG=asyncpreempt=0 when building linux/amd64 under CPU +# emulation (QEMU/Rosetta on Apple Silicon), where emulated signal delivery +# can crash the go toolchain (SIGSEGV in runtime.suspendG). On native builds +# (Linux CI) leave it unset. The ARG value is exposed as an environment +# variable to the RUN steps below; no ENV needed. +ARG GODEBUG="" + +# GitHub token for private repos (GOPRIVATE) is mounted from the host at build +# time; it is never baked into the image layers. Passed via buildx/bake as +# secret id=netrc (a .netrc file readable by git over HTTPS). +RUN --mount=type=secret,id=netrc,target=/root/.netrc make install RUN GOPATH="$(go env GOPATH)" -RUN ARCH=`uname -m` && ln -s $GOPATH/pkg/mod/github.com/\!cosm\!wasm/wasmvm/v*/internal/api/libwasmvm.${ARCH}.so /usr/lib/libwasmvm.${ARCH}.so +# Resolve the wasmvm module's actual cache directory via go list -m, so this +# works regardless of any `replace` directives in go.mod (e.g. the private +# github.com/fetchai/priv_wasmvm_sec replacement) or if they are later dropped. +RUN ARCH=$(uname -m) && \ + WASMVM_DIR=$(go list -m -f '{{.Dir}}' github.com/CosmWasm/wasmvm/v3) && \ + ln -s "${WASMVM_DIR}/internal/api/libwasmvm.${ARCH}.so" /usr/lib/libwasmvm.${ARCH}.so && \ + test -e /usr/lib/libwasmvm.${ARCH}.so || \ + { echo "ERROR: /usr/lib/libwasmvm.${ARCH}.so is a dangling symlink;" >&2; \ + echo " go list could not resolve github.com/CosmWasm/wasmvm/v3 —" >&2; \ + echo " check go.mod and GOPRIVATE settings." >&2; exit 1; } # ################################## diff --git a/Makefile b/Makefile index 6fff23b55..44b0f9d3d 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ PACKAGES_NOSIMULATION=$(shell go list ./... | grep -v '/simulation') PACKAGES_SIMTEST=$(shell go list ./... | grep '/simulation') -VERSION := $(shell echo $(shell git describe --tags)) +VERSION := $(shell git describe --tags --long --always --dirty 2> /dev/null || echo v0.0.0-dev) COMMIT := $(shell git log -1 --format='%H') LEDGER_ENABLED ?= true BINDIR ?= $(GOPATH)/bin @@ -14,6 +14,7 @@ DOCKER_BUF := docker run -v $(shell pwd):/workspace --workdir /workspace bufbuil PROJECT_NAME = $(shell git remote get-url origin | xargs basename -s .git) export GO111MODULE = on +export GOPRIVATE=github.com/fetchai/priv_wasmd_sec,github.com/fetchai/priv_wasmvm_sec # process build tags @@ -101,6 +102,95 @@ endif install: go.sum go install -mod=readonly $(BUILD_FLAGS) ./cmd/fetchd +# Static (musl) builds: link fetchd against the static wasmvm archive +# (libwasmvm_muslc..a) instead of the dynamic libwasmvm..so, so the +# resulting binary is fully static and the .so libraries do not need to be +# distributed/shipped alongside it. +# +# Requirements: +# - the build must run on a musl-based system (e.g. Alpine Linux) with +# gcc, make and xz available; the static-pie link mode used below is +# supported by musl but NOT by glibc +# - write access to /lib (the extracted archive must live in a default +# linker search path, because the wasmvm cgo bindings link it via +# -lwasmvm_muslc.; in Docker/container builds this is a given) + +UNAME_M := $(shell uname -m) +STATIC_WASMVM_MODULE := github.com/CosmWasm/wasmvm/v3 +STATIC_WASMVM_VERSION := $(shell go list -mod=readonly -m -f '{{.Version}}' $(STATIC_WASMVM_MODULE)) +STATIC_WASMVM_LIB := /lib/libwasmvm_muslc.$(UNAME_M).a +STATIC_BUILD_TAGS := muslc +STATIC_LDFLAGS := -linkmode=external -extldflags=-static + +# Extract the static (musl) wasmvm archive shipped inside the module that +# satisfies github.com/CosmWasm/wasmvm/v3 (following any `replace` directives +# in go.mod) into /lib, so that the external linker (gcc) finds it when the +# `muslc` build tag makes the wasmvm cgo bindings link it +# (-lwasmvm_muslc.). +# +# The module dir is resolved *inside the recipe shell* (not via a Make +# $(shell) expansion) so that the resolution provably happens after the +# `go mod download` that precedes it. GOPRIVATE is cleared for the go +# commands so the file:// GOPROXY cache is used for the private module +# instead of an authenticated VCS fetch. +.PHONY: static-wasmvm-lib +static-wasmvm-lib: go.sum + @echo "--> Extracting static wasmvm library ($(STATIC_WASMVM_VERSION)) to $(STATIC_WASMVM_LIB)" + @dir=$$(GOPRIVATE= go list -mod=readonly -m -f '{{.Dir}}' $(STATIC_WASMVM_MODULE)); \ + if [ -z "$$dir" ]; then \ + GOPRIVATE= go mod download $(STATIC_WASMVM_MODULE); \ + dir=$$(GOPRIVATE= go list -mod=readonly -m -f '{{.Dir}}' $(STATIC_WASMVM_MODULE)); \ + fi; \ + if [ -z "$$dir" ]; then \ + echo "ERROR: could not resolve module dir for $(STATIC_WASMVM_MODULE)" >&2; \ + exit 1; \ + fi; \ + unxz -c "$$dir/internal/api/libwasmvm_muslc.$(UNAME_M).a.xz" > "$(STATIC_WASMVM_LIB)" + @chmod 644 "$(STATIC_WASMVM_LIB)" + +# Generic static build: no wasmvm archive checksum pinning. Use this for +# non-release builds (e.g. local Alpine builds, CI builds from master). For +# release builds use the version-pinned target below instead. +install-static: static-wasmvm-lib + $(MAKE) install \ + LEDGER_ENABLED=$(LEDGER_ENABLED) \ + BUILD_TAGS=$(STATIC_BUILD_TAGS) \ + LDFLAGS="$(STATIC_LDFLAGS)" + +build-static: static-wasmvm-lib + $(MAKE) build \ + LEDGER_ENABLED=$(LEDGER_ENABLED) \ + BUILD_TAGS=$(STATIC_BUILD_TAGS) \ + LDFLAGS="$(STATIC_LDFLAGS)" + +# Release-pinned static build for v0.15.1: asserts the exact wasmvm version +# resolved from go.mod and the SHA256 of the *decompressed* static archive +# before building. The checksums are for github.com/fetchai/priv_wasmvm_sec/v3 +# v3.0.8-rc.2 (the go.mod replace target of github.com/CosmWasm/wasmvm/v3); +# update them whenever the wasmvm pin changes. +verify-static-wasmvm-lib-v0.15.1: WASMVM_VERSION := v3.0.8-rc.2 +verify-static-wasmvm-lib-v0.15.1: WASMVM_SHA256_x86_64 := 6863af60cebf04d094bc3bcf22a2777e1e1b4f1295d54e3b9a1082a3b359de8a +verify-static-wasmvm-lib-v0.15.1: WASMVM_SHA256_aarch64 := 46f4d0913331096f2926f28d5d0f4405eb700f571be229cf8774d942619370a4 +verify-static-wasmvm-lib-v0.15.1: static-wasmvm-lib + @echo "--> Verifying wasmvm version pin for fetchd v0.15.1" + @if [ "$(WASMVM_VERSION)" != "$(STATIC_WASMVM_VERSION)" ]; then \ + echo "ERROR: wasmvm module version is '$(STATIC_WASMVM_VERSION)', but fetchd" >&2; \ + echo " v0.15.1 is pinned to '$(WASMVM_VERSION)'." >&2; \ + exit 1; \ + fi + @echo "--> Verifying static wasmvm library checksum" + @if [ "$$(sha256sum $(STATIC_WASMVM_LIB) | cut -d' ' -f1)" != "$(WASMVM_SHA256_$(UNAME_M))" ]; then \ + echo "ERROR: SHA256 mismatch for $(STATIC_WASMVM_LIB)" >&2; \ + echo " expected: $(WASMVM_SHA256_$(UNAME_M))" >&2; \ + echo " actual: $$(sha256sum $(STATIC_WASMVM_LIB) | cut -d' ' -f1)" >&2; \ + exit 1; \ + fi + @echo " OK" + +install-static-v0.15.1: verify-static-wasmvm-lib-v0.15.1 install-static + +.PHONY: install-static build-static verify-static-wasmvm-lib-v0.15.1 install-static-v0.15.1 + ######################################## ### Tools & dependencies diff --git a/app/upgrades.go b/app/upgrades.go index 9b7189c54..4f335c21f 100644 --- a/app/upgrades.go +++ b/app/upgrades.go @@ -13,16 +13,12 @@ import ( circuittypes "cosmossdk.io/x/circuit/types" "cosmossdk.io/x/nft" upgradetypes "cosmossdk.io/x/upgrade/types" - "github.com/CosmWasm/wasmd/app/upgrades" - "github.com/CosmWasm/wasmd/app/upgrades/noop" - v060 "github.com/CosmWasm/wasmd/app/upgrades/v060" tmproto "github.com/cometbft/cometbft/proto/tendermint/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" consensusparamtypes "github.com/cosmos/cosmos-sdk/x/consensus/types" epochstypes "github.com/cosmos/cosmos-sdk/x/epochs/types" "github.com/cosmos/cosmos-sdk/x/group" - "github.com/strangelove-ventures/tokenfactory/x/tokenfactory/keeper" //minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" paramstypes "github.com/cosmos/cosmos-sdk/x/params/types" @@ -35,14 +31,67 @@ import ( ibcexported "github.com/cosmos/ibc-go/v10/modules/core/exported" "github.com/fetchai/fetchd/app/ica_migration" "github.com/fetchai/fetchd/app/traces" + "github.com/strangelove-ventures/tokenfactory/x/tokenfactory/keeper" tokenfactorytypes "github.com/strangelove-ventures/tokenfactory/x/tokenfactory/types" ) -// ---- Match this to the plan name that is already stored on disk and halted the chain. -const UpgradeNameV053 = "v0.15.0" +// ============================================================================ +// UPGRADE PLAN NAMES +// ============================================================================ + +// UpgradeNameV0_15_0 is the plan name of the PREVIOUS upgrade ("v0.15.0"). +// It is already applied on-chain; its handler is kept only so that this +// binary can still correctly execute that upgrade if it is ever pending. +const UpgradeNameV0_15_0 = "v0.15.0" + +// UpgradeNameV0_15_1 is the plan name of the CURRENT UPCOMING upgrade. +const UpgradeNameV0_15_1 = "v0.15.1" + +// RegisterUpgradeHandlers registers the software upgrade handlers. +// +// The code is split into two clearly separated sections below: +// one for the previous upgrade (v0.15.0) and one for the upcoming +// upgrade (v0.15.1). +func (app *App) RegisterUpgradeHandlers(cfg module.Configurator) { + app.registerV0_15_0UpgradeHandler(cfg) + app.registerV0_15_1UpgradeHandler(cfg) + app.setStoreLoaderForPendingV0_15_0Upgrade() +} -// List ALL new/renamed/deleted KV stores at this upgrade height. -var v053StoreUpgrades = storetypes.StoreUpgrades{ +// ============================================================================ +// PREVIOUS UPGRADE: v0.15.0 ("v0.15.0") +// ============================================================================ +// +// Everything in this section belongs exclusively to the previous v0.15.0 +// upgrade. It is kept ONLY so that this binary can still perform that +// upgrade if its plan is pending on some node (or during a re-run of it). +// +// >>> CLEANUP CHECKLIST — for the upgrade AFTER v0.15.1: <<< +// Once v0.15.1 has been applied on-chain, the v0.15.0 plan can never be +// pending again, and this whole section can be deleted. That means, in the +// NEXT release (the one after the v0.15.1 release): +// +// 1. Delete the entire "PREVIOUS UPGRADE: v0.15.0" section below +// (registerV0_15_0UpgradeHandler, v0_15_0StoreUpgrades, +// setStoreLoaderForPendingV0_15_0Upgrade, +// migrateConsensusParamsFromParamsStore, parseI64). +// 2. Delete the registerV0_15_0UpgradeHandler and +// setStoreLoaderForPendingV0_15_0Upgrade calls in +// RegisterUpgradeHandlers above. +// 3. Remove the now-unused imports (fmt, json, strconv, strings, time, +// math, storetypes, circuittypes, nft, tmproto, sdk, module's +// Configurator stays if still needed, consensusparamtypes, +// epochstypes, group, paramstypes, protocolpooltypes, stakingtypes, +// liquidtypes, ica* / ibc* types, ica_migration, traces, tokenfactory +// imports — let the compiler/gopls prune them). +// 4. At that point, keep the (still registered) empty v0.15.1 handler +// as the "previous upgrade" handler for one more release, per the +// convention of retaining the previous upgrade's handler. +// ============================================================================ + +// v0_15_0StoreUpgrades lists all new/renamed/deleted KV stores at the +// v0.15.0 upgrade height. +var v0_15_0StoreUpgrades = storetypes.StoreUpgrades{ Added: []string{ protocolpooltypes.StoreKey, circuittypes.StoreKey, @@ -63,34 +112,12 @@ var v053StoreUpgrades = storetypes.StoreUpgrades{ }, } -// Keep your existing wasmd upgrades -var Upgrades = []upgrades.Upgrade{v060.Upgrade} - -func (app *App) RegisterUpgradeHandlers(cfg module.Configurator) { - if len(Upgrades) == 0 { - Upgrades = append(Upgrades, noop.NewUpgrade(app.Version())) - } - - keepers := upgrades.AppKeepers{ - AccountKeeper: &app.AccountKeeper, - ConsensusParamsKeeper: &app.ConsensusParamsKeeper, - IBCKeeper: app.IBCKeeper, - Codec: app.appCodec, - GetStoreKey: app.GetKey, - } - app.GetStoreKeys() - - // 1) Existing wasmd handlers - for _, upgrade := range Upgrades { - app.UpgradeKeeper.SetUpgradeHandler( - upgrade.UpgradeName, - upgrade.CreateUpgradeHandler(app.mm, cfg, &keepers), - ) - } - - // 2) The v0.53 handler that runs per-module Migrators +// registerV0_15_0UpgradeHandler registers the v0.15.0 upgrade handler, +// which runs the per-module Migrators and the custom state migrations of +// the v0.15.0 upgrade. +func (app *App) registerV0_15_0UpgradeHandler(cfg module.Configurator) { app.UpgradeKeeper.SetUpgradeHandler( - UpgradeNameV053, + UpgradeNameV0_15_0, func(ctx context.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) { sdkCtx := sdk.UnwrapSDKContext(ctx) @@ -224,8 +251,20 @@ func (app *App) RegisterUpgradeHandlers(cfg module.Configurator) { return res, err }, ) +} - // 3) Load the correct store shape at the upgrade height +// setStoreLoaderForPendingV0_15_0Upgrade installs the custom store loader +// needed by the v0.15.0 upgrade (it mounts new KV stores). +// +// This must run during app construction — before LoadLatestVersion — and +// therefore CANNOT live inside the upgrade handler. It is a no-op unless +// the v0.15.0 plan is currently pending on disk (no upgrade-info.json file +// or a different plan name => default store loader is used). +// +// NOTE: this reads the pending plan from disk on every startup, but only +// actually overrides the store loader for the v0.15.0 plan. It belongs to +// the v0.15.0 section and is part of the cleanup checklist above. +func (app *App) setStoreLoaderForPendingV0_15_0Upgrade() { upgradeInfo, err := app.UpgradeKeeper.ReadUpgradeInfoFromDisk() if err != nil { panic(fmt.Sprintf("failed to read upgrade info from disk %s", err)) @@ -234,20 +273,15 @@ func (app *App) RegisterUpgradeHandlers(cfg module.Configurator) { return } - // Try wasmd-defined upgrades first - for _, u := range Upgrades { - if upgradeInfo.Name == u.UpgradeName { - app.SetStoreLoader(upgradetypes.UpgradeStoreLoader(upgradeInfo.Height, &u.StoreUpgrades)) - return - } - } - - // Then our v0.53 plan - if upgradeInfo.Name == UpgradeNameV053 { - app.SetStoreLoader(upgradetypes.UpgradeStoreLoader(upgradeInfo.Height, &v053StoreUpgrades)) + // The v0.15.0 plan needs the new stores mounted. + if upgradeInfo.Name == UpgradeNameV0_15_0 { + app.SetStoreLoader(upgradetypes.UpgradeStoreLoader(upgradeInfo.Height, &v0_15_0StoreUpgrades)) } } +// migrateConsensusParamsFromParamsStore migrates the consensus params from +// the legacy x/params store into the x/consensus module (part of the +// v0.15.0 upgrade). func migrateConsensusParamsFromParamsStore(app *App, ctx sdk.Context) error { paramsStore := ctx.KVStore(app.GetKey(paramstypes.StoreKey)) @@ -313,6 +347,33 @@ func migrateConsensusParamsFromParamsStore(app *App, ctx sdk.Context) error { return nil } +// parseI64 is a helper of migrateConsensusParamsFromParamsStore (v0.15.0). func parseI64(s string) (int64, error) { return strconv.ParseInt(strings.TrimSpace(s), 10, 64) } + +// ============================================================================ +// CURRENT UPCOMING UPGRADE: v0.15.1 ("v0.15.1") +// ============================================================================ +// +// The v0.15.1 upgrade requires no state migrations and no store shape +// changes (no added/renamed/deleted KV stores), hence: +// - the handler below is empty (no-op), and +// - no custom store loader is set up for it (the default one is used). +// +// Once v0.15.1 has been applied on-chain, keep this empty handler +// registered for one more release (as the "previous upgrade" handler), +// then it can be removed alongside the v0.15.0 cleanup described above. +// ============================================================================ + +// registerV0_15_1UpgradeHandler registers the empty (no-op) v0.15.1 +// upgrade handler. +func (app *App) registerV0_15_1UpgradeHandler(cfg module.Configurator) { + app.UpgradeKeeper.SetUpgradeHandler( + UpgradeNameV0_15_1, + func(ctx context.Context, _ upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) { + // No state migrations or store changes needed for this upgrade. + return fromVM, nil + }, + ) +} diff --git a/build-production-image-with-push.sh b/build-production-image-with-push.sh new file mode 100755 index 000000000..32ea8279a --- /dev/null +++ b/build-production-image-with-push.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Build the production images LOCALLY and push the resulting multi-platform +# images to their upstream registries. +# +# Sources build-production-image.sh so the build happens in THIS shell: +# - the multi-platform images land in the local Docker image store +# (containerd), from where a plain `docker push` uploads the full +# manifest list to the registry +# - the sourced script exports GIT_TAG / BUILT_TAGS, so this wrapper needs +# no duplicated git-metadata or tag-derivation logic +# +# No post-push pull-back is needed: the images are already present in the +# local Docker image store (they were built there and pushed from there). +# +# Usage: +# ./build-production-image-with-push.sh [default|musl] +# +# default -> glibc images: +# gcr.io/fetch-ai-images/fetchd:- (target: gcr, also :) +# fetchai/fetchd: (target: hub) +# musl -> static (musl) image: +# fetchai/fetchd:-musl (target: musl-hub) + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Mode for the sourced build script (it honors a pre-set MODE so the +# positional argument is not consumed twice). +MODE="${1:-default}" + +# --- Ensure a multi-platform-capable builder exists (and is bootstrapped) --- +# (a docker-container driver builder is required for multi-platform builds +# even when only loading locally) +docker buildx create --use --name multiarch-builder >/dev/null 2>&1 || true +docker buildx inspect --bootstrap + +# --- Build locally (all logic in build-production-image.sh) --- +# Sourced, not executed: on success this shell has GIT_TAG and BUILT_TAGS. +# The script's `set -e` propagates failures, and its `exit` calls abort this +# wrapper too, which is the desired behavior. +source "${SCRIPT_DIR}/build-production-image.sh" "${MODE}" + +# --- Push the locally built multi-platform images to their registries --- +for image in "${BUILT_TAGS[@]}"; do + echo "Pushing ${image}" + docker push "${image}" + echo "Pushed ${image}" + echo " (already present in the local Docker image store - pushed from there)" +done diff --git a/build-production-image.sh b/build-production-image.sh new file mode 100755 index 000000000..45e2180de --- /dev/null +++ b/build-production-image.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Registry image names (overridable via environment for testing/registries +# other than the production ones). +GCR_IMAGE="${GCR_IMAGE:-gcr.io/fetch-ai-images/fetchd}" +DOCKER_HUB_IMAGE="${DOCKER_HUB_IMAGE:-fetchai/fetchd}" + +# Build mode (first positional argument; or pre-set the MODE variable when +# sourcing this script, as build-production-image-with-push.sh does): +# default -> glibc images (gcr + hub), the original behavior +# musl -> static (musl) image (musl-hub), see musl.Dockerfile +# +# On success, BUILT_TAGS contains the full image:tag list produced (also +# available to sourcing scripts, since all of this runs in one shell). +MODE="${MODE:-${1:-default}}" + +# Git metadata, derived ONCE here and exported. When invoked as a child of +# build-production-image-with-push.sh, the wrapper exports these so +# both scripts use identical values (child-process env doesn't flow back to +# the parent, hence the override pattern instead of re-deriving in the +# wrapper). +export GIT_TAG="${GIT_TAG:-$(git describe --tags --abbrev=0)}" +export GIT_HASH="${GIT_HASH:-$(git rev-parse --short HEAD)}" +IMAGE_TAG="${GIT_TAG}-${GIT_HASH}" + +GODEBUG_VALUE="${GODEBUG:-}" + +case "${MODE}" in + default) + # Nothing mode-specific; the Apple Silicon workaround below is applied + # for both modes since both build linux/amd64 under emulation on arm64. + ;; + musl) + # Populate the Go proxy-layout module cache consumed by musl.Dockerfile + # (the static build resolves ALL modules, including the private ones, + # from it via GOPROXY=file:///modcache - no netrc secret involved). + # + # The cache is refreshed (rsync --delete) so a stale .modcache can never + # mask a go.mod/go.sum change: after an update, the differing zips are + # copied over and the build picks them up. + MODCACHE_SRC="$(go env GOMODCACHE)/cache/download" + if [[ ! -d "${MODCACHE_SRC}" ]]; then + echo "ERROR: module cache not found at ${MODCACHE_SRC}" >&2 + echo " Run 'go mod download' first to populate it." >&2 + exit 1 + fi + echo "Populating .modcache/ from ${MODCACHE_SRC} ..." + mkdir -p .modcache + rsync -a --delete "${MODCACHE_SRC}/" .modcache/ + ;; + *) + echo "ERROR: unknown mode '${MODE}' (expected 'default' or 'musl')" >&2 + echo "Usage: $0 [default|musl]" >&2 + exit 1 + ;; +esac + +# --- GitHub token for private repos (GOPRIVATE) --- +# Only needed for the glibc (default) mode: the musl build resolves all +# modules from .modcache/, so no authenticated VCS access is involved. +NETRC_CONTENT="" +if [[ "${MODE}" == "default" ]]; then + # Ask git for the SAME credential it uses to clone the private repos + # (osxkeychain / gh CLI / whatever credential.helper is configured). + # If local `git clone` / `go build` works, this is guaranteed to work too. + # GIT_ASKPASS=true prevents an interactive prompt if no credential is stored. + CRED=$(printf 'protocol=https\nhost=github.com\n\n' \ + | GIT_ASKPASS=true GIT_TERMINAL_PROMPT=0 git credential fill 2>/dev/null || true) + GITHUB_LOGIN=$(sed -n 's/^username=//p' <<< "${CRED}") + GITHUB_TOKEN=$(sed -n 's/^password=//p' <<< "${CRED}") + if [[ -z "${GITHUB_TOKEN}" && -x "$(command -v gh)" ]]; then + # gh CLI stored token (login name irrelevant; GitHub accepts any with PAT) + GITHUB_TOKEN=$(gh auth token 2>/dev/null || true) + fi + if [[ -z "${GITHUB_TOKEN}" ]]; then + echo "ERROR: no GitHub token found (tried git credential helper and 'gh auth token')" >&2 + echo " If local 'git clone https://github.com/fetchai/priv_wasmd_sec' works," >&2 + echo " the credential helper should provide it automatically." >&2 + exit 1 + fi + GITHUB_LOGIN="${GITHUB_LOGIN:-token}" + + NETRC_CONTENT=$(printf 'machine github.com\nlogin %s\npassword %s\n' "${GITHUB_LOGIN}" "${GITHUB_TOKEN}") +fi + +# Apple Silicon: building linux/amd64 runs under CPU emulation (QEMU/Rosetta), +# where the go toolchain intermittently crashes (SIGSEGV in +# runtime.suspendG) due to signal-based async preemption. Disable it. +if [[ "$(uname -s)" == "Darwin" && "$(uname -m)" == "arm64" ]]; then + GODEBUG_VALUE="${GODEBUG_VALUE:+${GODEBUG_VALUE},}asyncpreempt=0" +fi + +# --- Invoke buildx bake for the selected mode --- +# Bake variables (GCR_IMAGE, DOCKER_HUB_IMAGE, IMAGE_TAG, GIT_TAG, ...) are +# overridden via same-named environment variables. +# +# --load exports to the local Docker image store; the resulting multi-platform +# images (containerd image store) can later be pushed with plain `docker push`. +# NOTE: loading multi-platform images requires the containerd image store +# (Docker Desktop: Settings > General > "Use containerd for storing and +# managing images"). +case "${MODE}" in + default) + echo "Building (local only, no push):" + echo " ${GCR_IMAGE}:${IMAGE_TAG} (target: gcr)" + echo " ${GCR_IMAGE}:${GIT_TAG} (target: gcr, same image re-tagged)" + echo " ${DOCKER_HUB_IMAGE}:${GIT_TAG} (target: hub)" + echo " ${DOCKER_HUB_IMAGE}:${IMAGE_TAG} (target: hub, same image re-tagged)" + BUILT_TAGS=( + "${GCR_IMAGE}:${IMAGE_TAG}" + "${GCR_IMAGE}:${GIT_TAG}" + "${DOCKER_HUB_IMAGE}:${GIT_TAG}" + "${DOCKER_HUB_IMAGE}:${IMAGE_TAG}" + ) + ;; + musl) + echo "Building (local only, no push):" + echo " ${DOCKER_HUB_IMAGE}:${GIT_TAG}-musl (target: musl-hub, static binary)" + echo " ${DOCKER_HUB_IMAGE}:${IMAGE_TAG}-musl (same image, re-tagged with commit hash)" + echo " ${GCR_IMAGE}:${GIT_TAG}-musl (target: musl-gcr, static binary + extra entrypoints)" + echo " ${GCR_IMAGE}:${IMAGE_TAG}-musl (same image, re-tagged with commit hash)" + BUILT_TAGS=( + "${DOCKER_HUB_IMAGE}:${GIT_TAG}-musl" + "${DOCKER_HUB_IMAGE}:${IMAGE_TAG}-musl" + "${GCR_IMAGE}:${GIT_TAG}-musl" + "${GCR_IMAGE}:${IMAGE_TAG}-musl" + ) + ;; +esac +export BUILT_TAGS + +GCR_IMAGE="${GCR_IMAGE}" \ +DOCKER_HUB_IMAGE="${DOCKER_HUB_IMAGE}" \ +IMAGE_TAG="${IMAGE_TAG}" \ +GIT_TAG="${GIT_TAG}" \ +GODEBUG="${GODEBUG_VALUE}" \ +NETRC_CONTENT="${NETRC_CONTENT}" \ +docker buildx bake "${MODE}" \ + --load \ + -f docker-bake.hcl + +case "${MODE}" in + default) + echo "Built (local only, nothing was pushed):" + echo " ${GCR_IMAGE}:${IMAGE_TAG}" + echo " ${GCR_IMAGE}:${GIT_TAG}" + echo " ${DOCKER_HUB_IMAGE}:${GIT_TAG}" + ;; + musl) + echo "Built (local only, nothing was pushed):" + echo " ${DOCKER_HUB_IMAGE}:${GIT_TAG}-musl" + ;; +esac diff --git a/ci.Dockerfile b/ci.Dockerfile index c8bc9446a..e1bcfbf65 100644 --- a/ci.Dockerfile +++ b/ci.Dockerfile @@ -1,9 +1,15 @@ +# syntax=docker/dockerfile:1 FROM golang:1.25.7-bookworm WORKDIR /src COPY . . -RUN make go-mod-cache && \ +# The netrc secret (id=netrc) is provided by CI (docker build --secret) and holds +# a GitHub token with read access to the private go module dependencies. +# See .github/workflows/build_and_test.yml +RUN --mount=type=secret,id=netrc,target=/root/.netrc \ + GOPRIVATE=github.com/fetchai/priv_wasmd_sec,github.com/fetchai/priv_wasmvm_sec \ + make go-mod-cache && \ go mod download all && \ make build diff --git a/docker-bake.hcl b/docker-bake.hcl new file mode 100644 index 000000000..06fb1ab0d --- /dev/null +++ b/docker-bake.hcl @@ -0,0 +1,162 @@ +# Build all multi-platform image targets in one go with: +# +# docker buildx bake +# +# Targets: +# - gcr: gcr.io/fetch-ai-images/fetchd:- AND : (same image, two tags) +# - hub: fetchai/fetchd: +# - musl-hub: fetchai/fetchd:-musl (static binary, no wasmvm .so) +# +# The gcr target extends hub, so all layers up to `hub` are built and cached once. +# The musl-hub target is fully independent (different base images, different +# build path), so it is in its own group. +# +# Groups: +# docker buildx bake default -> gcr + hub (glibc images) +# docker buildx bake musl -> musl-hub (static musl image) + +variable "GCR_IMAGE" { + default = "gcr.io/fetch-ai-images/fetchd" +} + +variable "DOCKER_HUB_IMAGE" { + default = "fetchai/fetchd" +} + +variable "IMAGE_TAG" { + default = "" +} + +variable "GIT_TAG" { + default = "" +} + +# Optional Go runtime tweak for the builder stage, empty by default. Set to +# "asyncpreempt=0" in this (local, Apple Silicon) build workflow to avoid the +# go toolchain SIGSEGV under amd64 CPU emulation. +variable "GODEBUG" { + default = "" +} + +# --- Static (musl) image variables (see musl.Dockerfile) --- + +# Which Makefile static-install target to run in the builder stage: +# install-static : generic static build, no checksum pinning +# install-static-v0.15.1 : release-pinned build (asserts the wasmvm version +# and the static wasmvm archive SHA256) +variable "INSTALL_TARGET" { + default = "install-static-v0.15.1" +} + +# Ledger support in the static binary ("true"/"false"). Defaults to true, +# matching the ARG default in musl.Dockerfile. +variable "LEDGER_ENABLED" { + default = "true" +} + +# Tag suffix distinguishing the static image from the glibc ones. +variable "MUSL_TAG_SUFFIX" { + default = "-musl" +} + +group "default" { + targets = ["gcr", "hub"] +} + +group "musl" { + targets = ["musl-hub", "musl-gcr"] +} + +target "gcr" { + context = "." + dockerfile = "Dockerfile" + target = "gcr" + platforms = ["linux/amd64", "linux/arm64"] + # GitHub credentials for private repos (GOPRIVATE), sourced from the + # NETRC_CONTENT environment variable (avoids fs-read entitlement prompts). + # Mounted only into the `make install` step. + secret = ["id=netrc,env=NETRC_CONTENT"] + args = { + GODEBUG = "${GODEBUG}" + } + tags = [ + # Both tags point at the same image, so the gcr stage is built exactly once + "${GCR_IMAGE}:${IMAGE_TAG}", + "${GCR_IMAGE}:${GIT_TAG}", + ] +} + +target "hub" { + context = "." + dockerfile = "Dockerfile" + target = "hub" + platforms = ["linux/amd64", "linux/arm64"] + secret = ["id=netrc,env=NETRC_CONTENT"] + args = { + GODEBUG = "${GODEBUG}" + } + # Both tags point at the same image (git-tag-only and git-tag-commit-hash) + tags = [ + "${DOCKER_HUB_IMAGE}:${GIT_TAG}", + "${DOCKER_HUB_IMAGE}:${IMAGE_TAG}", + ] +} + +# Static (musl) images: the fetchd binary is statically linked against the +# wasmvm static archive, so no libwasmvm..so needs to be shipped. +# +# NOTE: no netrc secret here - the musl build resolves ALL modules (including +# the private ones) from the .modcache/ proxy-layout cache in the build +# context. Populate it first, e.g. via `./build-production-image.sh musl` +# (which does it automatically) or manually: +# cp -R "$(go env GOMODCACHE)/cache/download/." .modcache/ +# +# Two targets, mirroring the glibc scheme: +# - musl-hub: the `hub` docker stage (Docker Hub image) +# - musl-gcr: the `gcr` docker stage (GCR image, extra entrypoint scripts) +# The shared builder stage is built once and both final stages reuse it +# (same context/dockerfile/args => cached builder layers). +target "musl-hub" { + context = "." + dockerfile = "musl.Dockerfile" + target = "hub" + platforms = ["linux/amd64", "linux/arm64"] + # The Go module cache is passed as the named `modcache` build context (see + # musl.Dockerfile), keeping the ~5 GB cache out of the image layers and + # out of the main build context. + contexts = { + modcache = ".modcache" + } + args = { + GODEBUG = "${GODEBUG}" + INSTALL_TARGET = "${INSTALL_TARGET}" + LEDGER_ENABLED = "${LEDGER_ENABLED}" + } + # Both tag variants (git-tag-only and git-tag-commit-hash), mirroring the + # glibc hub target. Multiple tags = one build, all tags together. + tags = [ + "${DOCKER_HUB_IMAGE}:${GIT_TAG}${MUSL_TAG_SUFFIX}", + "${DOCKER_HUB_IMAGE}:${IMAGE_TAG}${MUSL_TAG_SUFFIX}", + ] +} + +target "musl-gcr" { + context = "." + dockerfile = "musl.Dockerfile" + target = "gcr" + platforms = ["linux/amd64", "linux/arm64"] + contexts = { + modcache = ".modcache" + } + args = { + GODEBUG = "${GODEBUG}" + INSTALL_TARGET = "${INSTALL_TARGET}" + LEDGER_ENABLED = "${LEDGER_ENABLED}" + } + # Both tag variants (git-tag-only and git-tag-commit-hash), mirroring the + # glibc gcr target. + tags = [ + "${GCR_IMAGE}:${GIT_TAG}${MUSL_TAG_SUFFIX}", + "${GCR_IMAGE}:${IMAGE_TAG}${MUSL_TAG_SUFFIX}", + ] +} diff --git a/go.mod b/go.mod index e3eb2a050..fed7c3ecf 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.25.7 require ( github.com/CosmWasm/wasmd v0.61.11 - github.com/CosmWasm/wasmvm/v3 v3.0.4 // indirect + github.com/CosmWasm/wasmvm/v3 v3.0.8-rc.2 // indirect github.com/cosmos/cosmos-proto v1.0.0-beta.5 // indirect github.com/cosmos/gogogateway v1.2.0 // indirect github.com/cosmos/gogoproto v1.7.2 @@ -271,3 +271,9 @@ replace github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alp replace github.com/cosmos/cosmos-sdk => github.com/fetchai/cosmos-sdk v0.20.0 replace cosmossdk.io/api => github.com/fetchai/cosmos-sdk/api v0.0.0-20260511202058-5a24a3348e83 + +replace ( + // Using rather Fetch.ai own private *clones* of the CosmWasm repositories: + github.com/CosmWasm/wasmd => github.com/fetchai/priv_wasmd_sec v0.61.15-rc.2 // git commit: 9220bb5678286bd418093e8b8d13fe0761bd3916 + github.com/CosmWasm/wasmvm/v3 => github.com/fetchai/priv_wasmvm_sec/v3 v3.0.8-rc.2 // git commit: 08323d45269cd6a3fd27863e1ffa65d30593dbf7 +) diff --git a/go.sum b/go.sum index e9ead98c1..a0fe72087 100644 --- a/go.sum +++ b/go.sum @@ -657,10 +657,6 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25 github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/CosmWasm/wasmd v0.61.11 h1:p7kAJACHHexQQSdoFgCvxa1pkbZotDEeAZCsyJhvFrE= -github.com/CosmWasm/wasmd v0.61.11/go.mod h1:pfuEzkWBQ24nVfMrMBNGHh5Gmr/RBk4kIdQGiIIV68A= -github.com/CosmWasm/wasmvm/v3 v3.0.4 h1:nccZU7jzH3bSMnHzXE7JjpSfeXayv6IUcpugdRVX5uc= -github.com/CosmWasm/wasmvm/v3 v3.0.4/go.mod h1:oknpb1bFERvvKcY7vHRp1F/Y/z66xVrsl7n9uWkOAlM= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/datadog-go v4.8.3+incompatible h1:fNGaYSuObuQb5nzeTQqowRAd9bpDIRRV4/gUtIBjh8Q= github.com/DataDog/datadog-go v4.8.3+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= @@ -940,6 +936,10 @@ github.com/fetchai/cosmos-sdk v0.20.0 h1:55YcldC89mXkTC/5jMWirmkVeIQAIvg+ro9pSg1 github.com/fetchai/cosmos-sdk v0.20.0/go.mod h1:JzUvl8EYwGynKCfnBAQ1MC5Za6Pecl9GTIxMBHo5TQg= github.com/fetchai/cosmos-sdk/api v0.0.0-20260511202058-5a24a3348e83 h1:9t3RKmMH/Ay+07CVfjNN7n/DQhx73HboWFG514jmVkU= github.com/fetchai/cosmos-sdk/api v0.0.0-20260511202058-5a24a3348e83/go.mod h1:e6faZ24J0TyOWdEv/QdE5RmQMzyCBTTz674ourTi0yU= +github.com/fetchai/priv_wasmd_sec v0.61.15-rc.2 h1:gT6KThovHpybMKQ3+cUCCcjNVDfjUaWXXRX6duuFdtM= +github.com/fetchai/priv_wasmd_sec v0.61.15-rc.2/go.mod h1:qBrVLabNUQBjPZVKdHZs+akf8Cz7rvE+l5oZ5A1dKOQ= +github.com/fetchai/priv_wasmvm_sec/v3 v3.0.8-rc.2 h1:YxCz61d571z8CQWHHbxw6Ygz7w5rHPPdq3kVQPN2BNg= +github.com/fetchai/priv_wasmvm_sec/v3 v3.0.8-rc.2/go.mod h1:oknpb1bFERvvKcY7vHRp1F/Y/z66xVrsl7n9uWkOAlM= github.com/fetchai/tokenfactory v0.1.0 h1:BY94cCNWNIL7bh86Mpe0+0WD5fbHiXZYFZM58CQqMrY= github.com/fetchai/tokenfactory v0.1.0/go.mod h1:6oaM728KRoZdncFhltJsYx8DtY1UaErb85T1gKTh9R0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= diff --git a/musl.Dockerfile b/musl.Dockerfile new file mode 100644 index 000000000..b7466e997 --- /dev/null +++ b/musl.Dockerfile @@ -0,0 +1,161 @@ +# syntax=docker/dockerfile:1 +# +# Static (musl) build of fetchd, producing a self-contained image that does NOT +# need the dynamic libwasmvm..so libraries shipped alongside the binary. +# +# Differences to the (glibc) Dockerfile: +# - Builder runs on Alpine (musl), not Debian (glibc). +# - fetchd is statically linked against the pre-built static wasmvm archive +# (libwasmvm_muslc..a) selected by the `muslc` build tag. +# - The runtime image is plain Alpine + the static binary: no .so copying. +# +# The build is fully self-contained w.r.t. Go module downloads: the host must +# provide a Go proxy-layout module cache (see the header of .dockerignore), +# passed as a NAMED BUILD CONTEXT so it is never copied into image layers: +# +# docker build -f musl.Dockerfile \ +# --build-context modcache=.modcache \ +# --target hub -t fetchai/fetchd:musl . +# +# (the musl-hub target in docker-bake.hcl wires this up automatically). This +# avoids the need for a GitHub token (netrc) at build time even though the +# module graph pulls in private repositories (github.com/fetchai/priv_wasmd_sec, +# github.com/fetchai/priv_wasmvm_sec). +# +# Populate the module cache on the host before building, e.g.: +# +# rm -rf .modcache && mkdir -p .modcache +# cp -R "$(go env GOMODCACHE)/cache/download/." .modcache/ +# +# (i.e. copy the *proxy layout* part of the Go module cache, the one rooted at +# GOMODCACHE/cache/download - that is what GOPROXY=file:///modcache expects.) +# +# Build (single platform): see the --build-context example above. +# +# Build (multi-platform, via buildx): +# +# docker buildx build --platform linux/amd64,linux/arm64 \ +# -f musl.Dockerfile --build-context modcache=.modcache \ +# --target hub -t fetchai/fetchd:musl --push . + +FROM golang:1.25.7-alpine3.23 AS builder + +# build-base: gcc + musl-dev + make (needed for the static cgo/wasmvm link) +# git: the Makefile derives VERSION/COMMIT via `git describe`/`git log` +# (the build context includes .git, so do not add a .git entry to +# .dockerignore for this image) +# xz: to decompress the static wasmvm archive +# linux-headers: Linux kernel UAPI headers, needed by the ledger support +# (github.com/zondax/hid compiles libusb sources that include +# etc.); only used when LEDGER_ENABLED=true, but +# installed unconditionally to keep the builder image uniform +RUN apk add --no-cache build-base git xz linux-headers + +# Serve all Go modules from the pre-populated proxy cache, falling back to the +# public proxy/direct only if something is missing. GOSUMDB is disabled since +# the private modules are not in the public checksum database; their integrity +# is still enforced by the entries in go.sum (and the wasmvm archive checksum +# verified explicitly below). GOPRIVATE must NOT be set here: it would make +# `go` bypass GOPROXY (the file:// cache) for the private modules and attempt +# authenticated VCS fetches instead. +ENV GOPROXY=file:///modcache,https://proxy.golang.org,direct +ENV GOSUMDB=off + +# Optional Go runtime setting, empty by default. Set via --build-arg only where +# needed, e.g. GODEBUG=asyncpreempt=0 when building linux/amd64 under CPU +# emulation (QEMU/Rosetta on Apple Silicon), where emulated signal delivery +# can crash the go toolchain (SIGSEGV in runtime.suspendG). On native builds +# (Linux CI) leave it unset. The ARG value is exposed as an environment +# variable to the RUN steps below; no ENV needed. +ARG GODEBUG="" + +WORKDIR /fetchd + +# The module cache is provided as the `modcache` named build context (see the +# header comment) and bind-mounted read-only at /modcache for the steps that +# need it; it is excluded from the main context via .dockerignore so +# `COPY . .` never copies ~5 GB into image layers. +# +# The Go module cache and build cache use BuildKit cache mounts, so the +# downloaded/extracted modules and compiled packages never bloat the builder +# image layers either (and persist across rebuilds on the same builder). +# The build cache is per-platform (the go build cache is content-keyed and +# arch-sensitive), the module cache is arch-independent and shared. +COPY go.mod go.sum ./ +RUN --mount=type=bind,from=modcache,source=/,target=/modcache \ + --mount=type=cache,target=/go/pkg/mod \ + go mod download + +# Extract the static (musl) wasmvm archive shipped inside the module that +# satisfies github.com/CosmWasm/wasmvm/v3 (following any `replace` directives +# in go.mod) into /lib, verify it, and build. All of that lives in the +# Makefile (see the static-wasmvm-lib / install-static* targets below), so +# the same flow can be run outside Docker (e.g. on a bare Alpine CI runner). + +# Optional: enable ledger support in the static build. Disabled by default: +# statically linking the ledger/HID USB stack on musl needs extra care, and +# the usual motivation for a static build is a minimal, reproducible runtime. +ARG LEDGER_ENABLED=true + +# Which Makefile static-install target to run: +# - install-static : generic static build, no archive checksum pinning +# - install-static-v0.15.1 : release build for v0.15.1, additionally asserts +# the wasmvm version and the SHA256 of the +# static wasmvm archive before building +# The extraction of libwasmvm_muslc..a from the module and the optional +# checksum verification live in the Makefile, so they can be reused outside +# Docker (e.g. on a bare Alpine CI runner) as well. +ARG INSTALL_TARGET=install-static-v0.15.1 + +COPY . . + +# Static build via the Makefile: the `muslc` build tag selects the static +# wasmvm cgo bindings (internal/api/link_muslc_*.go) instead of the dynamic +# .so ones, and the static external link keeps the Makefile's -buildmode=pie +# (musl fully supports static-PIE, unlike glibc). The Makefile's usual +# version ldflags (name, commit, tags, ...) are preserved. +# NOTE: ARG values are exposed as environment variables to RUN steps, hence +# the shell-style (not Make-style) $INSTALL_TARGET reference. +# The same cache mounts as the `go mod download` step above are used, so +# the modules and compiled packages persist across builds without landing in +# image layers. +RUN --mount=type=bind,from=modcache,source=/,target=/modcache,ro \ + --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + make ${INSTALL_TARGET} LEDGER_ENABLED=${LEDGER_ENABLED} + +# Sanity check: the binary must be statically linked (note that busybox ldd +# misleadingly prints the musl loader line even for static binaries, so use +# `file` instead). Static-PIE is expected, since the Makefile forces PIE and +# musl supports static-PIE. +RUN file /go/bin/fetchd | grep -E "static-pie linked|statically linked" + +# ################################## + +FROM alpine:3.23 AS hub + +# Runtime dependencies of entrypoint.sh (bash + curl + jq; sed is in busybox) +RUN apk add --no-cache bash jq curl + +COPY --from=builder /go/bin/fetchd /usr/bin/fetchd +COPY entrypoints/entrypoint.sh /usr/bin/entrypoint.sh + +VOLUME /root/.fetchd +VOLUME /root/secret-temp-config + +WORKDIR /root + +ENTRYPOINT [ "/usr/bin/entrypoint.sh" ] +EXPOSE 1317 +EXPOSE 26656 +EXPOSE 26657 +STOPSIGNAL SIGTERM + +# ################################## + +# Same as hub, plus the extra entrypoint scripts used by internal (GCR) +# deployments - mirrors the gcr stage of the (glibc) Dockerfile. +FROM hub AS gcr + +COPY ./entrypoints/run-node.sh /usr/bin/run-node.sh +COPY ./entrypoints/run-tx-server.sh /usr/bin/run-tx-server.sh