diff --git a/.github/workflows/genesis-build-smoke.yml b/.github/workflows/genesis-build-smoke.yml new file mode 100644 index 00000000..d68947f4 --- /dev/null +++ b/.github/workflows/genesis-build-smoke.yml @@ -0,0 +1,82 @@ +name: Genesis Build Smoke + +# Runs the release-time genesis build on PRs, with the same secrets and the same command, +# so a bad secret or a broken pipeline fails here instead of on master when a release is +# cut. The extractor's unit tests and `bash -n` cannot catch either — v0.5.6-rc1 died on a +# key `cast` could not decode, after every PR check had passed. +# +# Skipped on fork PRs: secrets are withheld there. +on: + pull_request: + branches: [master] + paths: + - "scripts/genesis/**" + - "scripts/deploy/**" + - "contracts/**" + - "**.sol" + - "deployments/**" + - "foundry.toml" + - ".github/workflows/publish-release.yml" + - ".github/workflows/publish-prerelease.yml" + - ".github/workflows/genesis-build-smoke.yml" + - ".github/actions/setup-foundry/action.yml" + push: + branches: [master] + paths: + - "scripts/genesis/**" + - ".github/workflows/genesis-build-smoke.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: genesis-smoke-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + genesis-build-smoke: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - uses: ./.github/actions/setup-foundry + + - uses: actions/setup-node@v4 + with: + node-version: "20" + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.2.6" + no-cache: true + + # Not optional: bun's postinstall runs setup.bash, which lands lib/ dependencies on + # their pinned SHAs. Without it the build compiles against stale OpenZeppelin and + # fails on override/mutability mismatches — the release workflow always runs this. + - run: bun install + + # The same secrets and command as the release step. The parity check inside the + # script runs for real: with the real factory key the addresses must equal the + # committed manifest, so nothing is skipped or stubbed. + - name: Build the genesis exactly as a release does + env: + FOUNDRY_DISABLE_NIGHTLY_WARNING: "1" + FACTORY_DEPLOYER_KEY: ${{ secrets.FACTORY_DEPLOYER_KEY }} + DOTNS_ADMIN_KEY: ${{ secrets.DOTNS_ADMIN_KEY }} + DOTNS_TLD: test + run: bash scripts/genesis/build-genesis.sh release + + - name: Assert the artifact shape + run: | + test -s release/dotns-genesis-test.json + jq -e '.tld == "test" and (.accounts | length > 0)' release/dotns-genesis-test.json >/dev/null + echo "accounts: $(jq '.accounts | length' release/dotns-genesis-test.json)" + + - uses: actions/upload-artifact@v4 + with: + name: dotns-genesis-test + path: release/dotns-genesis-test.json diff --git a/.github/workflows/publish-prerelease.yml b/.github/workflows/publish-prerelease.yml index 3fe36756..39460ca6 100644 --- a/.github/workflows/publish-prerelease.yml +++ b/.github/workflows/publish-prerelease.yml @@ -123,7 +123,6 @@ jobs: FOUNDRY_DISABLE_NIGHTLY_WARNING: "1" FACTORY_DEPLOYER_KEY: ${{ secrets.FACTORY_DEPLOYER_KEY }} DOTNS_ADMIN_KEY: ${{ secrets.DOTNS_ADMIN_KEY }} - DOTNS_ADMIN_MNEMONIC: ${{ secrets.DOTNS_ADMIN_MNEMONIC }} run: bash scripts/genesis/build-genesis.sh release - name: Extract ABIs diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 265523df..4a8cc25a 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -113,15 +113,13 @@ jobs: # contracts, the deploy scripts and the CREATE3 factory key that fixes every address, # and the artifact then ships from the same commit as the ABIs beside it. # - # The owner key is taken from DOTNS_ADMIN_KEY if set, otherwise derived from - # DOTNS_ADMIN_MNEMONIC. It ends up owning the registry, resolvers, registrar, store factory - # and beacons in the genesis storage, so the script refuses to run without one. + # DOTNS_ADMIN_KEY (a raw private key, the only accepted credential) ends up owning the + # registry, resolvers, registrar, store factory and beacons in the genesis storage. - name: Build pallet-revive genesis env: FOUNDRY_DISABLE_NIGHTLY_WARNING: "1" FACTORY_DEPLOYER_KEY: ${{ secrets.FACTORY_DEPLOYER_KEY }} DOTNS_ADMIN_KEY: ${{ secrets.DOTNS_ADMIN_KEY }} - DOTNS_ADMIN_MNEMONIC: ${{ secrets.DOTNS_ADMIN_MNEMONIC }} run: bash scripts/genesis/build-genesis.sh release - name: Extract ABIs diff --git a/scripts/genesis/build-genesis.sh b/scripts/genesis/build-genesis.sh index 8bde4c5c..d86f162b 100755 --- a/scripts/genesis/build-genesis.sh +++ b/scripts/genesis/build-genesis.sh @@ -37,23 +37,16 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DEPLOYMENT_FILE="deployments/localhost/31337.json" CANONICAL_MANIFEST="deployments/paseo-assethub/420420417.json" -# Who OWNS the contracts in the genesis state (REQUIRED, one of the three below). +# Who OWNS the contracts in the genesis state: DOTNS_ADMIN_KEY, a raw private key, +# REQUIRED. No DotNS *address* depends on it — with CREATE3 the addresses are a pure +# function of the factory — but every ownership and role assignment written into +# genesis storage does. # -# No DotNS *address* depends on this key — with CREATE3 the addresses are a pure -# function of the factory (see FACTORY_DEPLOYER_KEY below, which owns only the -# factory) — but every ownership and role assignment written into genesis storage -# does: this key ends up owning the registry, the resolvers, the registrar, the -# store factory and the beacons. -# -# Accepted, in order of precedence: -# DOTNS_ADMIN_KEY a raw private key — the admin credential this repo already holds -# DOTNS_ADMIN_MNEMONIC the admin mnemonic; index $DOTNS_ADMIN_INDEX (default 0) -# -# Deliberately NOT accepted: DOTNS_MNEMONIC. That is the operational credential the -# whitelist workflows drive the `dotns` CLI with, not the contract admin, and quietly -# making it the owner of every contract in a genesis would be a hard mistake to spot. -# Not DEPLOYER_KEY: that name is dotns-releases' own secret, and accepting it here -# would make which key owns a published genesis depend on which repo the build ran in. +# The ONLY accepted credential. No mnemonic, no fallback: with two credentials the +# build would guess which account owns everything, and nothing downstream would +# notice a wrong guess — the parity check validates addresses, not owners. +# Not DOTNS_MNEMONIC (the whitelist workflows' operational credential) and not +# DEPLOYER_KEY (dotns-releases' own secret) for the same reason. ADMIN_KEY="${DOTNS_ADMIN_KEY:-}" # Single-purpose CREATE3 factory deployer key (REQUIRED). Every DotNS address is @@ -90,18 +83,12 @@ for tool in forge anvil cast node jq curl; do command -v "$tool" >/dev/null 2>&1 || { echo "Error: $tool is not on PATH" >&2; exit 1; } done -# Needs cast, so it happens after the check above. -if [ -z "$ADMIN_KEY" ] && [ -n "${DOTNS_ADMIN_MNEMONIC:-}" ]; then - ADMIN_KEY="$(cast wallet private-key --mnemonic "$DOTNS_ADMIN_MNEMONIC" "${DOTNS_ADMIN_INDEX:-0}")" - echo "Owner key derived from DOTNS_ADMIN_MNEMONIC, index ${DOTNS_ADMIN_INDEX:-0}." -fi - if [ -z "$ADMIN_KEY" ]; then cat >&2 <<'MSG' -Error: no owner key. Set DOTNS_ADMIN_KEY or DOTNS_ADMIN_MNEMONIC. +Error: DOTNS_ADMIN_KEY is required. -Whichever is given becomes the owner of every DotNS contract in the genesis -state, so this build refuses to fall back to a public dev key. +It becomes the owner of every DotNS contract in the genesis state, so this +build refuses to fall back to a public dev key or any other credential. MSG exit 1 fi @@ -116,6 +103,30 @@ MSG exit 1 fi +# ---- Key shape preflight ---- +# `cast` rejects a malformed key with a bare "Failed to decode private key" — no variable +# name, no shape — and a GitHub secret cannot be read back to inspect. Describe the problem +# (length, word count) without ever printing the value. Surrounding whitespace is stripped: +# a pasted trailing newline is the classic way a secret breaks. +require_hex_key() { + local name="$1" raw="${2-}" + if printf '%s' "$raw" | tr -d '[:space:]' | grep -Eq '^(0x)?[0-9a-fA-F]{64}$'; then + return 0 + fi + local words; words=$(printf '%s' "$raw" | wc -w | tr -d ' ') + echo "Error: $name is not a raw private key (need 64 hex chars, 0x optional):" >&2 + echo " got ${#raw} char(s), $words word(s)." >&2 + if [ "$words" -ge 12 ]; then + echo " That shape is a mnemonic. This build takes only a key — derive one with" >&2 + echo " \`cast wallet private-key --mnemonic '<...>'\` and store the result." >&2 + fi + exit 1 +} +require_hex_key DOTNS_ADMIN_KEY "$ADMIN_KEY" +ADMIN_KEY="$(printf '%s' "$ADMIN_KEY" | tr -d '[:space:]')" +require_hex_key FACTORY_DEPLOYER_KEY "$FACTORY_DEPLOYER_KEY" +FACTORY_DEPLOYER_KEY="$(printf '%s' "$FACTORY_DEPLOYER_KEY" | tr -d '[:space:]')" + DEPLOYER_ADDR="$(cast wallet address --private-key "$ADMIN_KEY")" export WHITELIST_OPERATOR="${WHITELIST_OPERATOR:-$DEPLOYER_ADDR}" echo "Contract owner: $DEPLOYER_ADDR"