diff --git a/Makefile b/Makefile index 981160e..f1dbcdf 100644 --- a/Makefile +++ b/Makefile @@ -68,6 +68,25 @@ define require-chain-config exit 1; } endef +# Recipe-time guard for the targets that FORK the chain: refuse a non-EVM chainFamily before forge is +# handed an endpoint. Without it, `make deploy-token CHAIN=` sends eth_chainId to a +# Solana node and dies on `error code -32601: Method not found` - a transport-looking failure for a +# config-level answer. The other family-sensitive targets (add-lane, snapshot-chain, sync, doctor, +# probe-chain, verify-args, detect-evm-version) already answer by family in their own script. +# $(1) = chain name, $(2) = where the operator should go instead. +# Only an EXPLICIT non-evm declaration fires: chain-family.sh reads an absent or unparseable +# declaration as `evm`, so a broken config is still reported by the target that owns it. +define require-evm-chain + @fam="$$(bash script/config/chain-family.sh "$(1)")"; \ + test "$$fam" = "evm" || { \ + echo "$@: $(1) is chainFamily '$$fam' - this target is EVM-only."; \ + echo "$(2)"; \ + exit 1; } +endef + +# Non-EVM chains are destination-only here (docs/config-schema.md, "Non-EVM (Solana) chain file"). +NON_EVM_DEPLOY_HINT = Deploy it with that chain's own tooling, then record it: make adopt-token CHAIN=$(CHAIN) TOKEN_B58= [POOL_B58=] + # Canonical JSON format for config/chains/*.json: `jq --indent 2 -S .` (2-space indent, sorted keys, # trailing newline — jq always emits one). The committed files use this exact style, and every target # that writes a config re-canonicalizes it as its last step, so a no-drift `make sync` produces ZERO @@ -90,11 +109,24 @@ help: ## List the available targets @echo "Chain-config tooling golden path (raw commands: README.md > Chain config tooling):" @awk 'BEGIN {FS = ":.*## "} /^[a-z][a-z-]*:.*## / {printf " %-16s %s\n", $$1, $$2}' $(MAKEFILE_LIST) -tools: ## Check the required tools are installed (forge, curl, jq) +# The chain this invocation targets, if any: CHAIN for the single-chain targets, LOCAL for the lane +# targets. Recursive (`=`) so it expands when the recipe runs. +TOOLS_CHAIN = $(or $(CHAIN),$(LOCAL)) + +tools: ## Check the required tools are installed (forge, curl, jq; CHAIN= adds that chain's family-specific check) @command -v forge > /dev/null || { echo "missing: forge - install Foundry: https://book.getfoundry.sh/getting-started/installation"; exit 2; } @command -v curl > /dev/null || { echo "missing: curl - install it (usually preinstalled; else brew install curl / apt install curl)"; exit 2; } @command -v jq > /dev/null || { echo "missing: jq - install it (e.g. brew install jq / apt install jq)"; exit 2; } @echo "tools: forge, curl and jq are all present" + @# forge/curl/jq are the baseline for every family - the config tooling itself is forge-based and + @# still runs against a non-EVM chain (sync SKIPs, doctor checks the schema). Anything + @# family-specific hangs off the chain argument, so an EVM-only user never sees another family's + @# stack, and never pays for the lookup. svm needs nothing extra TODAY: this repo has no non-EVM + @# write path, and the Solana CLI the docs mention is advisory, never installed or invoked here. + @test -z "$(TOOLS_CHAIN)" || { \ + fam="$$(bash script/config/chain-family.sh "$(TOOLS_CHAIN)")"; \ + test "$$fam" = "evm" || \ + echo "tools: $(TOOLS_CHAIN) is chainFamily '$$fam' - destination-only here, no extra toolchain required (docs/config-schema.md)"; } discover: tools ## List the CCIP API chain catalog vs local configs, both planes (FILTER= narrows; ENVIRONMENT= narrows the plane) @FILTER="$(FILTER)" ENVIRONMENT="$(ENVIRONMENT)" bash script/config/sync-discover.sh @@ -164,6 +196,7 @@ ifdef TOKEN_B58 FOUNDRY_PROFILE=sync PROJECT_GROUP="$(GROUP)" forge script script/config/AdoptToken.s.sol $(call evm-version-flag,$(CHAIN)) --sig "runNonEvm(string,string,string)" "$(CHAIN)" "$(TOKEN_B58)" "$(POOL_B58)" else $(if $(TOKEN),,$(error TOKEN is required - the externally deployed token address to adopt (or TOKEN_B58 for a non-EVM chain))) + $(call require-evm-chain,$(CHAIN),TOKEN= is an EVM address - pass this family's own form: make adopt-token CHAIN=$(CHAIN) TOKEN_B58= [POOL_B58=]) @FOUNDRY_PROFILE=sync PROJECT_GROUP="$(GROUP)" bash script/config/forge-fork.sh "$(CHAIN)" -- forge script script/config/AdoptToken.s.sol $(call evm-version-flag,$(CHAIN)) --sig "run(string,address,address)" "$(CHAIN)" "$(TOKEN)" "$(or $(TOKEN_POOL),0x0000000000000000000000000000000000000000)" endif @@ -290,21 +323,25 @@ endef deploy-token: tools ## Deploy a cross-chain token on (CHAIN= + KEYSTORE_NAME= required; token params via env TOKEN_NAME= TOKEN_SYMBOL= ...; VERIFY=1 source-verifies; FORCE_REDEPLOY=1 overrides the redeploy guard; GROUP= scopes to a token group) $(if $(CHAIN),,$(error CHAIN is required: make deploy-token CHAIN= (token params via env: TOKEN_NAME= TOKEN_SYMBOL= TOKEN_DECIMALS= ...))) $(require-chain-config) + $(call require-evm-chain,$(CHAIN),$(NON_EVM_DEPLOY_HINT)) $(call run-deploy,script/deploy/DeployToken.s.sol) deploy-pool: tools ## Deploy a BurnMint token pool on (CHAIN= + KEYSTORE_NAME= required; token resolved from the registry, else TOKEN=; opt POOL_HOOKS=; VERIFY=1; FORCE_REDEPLOY=1; GROUP= scopes to a token group) $(if $(CHAIN),,$(error CHAIN is required: make deploy-pool CHAIN=)) $(require-chain-config) + $(call require-evm-chain,$(CHAIN),$(NON_EVM_DEPLOY_HINT)) $(call run-deploy,script/deploy/DeployBurnMintTokenPool.s.sol) deploy-lockbox: tools ## Deploy an ERC20 LockBox on for the LockRelease liquidity model (CHAIN= + KEYSTORE_NAME= required; token from the registry, else TOKEN=; opt AUTHORIZED_CALLERS=; VERIFY=1; GROUP= scopes to a token group) $(if $(CHAIN),,$(error CHAIN is required: make deploy-lockbox CHAIN=)) $(require-chain-config) + $(call require-evm-chain,$(CHAIN),$(NON_EVM_DEPLOY_HINT)) $(call run-deploy,script/deploy/DeployERC20LockBox.s.sol) deploy-lockrelease-pool: tools ## Deploy a LockRelease token pool on (CHAIN= + KEYSTORE_NAME= required; token + lock box from the registry, else TOKEN= LOCK_BOX=; opt POOL_HOOKS=; VERIFY=1; FORCE_REDEPLOY=1; GROUP= scopes to a token group) $(if $(CHAIN),,$(error CHAIN is required: make deploy-lockrelease-pool CHAIN=)) $(require-chain-config) + $(call require-evm-chain,$(CHAIN),$(NON_EVM_DEPLOY_HINT)) $(call run-deploy,script/deploy/DeployLockReleaseTokenPool.s.sol) deploy-new-chain: tools ## Guided deploy: add-chain -> deploy-token -> deploy-pool -> doctor (CHAIN= SELECTOR= + KEYSTORE_NAME= required; token params + VERIFY= via env). Register, set-pool, and wire-lane come next - see docs/workflows/greenfield-deploy.md; a green run means deployed, not yet cross-chain-live diff --git a/docs/config-schema.md b/docs/config-schema.md index a5bda52..4f71810 100644 --- a/docs/config-schema.md +++ b/docs/config-schema.md @@ -728,6 +728,13 @@ source). `config/chains/solana-devnet.json` keeps the same shape but: from the doctor's reciprocity rung), but `make add-lane LOCAL=solana-devnet ...` is refused (no lanes to write). Its `addresses{}` subtree DOES apply, holding the Solana token/pool as **base58 strings** (see below). +- **The EVM-only make targets refuse a non-EVM chain by family.** `deploy-token`, `deploy-pool`, + `deploy-lockbox`, `deploy-lockrelease-pool` (and `deploy-new-chain` through them) and `adopt-token + TOKEN=` name the family and point at the `adopt-token` path instead of handing forge an endpoint that + speaks no EVM JSON-RPC. The read paths are unaffected: `sync` and `detect-evm-version` SKIP, + `roles-check` SKIPs, `doctor` reports the unverifiable rungs, `probe-chain` and `verify-args` refuse + by family with their own message. `make tools CHAIN=` names the family and asks for + nothing extra to install, because this repo has no non-EVM write path. - **A non-EVM token/pool is stored as base58 and feeds `applyChainUpdates` from the store.** Adopt it with `make adopt-token CHAIN= TOKEN_B58= [POOL_B58=]` (the `runNonEvm` path), which family-validates each value (base58 decodes to exactly 32 bytes) and writes diff --git a/docs/primitives/catalog.json b/docs/primitives/catalog.json index 3ee1167..7178ff6 100644 --- a/docs/primitives/catalog.json +++ b/docs/primitives/catalog.json @@ -1019,7 +1019,7 @@ }, { "target": "tools", - "help": "Check the required tools are installed (forge, curl, jq)" + "help": "Check the required tools are installed (forge, curl, jq; CHAIN= adds that chain's family-specific check)" }, { "target": "discover", diff --git a/script/config/chain-family.sh b/script/config/chain-family.sh new file mode 100755 index 0000000..34eb00e --- /dev/null +++ b/script/config/chain-family.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# chain-family.sh : print the chain's declared chainFamily, lowercased and trimmed. +# +# Fails OPEN to `evm` when the chain is unknown, declares no family, or the file does not parse. +# Callers use this to REFUSE non-EVM work, so an unreadable file must never turn a working EVM path +# into a refusal; missing-config and schema errors belong to the targets that own them. +set -uo pipefail +cd "$(dirname "$0")/../.." + +name="${1:-}" +file="config/chains/${name}.json" +if [ -z "$name" ] || [ ! -r "$file" ]; then + printf 'evm\n' + exit 0 +fi + +family="$(jq -r 'if (.chainFamily | type) == "string" then (.chainFamily | ascii_downcase | gsub("^\\s+|\\s+$"; "")) else empty end' "$file" 2> /dev/null)" +printf '%s\n' "${family:-evm}" diff --git a/script/config/roles-check.sh b/script/config/roles-check.sh index fe7a3c9..aee6e1f 100755 --- a/script/config/roles-check.sh +++ b/script/config/roles-check.sh @@ -6,7 +6,8 @@ # with its group. With no chain args it checks every EVM chain whose group project store # (project/[/].json) DECLARES a roles{} block (chains without one are listed as SKIP — # bootstrap them with `make snapshot-chain CHAIN=`). With explicit chain args the run stays in -# the selected (or default) group — re-run per group to cover siblings. Chain family comes from +# the selected (or default) group — re-run per group to cover siblings — and a named non-EVM chain +# SKIPs, same as in the sweep. Chain family comes from # config/chains; the declared roles{} lives in the project store. Classifies the forge # output into the CI-ready exit-code contract (the contract belongs to THIS SCRIPT — GNU make remaps any # recipe failure to exit 2, so `make roles-check` is pass/fail only; CI calls the script directly, the @@ -85,8 +86,17 @@ fi # Build the (group, chain) work list. Explicit chain args stay in the requested (or default) group; with # no args, discover every chain that DECLARES roles{} in each scanned group. declare -a pair_group=() pair_chain=() +skipped=0 if [ "$#" -gt 0 ]; then for name in "$@"; do + # Same SKIP the no-args sweep has always applied: roles{} is an EVM authority surface, and a + # read path reports an absent surface as SKIP, never as drift. Naming the chain used to end + # in a forge revert classified as ROLES_DRIFT. + if [ "$(bash script/config/chain-family.sh "$name")" != "evm" ]; then + echo ">> roles-check [group: $(group_label "$requested_group")] $name: SKIP (non-EVM)" + skipped=$((skipped + 1)) + continue + fi pair_group+=("$requested_group") pair_chain+=("$name") done @@ -96,7 +106,7 @@ else name="$(basename "$f" .json)" # Skip the gitignored zz-scratch-* files the test suites write here (fake selectors). case "$name" in zz-scratch-*) continue ;; esac - if [ "$(jq -r '.chainFamily' "$f")" != "evm" ]; then + if [ "$(bash script/config/chain-family.sh "$name")" != "evm" ]; then echo ">> roles-check [group: $(group_label "$g")] $name: SKIP (non-EVM)" continue fi @@ -114,7 +124,11 @@ else fi if [ ${#pair_chain[@]} -eq 0 ]; then - echo "roles-check: CLEAN - no chain declares a roles{} block yet (bootstrap with make snapshot-chain)" + if [ "$skipped" -gt 0 ]; then + echo "roles-check: CLEAN - nothing to reconcile (every named chain SKIPped)" + else + echo "roles-check: CLEAN - no chain declares a roles{} block yet (bootstrap with make snapshot-chain)" + fi exit 0 fi diff --git a/script/config/test-tooling.sh b/script/config/test-tooling.sh index 97bba60..52bcb75 100755 --- a/script/config/test-tooling.sh +++ b/script/config/test-tooling.sh @@ -2879,6 +2879,201 @@ JSON fi fi +# ---------------------------------------------------------------- Makefile family guards (offline) +# 33. The targets that FORK a chain refuse a non-EVM chainFamily before forge is handed an endpoint. +# Before this, `make deploy-token CHAIN=` with the RPC set sent eth_chainId to a +# Solana node and died on `error code -32601: Method not found`. +# Every case binds its own fixture and asserts an exit code, so deleting a guard fails a case. +# deploy-new-chain is covered transitively: it is a $(MAKE) chain through deploy-token. +if offline_enabled; then + FAM_SVM="zz-scratch-fam-svm" + FAM_EVM="zz-scratch-fam-evm" + FAM_NOKEY="zz-scratch-fam-nokey" + FAM_BROKEN="zz-scratch-fam-broken" + FAM_APTOS="zz-scratch-fam-aptos" + FAM_UPPER="zz-scratch-fam-upper" + FAM_PAD="zz-scratch-fam-pad" + jq --indent 2 -S '.name = "'"$FAM_SVM"'" | .chainNameIdentifier = "ZZ_SCRATCH_FAM_SVM" + | .rpcEnv = "ZZ_SCRATCH_FAM_SVM_RPC_URL" | .chainSelector = "9932100000000000021"' \ + config/chains/solana-devnet.json > "config/chains/$FAM_SVM.json" + jq --indent 2 -S '.name = "'"$FAM_EVM"'" | .chainNameIdentifier = "ZZ_SCRATCH_FAM_EVM" + | .rpcEnv = "ZZ_SCRATCH_FAM_EVM_RPC_URL" | .chainSelector = "9932100000000000022"' \ + config/chains/ethereum-testnet-sepolia.json > "config/chains/$FAM_EVM.json" + # A config that declares NO chainFamily: the resolver reads absence as evm, so a schema gap never + # becomes a family refusal - it stays the doctor's schema rung to report. + jq --indent 2 -S 'del(.chainFamily) | .name = "'"$FAM_NOKEY"'" + | .chainNameIdentifier = "ZZ_SCRATCH_FAM_NOKEY" | .rpcEnv = "ZZ_SCRATCH_FAM_NOKEY_RPC_URL" + | .chainSelector = "9932100000000000023"' \ + config/chains/ethereum-testnet-sepolia.json > "config/chains/$FAM_NOKEY.json" + # A family that is neither evm nor svm: the guard must key on "is it evm", not "is it svm". + # aptos ships in config/chains and ChainHandlers supports it. + jq --indent 2 -S '.name = "'"$FAM_APTOS"'" | .chainNameIdentifier = "ZZ_SCRATCH_FAM_APTOS" + | .rpcEnv = "ZZ_SCRATCH_FAM_APTOS_RPC_URL" | .chainSelector = "9932100000000000024"' \ + config/chains/aptos-testnet.json > "config/chains/$FAM_APTOS.json" + # Declarations the resolver must normalize: a refusal here would be fail-CLOSED, blocking an EVM + # chain whose path works. One fixture per transform so dropping either is named. + jq --indent 2 -S '.chainFamily = "EVM" | .name = "'"$FAM_UPPER"'" + | .chainNameIdentifier = "ZZ_SCRATCH_FAM_UPPER" | .rpcEnv = "ZZ_SCRATCH_FAM_UPPER_RPC_URL" + | .chainSelector = "9932100000000000025"' \ + config/chains/ethereum-testnet-sepolia.json > "config/chains/$FAM_UPPER.json" + jq --indent 2 -S '.chainFamily = " evm\t" | .name = "'"$FAM_PAD"'" + | .chainNameIdentifier = "ZZ_SCRATCH_FAM_PAD" | .rpcEnv = "ZZ_SCRATCH_FAM_PAD_RPC_URL" + | .chainSelector = "9932100000000000026"' \ + config/chains/ethereum-testnet-sepolia.json > "config/chains/$FAM_PAD.json" + + # 33a. chain-family.sh - the one resolver every caller uses, including its fail-open cases. + printf '{not json' > "config/chains/$FAM_BROKEN.json" + fam_ok=1 + fam_detail="" + for pair in "$FAM_SVM:svm" "$FAM_APTOS:aptos" "$FAM_EVM:evm" "$FAM_UPPER:evm" "$FAM_PAD:evm" \ + "$FAM_NOKEY:evm" "$FAM_BROKEN:evm" "doesnotexist:evm" ":evm"; do + got="$(bash script/config/chain-family.sh "${pair%%:*}")" + [ "$got" = "${pair##*:}" ] || { fam_ok=0; fam_detail="$fam_detail ${pair%%:*}=>$got"; } + done + rm_fixture_config "config/chains/$FAM_BROKEN.json" + if [ $fam_ok -eq 1 ]; then + pass=$((pass + 1)) + echo "[PASS] chain-family.sh: svm and aptos read through; case and padding normalize; unknown, absent and unparseable fail open to evm" + else + fail=$((fail + 1)) + failures+=("chain-family.sh resolver") + echo "[FAIL] chain-family.sh resolver ($fam_detail)" + fi + + # 33b. the deploy path, with the RPC and keystore BOUND so the guard is demonstrably what stops + # the run - without it this reaches forge and dies on the endpoint instead. + run_case "deploy-token refuses a non-EVM chain by family" nonzero \ + "deploy-token: $FAM_SVM is chainFamily 'svm' - this target is EVM-only" -- \ + env ZZ_SCRATCH_FAM_SVM_RPC_URL="http://127.0.0.1:1" KEYSTORE_NAME="zz-scratch-fam-none" \ + make deploy-token CHAIN="$FAM_SVM" + run_case "deploy-token's refusal names the adopt path" nonzero \ + "make adopt-token CHAIN=$FAM_SVM TOKEN_B58=" -- \ + env ZZ_SCRATCH_FAM_SVM_RPC_URL="http://127.0.0.1:1" KEYSTORE_NAME="zz-scratch-fam-none" \ + make deploy-token CHAIN="$FAM_SVM" + run_case "deploy-pool refuses a non-EVM chain by family" nonzero \ + "deploy-pool: $FAM_SVM is chainFamily 'svm' - this target is EVM-only" -- \ + env ZZ_SCRATCH_FAM_SVM_RPC_URL="http://127.0.0.1:1" KEYSTORE_NAME="zz-scratch-fam-none" \ + make deploy-pool CHAIN="$FAM_SVM" + run_case "deploy-lockbox refuses a non-EVM chain by family" nonzero \ + "deploy-lockbox: $FAM_SVM is chainFamily 'svm' - this target is EVM-only" -- \ + env ZZ_SCRATCH_FAM_SVM_RPC_URL="http://127.0.0.1:1" KEYSTORE_NAME="zz-scratch-fam-none" \ + make deploy-lockbox CHAIN="$FAM_SVM" + run_case "deploy-lockrelease-pool refuses a non-EVM chain by family" nonzero \ + "deploy-lockrelease-pool: $FAM_SVM is chainFamily 'svm' - this target is EVM-only" -- \ + env ZZ_SCRATCH_FAM_SVM_RPC_URL="http://127.0.0.1:1" KEYSTORE_NAME="zz-scratch-fam-none" \ + make deploy-lockrelease-pool CHAIN="$FAM_SVM" + run_case "adopt-token TOKEN= on a non-EVM chain names the adopt path" nonzero \ + "TOKEN= is an EVM address - pass this family's own form" -- \ + env ZZ_SCRATCH_FAM_SVM_RPC_URL="http://127.0.0.1:1" KEYSTORE_NAME="zz-scratch-fam-none" \ + make adopt-token CHAIN="$FAM_SVM" \ + TOKEN=0x1111111111111111111111111111111111111111 + # Not svm: the guard asks "is it evm", so every other family is refused the same way. + run_case "deploy-token refuses an aptos chain, not just svm" nonzero \ + "deploy-token: $FAM_APTOS is chainFamily 'aptos' - this target is EVM-only" -- \ + env ZZ_SCRATCH_FAM_APTOS_RPC_URL="http://127.0.0.1:1" KEYSTORE_NAME="zz-scratch-fam-none" \ + make deploy-token CHAIN="$FAM_APTOS" + + # 33c. no -32601: the whole point is that the refusal is a config answer, not a transport failure. + fam_out="$(env ZZ_SCRATCH_FAM_SVM_RPC_URL="https://api.devnet.solana.com" \ + KEYSTORE_NAME="zz-scratch-fam-none" make deploy-token CHAIN="$FAM_SVM" 2>&1)" + fam_status=$? + if [ $fam_status -ne 0 ] && ! grep -q "32601" <<< "$fam_out" && + ! grep -q "chain ID from fork endpoint" <<< "$fam_out"; then + pass=$((pass + 1)) + echo "[PASS] the family refusal replaces the -32601 fork failure (real Solana endpoint bound)" + else + fail=$((fail + 1)) + failures+=("family guard still reaches the endpoint") + echo "[FAIL] family guard still reaches the endpoint (exit=$fam_status)" + echo "$fam_out" | tail -6 | sed 's/^/ | /' + fi + + # 33d. the family answer beats the RPC/keystore prerequisites: an operator with no endpoint set + # should not be told to configure one for a chain that can never be deployed to. + fam_out="$(env -u ZZ_SCRATCH_FAM_SVM_RPC_URL -u KEYSTORE_NAME \ + make deploy-token CHAIN="$FAM_SVM" 2>&1)" + fam_status=$? + if [ $fam_status -ne 0 ] && grep -q "this target is EVM-only" <<< "$fam_out" && + ! grep -q "RPC URL not set" <<< "$fam_out"; then + pass=$((pass + 1)) + echo "[PASS] the family refusal precedes the RPC/keystore prerequisites" + else + fail=$((fail + 1)) + failures+=("family guard ordering") + echo "[FAIL] family guard ordering (exit=$fam_status)" + echo "$fam_out" | tail -6 | sed 's/^/ | /' + fi + + # 33e. the non-EVM path that DOES exist is untouched: adopt-token TOKEN_B58= still records. + run_case "adopt-token TOKEN_B58= still works on a non-EVM chain" zero "$FAM_SVM" -- \ + make adopt-token CHAIN="$FAM_SVM" TOKEN_B58="BPympxtoS3GZmNcGiTxqsH6kyRgKiS9QFjfviSLaqxRE" + + # 33f. the EVM side is untouched. An evm chain AND a config with no chainFamily both walk past + # the guard and fail for their own reason (no RPC), never by family. + fam_evm_ok=1 + fam_detail="" + for c in "$FAM_EVM" "$FAM_NOKEY" "$FAM_UPPER" "$FAM_PAD"; do + fam_out="$(env -u ZZ_SCRATCH_FAM_EVM_RPC_URL -u ZZ_SCRATCH_FAM_NOKEY_RPC_URL \ + -u ZZ_SCRATCH_FAM_UPPER_RPC_URL -u ZZ_SCRATCH_FAM_PAD_RPC_URL \ + make deploy-token CHAIN="$c" 2>&1)" + grep -q "EVM-only" <<< "$fam_out" && { fam_evm_ok=0; fam_detail="$fam_detail $c:refused"; } + grep -q "RPC URL not set" <<< "$fam_out" || { fam_evm_ok=0; fam_detail="$fam_detail $c:no-rpc-msg"; } + done + if [ $fam_evm_ok -eq 1 ]; then + pass=$((pass + 1)) + echo "[PASS] deploy-token is not family-refused on an evm chain, a chainFamily-less config, or a mixed-case/padded evm declaration" + else + fail=$((fail + 1)) + failures+=("family guard hits an EVM chain") + echo "[FAIL] family guard hits an EVM chain, a chainFamily-less config or a normalizable evm declaration ($fam_detail)" + fi + + # 33g. roles-check is a READ path: a named non-EVM chain SKIPs (exit 0). It used to reach forge + # and be classified as ROLES_DRIFT - an absent surface reported as a verdict. + run_case "roles-check SKIPs a named non-EVM chain instead of failing" zero \ + "$FAM_SVM: SKIP (non-EVM)" -- \ + make roles-check CHAIN="$FAM_SVM" + # CI calls the script directly for the exit-code contract, so assert it there: all-SKIP is CLEAN + # (exit 0), where naming the chain used to return ROLES_DRIFT (exit 1). + run_case "roles-check exits CLEAN when every named chain SKIPped" zero \ + "roles-check: CLEAN - nothing to reconcile" -- \ + bash script/config/roles-check.sh "$FAM_SVM" + + # 33h. `make tools`: the family-specific part hangs off the chain argument, so with no chain, and + # on an EVM chain, the output is the single baseline line it has always been. + if offline_enabled; then + tools_ok=1 + tools_detail="" + fam_bare="$(make tools 2>&1)" + [ "$fam_bare" = "tools: forge, curl and jq are all present" ] || + { tools_ok=0; tools_detail="bare='$fam_bare'"; } + for arg in "CHAIN=$FAM_EVM" "LOCAL=$FAM_EVM" "CHAIN=$FAM_NOKEY" "CHAIN=doesnotexist"; do + fam_out="$(make tools "$arg" 2>&1)" + [ "$fam_out" = "$fam_bare" ] || { tools_ok=0; tools_detail="$tools_detail $arg='$fam_out'"; } + done + if [ $tools_ok -eq 1 ]; then + pass=$((pass + 1)) + echo "[PASS] make tools is unchanged with no chain argument and on every EVM-resolving chain" + else + fail=$((fail + 1)) + failures+=("make tools EVM output") + echo "[FAIL] make tools EVM output ($tools_detail)" + fi + fi + run_case "make tools names the family for a non-EVM chain and asks for nothing extra" zero \ + "$FAM_SVM is chainFamily 'svm' - destination-only here, no extra toolchain required" -- \ + make tools CHAIN="$FAM_SVM" + run_case "make tools reads the family from LOCAL for the lane targets" zero \ + "$FAM_SVM is chainFamily 'svm' - destination-only here" -- \ + make tools LOCAL="$FAM_SVM" + + rm_fixture_config "config/chains/$FAM_SVM.json" "config/chains/$FAM_EVM.json" \ + "config/chains/$FAM_NOKEY.json" "config/chains/$FAM_APTOS.json" \ + "config/chains/$FAM_UPPER.json" "config/chains/$FAM_PAD.json" + rm -f "project/$FAM_SVM.json" "project/$FAM_EVM.json" "project/$FAM_NOKEY.json" \ + "project/$FAM_APTOS.json" "project/$FAM_UPPER.json" "project/$FAM_PAD.json" +fi + assert_configs_intact echo ""