diff --git a/app/upgrades/upgrades_test.go b/app/upgrades/upgrades_test.go index 2e78a1c8..fa6e2569 100644 --- a/app/upgrades/upgrades_test.go +++ b/app/upgrades/upgrades_test.go @@ -228,6 +228,29 @@ func TestV1201CarriesEVMBringupOnAllNetworks(t *testing.T) { } } +// TestV1202RegistersOnAllNetworks asserts v1.20.2 is wired up everywhere. +// +// NOTE: this test previously also asserted `config.StoreUpgrade == nil`, on the +// reasoning that v1.20.2 is a migration-only carrier. That premise was correct +// for testnet and WRONG for mainnet, and the mainnet-shaped devnet rehearsal +// disproved it: mainnet is still on 1.12.0 with NO EVM stores, so declaring no +// StoreUpgrades made every validator crash-loop with +// +// panic: version of store evmigration mismatch root store's version; +// expected 155 got 0; new stores should be added using StoreUpgrades +// +// v1.20.2 therefore declares the EVM store additions on every network, paired +// with the add-only store loader that mounts only the keys missing from +// committed state. Store expectations now live in v1_20_2_store_test.go. +func TestV1202RegistersOnAllNetworks(t *testing.T) { + for _, chainID := range []string{"lumera-mainnet-1", "lumera-testnet-2", "lumera-devnet-1"} { + params := newTestUpgradeParams(chainID) + config, found := SetupUpgrades(upgrade_v1_20_2.UpgradeName, params) + require.True(t, found) + require.NotNil(t, config.Handler, "v1.20.2 must register a handler on %s", chainID) + } +} + func newTestUpgradeParams(chainID string) appParams.AppUpgradeParams { return appParams.AppUpgradeParams{ ChainID: chainID, diff --git a/app/upgrades/v1_20_2/upgrade_test.go b/app/upgrades/v1_20_2/upgrade_test.go new file mode 100644 index 00000000..72ff542a --- /dev/null +++ b/app/upgrades/v1_20_2/upgrade_test.go @@ -0,0 +1,11 @@ +package v1_20_2 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestUpgradeName(t *testing.T) { + require.Equal(t, "v1.20.2", UpgradeName) +} diff --git a/devnet/scripts/account-registry.sh b/devnet/scripts/account-registry.sh index 5104c0fb..f6b26ae5 100644 --- a/devnet/scripts/account-registry.sh +++ b/devnet/scripts/account-registry.sh @@ -5,6 +5,7 @@ ACCOUNTS_DISPLAY_DENOM_DEFAULT="${ACCOUNTS_DISPLAY_DENOM_DEFAULT:-lume}" ACCOUNTS_DISPLAY_EXPONENT_DEFAULT="${ACCOUNTS_DISPLAY_EXPONENT_DEFAULT:-6}" +ACCOUNTS_FILE_MODE="${ACCOUNTS_FILE_MODE:-600}" accounts_registry_init() { local node_status_dir="$1" @@ -28,7 +29,7 @@ ensure_accounts_registry() { if [[ ! -f "${ACCOUNTS_FILE}" ]]; then printf '[]\n' >"${ACCOUNTS_FILE}" fi - chmod 644 "${ACCOUNTS_FILE}" + chmod "${ACCOUNTS_FILE_MODE}" "${ACCOUNTS_FILE}" } accounts_registry_parse_coin() { @@ -148,9 +149,9 @@ accounts_registry_upsert() { }] | sort_by(.name) ' "${ACCOUNTS_FILE}" >"${tmp_file}" - chmod 644 "${tmp_file}" + chmod "${ACCOUNTS_FILE_MODE}" "${tmp_file}" mv "${tmp_file}" "${ACCOUNTS_FILE}" - chmod 644 "${ACCOUNTS_FILE}" + chmod "${ACCOUNTS_FILE_MODE}" "${ACCOUNTS_FILE}" } accounts_registry_get_field() { diff --git a/devnet/scripts/submit-upgrade-proposal.sh b/devnet/scripts/submit-upgrade-proposal.sh index 71b7819d..44ded799 100755 --- a/devnet/scripts/submit-upgrade-proposal.sh +++ b/devnet/scripts/submit-upgrade-proposal.sh @@ -13,13 +13,15 @@ # Configuration VERSION="$1" UPGRADE_HEIGHT="$2" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEVNET_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" CHAIN_ID="lumera-devnet-1" SERVICE="supernova_validator_1" # primary validator LUMERA_SHARED="/tmp/${CHAIN_ID}/shared" KEYRING="test" HOST_PROPOSAL_FILE="${LUMERA_SHARED}/upgrade_${VERSION}.json" CONTAINER_PROPOSAL_FILE="/shared/upgrade_${VERSION}.json" -COMPOSE_FILE="../docker-compose.yml" +COMPOSE_FILE="${DEVNET_ROOT}/docker-compose.yml" STATUS_DIR="${LUMERA_SHARED}/status" ACCOUNT_REGISTRY_FILE="${STATUS_DIR}/${SERVICE}/accounts.json" diff --git a/devnet/scripts/vote-all.sh b/devnet/scripts/vote-all.sh index d325aa14..c428c382 100755 --- a/devnet/scripts/vote-all.sh +++ b/devnet/scripts/vote-all.sh @@ -7,11 +7,13 @@ if [ -z "$1" ]; then fi # Configuration +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEVNET_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" CHAIN_ID="lumera-devnet-1" KEYRING_BACKEND="test" PROPOSAL_ID="$1" SERVICE_NAME="supernova_validator_1" -COMPOSE_FILE="../docker-compose.yml" +COMPOSE_FILE="${DEVNET_ROOT}/docker-compose.yml" FEES="5000ulume" # Gas configuration — use a fixed gas amount by default. `--gas auto` simulates # a gov vote at ~57.9k and even with a 1.3x bump lands right at the real usage diff --git a/devnet/tests/common/account_registry_permissions_test.go b/devnet/tests/common/account_registry_permissions_test.go new file mode 100644 index 00000000..d246a559 --- /dev/null +++ b/devnet/tests/common/account_registry_permissions_test.go @@ -0,0 +1,35 @@ +package common + +import ( + "os/exec" + "path/filepath" + "testing" +) + +func TestAccountRegistryContainingMnemonicsIsOwnerOnly(t *testing.T) { + scriptPath, err := filepath.Abs("../../scripts/account-registry.sh") + if err != nil { + t.Fatalf("resolve account registry script path: %v", err) + } + + const scenario = ` +set -euo pipefail +source "$1" +accounts_registry_init "$2" + +ensure_accounts_registry +[[ "$(stat -c '%a' "$ACCOUNTS_FILE")" == "600" ]] + +chmod 644 "$ACCOUNTS_FILE" +ensure_accounts_registry +[[ "$(stat -c '%a' "$ACCOUNTS_FILE")" == "600" ]] + +accounts_registry_upsert test-account lumera1fixture 'fixture mnemonic' cosmos 1ulume genesis ABC123 +[[ "$(stat -c '%a' "$ACCOUNTS_FILE")" == "600" ]] +jq -e '. == [{name:"test-account",address:"lumera1fixture",mnemonic:"fixture mnemonic",type:"cosmos",funded:{display_amount:"0.000001",display_denom:"lume",base_amount:"1",base_denom:"ulume"},funding_key:"genesis",funding_txhash:"ABC123",created_at:(.[] | .created_at)}]' "$ACCOUNTS_FILE" >/dev/null +` + cmd := exec.Command("bash", "-c", scenario, "account-registry-permissions-test", scriptPath, t.TempDir()) + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("account registry must remain owner-only across every write path: %v\n%s", err, out) + } +} diff --git a/devnet/tests/common/upgrade_proposal_script_test.go b/devnet/tests/common/upgrade_proposal_script_test.go index 7fd93a27..efdff7aa 100644 --- a/devnet/tests/common/upgrade_proposal_script_test.go +++ b/devnet/tests/common/upgrade_proposal_script_test.go @@ -3,6 +3,7 @@ package common import ( "os/exec" "path/filepath" + "strings" "testing" ) @@ -20,6 +21,24 @@ func runUpgradeProposalScriptScenario(t *testing.T, testScript string) { } } +func TestUpgradeProposalComposePathIsIndependentOfWorkingDirectory(t *testing.T) { + scriptPath, err := filepath.Abs("../../scripts/submit-upgrade-proposal.sh") + if err != nil { + t.Fatalf("resolve proposal script path: %v", err) + } + expected := filepath.Clean(filepath.Join(filepath.Dir(scriptPath), "..", "docker-compose.yml")) + + cmd := exec.Command("bash", "-c", `source "$1"; printf '%s\n' "$COMPOSE_FILE"`, "upgrade-proposal-path-test", scriptPath) + cmd.Dir = t.TempDir() + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("source proposal script: %v\n%s", err, out) + } + if got := strings.TrimSpace(string(out)); got != expected { + t.Fatalf("compose path must be script-relative: got %q, want %q", got, expected) + } +} + func TestUpgradeProposalRestoresKeyForPersistedMigratedAddress(t *testing.T) { const testScript = ` source "$1" diff --git a/devnet/tests/common/vote_all_script_test.go b/devnet/tests/common/vote_all_script_test.go new file mode 100644 index 00000000..b8ac5a87 --- /dev/null +++ b/devnet/tests/common/vote_all_script_test.go @@ -0,0 +1,32 @@ +package common + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestVoteAllComposePathIsScriptRelative(t *testing.T) { + scriptPath, err := filepath.Abs("../../scripts/vote-all.sh") + if err != nil { + t.Fatalf("resolve vote script path: %v", err) + } + contents, err := os.ReadFile(scriptPath) + if err != nil { + t.Fatalf("read vote script: %v", err) + } + script := string(contents) + for _, required := range []string{ + `SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"`, + `DEVNET_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"`, + `COMPOSE_FILE="${DEVNET_ROOT}/docker-compose.yml"`, + } { + if !strings.Contains(script, required) { + t.Fatalf("vote script must contain %q", required) + } + } + if strings.Contains(script, `COMPOSE_FILE="../docker-compose.yml"`) { + t.Fatal("vote script must not resolve compose path from caller working directory") + } +} diff --git a/docs/evm-integration/operator-artifacts/compatibility-manifest.schema.json b/docs/evm-integration/operator-artifacts/compatibility-manifest.schema.json new file mode 100644 index 00000000..68547b64 --- /dev/null +++ b/docs/evm-integration/operator-artifacts/compatibility-manifest.schema.json @@ -0,0 +1,399 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://lumera.io/schemas/evmigration-compatibility-manifest-v1.json", + "title": "Lumera EVM migration compatibility manifest", + "type": "object", + "additionalProperties": false, + "required": [ + "$schema", + "schema_version", + "release", + "chain", + "artifacts", + "operator_contracts", + "validation", + "approval" + ], + "properties": { + "$schema": { "const": "./compatibility-manifest.schema.json" }, + "schema_version": { "const": "1.0.0" }, + "release": { + "type": "object", + "additionalProperties": false, + "required": ["tag", "commit", "release_url"], + "properties": { + "tag": { "type": "string", "minLength": 1 }, + "commit": { "type": "string", "pattern": "^[0-9a-f]{40}$" }, + "release_url": { "type": "string", "minLength": 1 } + } + }, + "chain": { + "type": "object", + "additionalProperties": false, + "required": ["chain_id", "evm_chain_id", "minimum_upgrade"], + "properties": { + "chain_id": { "type": "string", "minLength": 1 }, + "evm_chain_id": { "type": ["integer", "string"] }, + "minimum_upgrade": { "type": "string", "minLength": 1 } + } + }, + "artifacts": { + "type": "object", + "additionalProperties": false, + "required": ["chain_executable", "supernode_executable", "sncli_executable", "relayer_executable", "container_image", "bound_files"], + "properties": { + "chain_executable": { "$ref": "#/$defs/executable" }, + "supernode_executable": { "$ref": "#/$defs/executable" }, + "sncli_executable": { "$ref": "#/$defs/executable" }, + "relayer_executable": { "$ref": "#/$defs/executable" }, + "container_image": { "$ref": "#/$defs/container_image" }, + "bound_files": { + "type": "object", + "additionalProperties": false, + "required": [ + "scripts/evmigration-common.sh", + "scripts/migrate-account.sh", + "scripts/migrate-validator.sh", + "scripts/migrate-multisig.sh", + "scripts/migrate-batch.sh", + "docs/evm-integration/user-guides/operator-migration-runbook.md", + "docs/evm-integration/user-guides/migration.md", + "docs/evm-integration/user-guides/migration-scripts.md", + "docs/evm-integration/user-guides/validator-migration.md", + "docs/evm-integration/user-guides/supernode-migration.md", + "docs/evm-integration/user-guides/relayer-migration.md", + "docs/evm-integration/evmigration/legacy-migration.md" + ], + "properties": { + "scripts/evmigration-common.sh": { "$ref": "#/$defs/file" }, + "scripts/migrate-account.sh": { "$ref": "#/$defs/file" }, + "scripts/migrate-validator.sh": { "$ref": "#/$defs/file" }, + "scripts/migrate-multisig.sh": { "$ref": "#/$defs/file" }, + "scripts/migrate-batch.sh": { "$ref": "#/$defs/file" }, + "docs/evm-integration/user-guides/operator-migration-runbook.md": { "$ref": "#/$defs/file" }, + "docs/evm-integration/user-guides/migration.md": { "$ref": "#/$defs/file" }, + "docs/evm-integration/user-guides/migration-scripts.md": { "$ref": "#/$defs/file" }, + "docs/evm-integration/user-guides/validator-migration.md": { "$ref": "#/$defs/file" }, + "docs/evm-integration/user-guides/supernode-migration.md": { "$ref": "#/$defs/file" }, + "docs/evm-integration/user-guides/relayer-migration.md": { "$ref": "#/$defs/file" }, + "docs/evm-integration/evmigration/legacy-migration.md": { "$ref": "#/$defs/file" } + } + } + } + }, + "operator_contracts": { + "type": "object", + "additionalProperties": false, + "required": ["binary_provenance", "keyring_provenance", "destination_prestage_no_echo", "single_broadcast"], + "properties": { + "binary_provenance": { "const": "absolute-path+version+sha256" }, + "keyring_provenance": { "const": "backend-and-location-separated" }, + "destination_prestage_no_echo": { + "type": "object", + "additionalProperties": false, + "required": ["status", "implementation"], + "properties": { + "status": { "enum": ["verified", "blocked"] }, + "implementation": { + "oneOf": [ + { "type": "null" }, + { "$ref": "#/$defs/pr2_implementation" } + ] + } + } + }, + "single_broadcast": { "const": true } + } + }, + "validation": { + "type": "object", + "additionalProperties": false, + "required": ["bats", "shellcheck", "cli_help", "schema", "json_parse", "docs_links", "owned_testnet", "portal_live"], + "properties": { + "bats": { "$ref": "#/$defs/check" }, + "shellcheck": { "$ref": "#/$defs/check" }, + "cli_help": { "$ref": "#/$defs/check" }, + "schema": { "$ref": "#/$defs/check" }, + "json_parse": { "$ref": "#/$defs/check" }, + "docs_links": { "$ref": "#/$defs/check" }, + "owned_testnet": { "$ref": "#/$defs/check" }, + "portal_live": { "$ref": "#/$defs/check" } + } + }, + "approval": { + "type": "object", + "additionalProperties": false, + "required": ["status", "release_owner_approved", "canonicalization", "detached_signature"], + "properties": { + "status": { "enum": ["approved", "blocked"] }, + "release_owner_approved": { "type": "boolean" }, + "canonicalization": { "const": "RFC8785-JCS" }, + "detached_signature": { + "type": "object", + "additionalProperties": false, + "required": ["format", "bundle", "certificate_identity", "certificate_oidc_issuer"], + "properties": { + "format": { "const": "sigstore-bundle" }, + "bundle": { "const": "compatibility-manifest.sigstore.json" }, + "certificate_identity": { "type": ["string", "null"] }, + "certificate_oidc_issuer": { "type": ["string", "null"] } + } + } + } + }, + "_verification": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 } + }, + "_signing_blocker": { "type": "string", "minLength": 1 } + }, + "allOf": [ + { + "if": { + "properties": { + "approval": { + "required": ["status"], + "properties": { "status": { "const": "approved" } } + } + } + }, + "then": { + "not": { "required": ["_signing_blocker"] }, + "properties": { + "release": { + "properties": { + "tag": { "$ref": "#/$defs/non_placeholder" }, + "commit": { "$ref": "#/$defs/commit" }, + "release_url": { "$ref": "#/$defs/non_placeholder" } + } + }, + "chain": { + "properties": { + "chain_id": { "$ref": "#/$defs/non_placeholder" }, + "evm_chain_id": { + "oneOf": [ + { "type": "integer", "minimum": 1 }, + { "$ref": "#/$defs/non_placeholder" } + ] + }, + "minimum_upgrade": { "$ref": "#/$defs/non_placeholder" } + } + }, + "artifacts": { "$ref": "#/$defs/approved_artifacts" }, + "operator_contracts": { + "properties": { + "destination_prestage_no_echo": { + "properties": { + "status": { "const": "verified" }, + "implementation": { "$ref": "#/$defs/approved_pr2_implementation" } + } + } + } + }, + "validation": { + "properties": { + "bats": { "$ref": "#/$defs/passing_check" }, + "shellcheck": { "$ref": "#/$defs/passing_check" }, + "cli_help": { "$ref": "#/$defs/passing_check" }, + "schema": { "$ref": "#/$defs/passing_check" }, + "json_parse": { "$ref": "#/$defs/passing_check" }, + "docs_links": { "$ref": "#/$defs/passing_check" }, + "owned_testnet": { "$ref": "#/$defs/passing_check" }, + "portal_live": { "$ref": "#/$defs/passing_check" } + } + }, + "approval": { + "properties": { + "release_owner_approved": { "const": true }, + "detached_signature": { "$ref": "#/$defs/approved_signature" } + } + }, + "_verification": { + "items": { "$ref": "#/$defs/non_placeholder" } + } + } + } + } + ], + "$defs": { + "absolute_path": { + "type": "string", + "minLength": 1, + "pattern": "^/" + }, + "non_placeholder": { + "type": "string", + "minLength": 3, + "pattern": "^(?!.*(?:[Pp][Ll][Aa][Cc][Ee][Hh][Oo][Ll][Dd][Ee][Rr]|[Tt][Oo][Dd][Oo]|[Tt][Bb][Dd]|[Uu][Nn][Kk][Nn][Oo][Ww][Nn]|[Rr][Ee][Pp][Ll][Aa][Cc][Ee])(?:[^A-Za-z]|$))(?!.*<[^>]+>).+\\S$" + }, + "non_placeholder_argv": { + "type": "string", + "minLength": 1, + "pattern": "^(?!.*(?:[Pp][Ll][Aa][Cc][Ee][Hh][Oo][Ll][Dd][Ee][Rr]|[Tt][Oo][Dd][Oo]|[Tt][Bb][Dd]|[Uu][Nn][Kk][Nn][Oo][Ww][Nn]|[Rr][Ee][Pp][Ll][Aa][Cc][Ee])(?:[^A-Za-z]|$))(?!.*<[^>]+>)\\S(?:.*\\S)?$" + }, + "sha256": { + "type": "string", + "pattern": "^(?!0{64}$)[0-9a-f]{64}$" + }, + "commit": { + "type": "string", + "pattern": "^(?!0{40}$)[0-9a-f]{40}$" + }, + "file": { + "type": "object", + "additionalProperties": false, + "required": ["release_path", "sha256", "source"], + "properties": { + "release_path": { "$ref": "#/$defs/absolute_path" }, + "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "source": { "type": "string", "minLength": 1 } + } + }, + "approved_file": { + "type": "object", + "properties": { + "release_path": { "$ref": "#/$defs/non_placeholder" }, + "sha256": { "$ref": "#/$defs/sha256" }, + "source": { "$ref": "#/$defs/non_placeholder" } + } + }, + "executable": { + "type": "object", + "additionalProperties": false, + "required": ["name", "release_path", "version", "tag", "commit", "sha256", "source"], + "properties": { + "name": { "type": "string", "minLength": 1 }, + "release_path": { "$ref": "#/$defs/absolute_path" }, + "version": { "type": "string", "minLength": 1 }, + "tag": { "type": "string", "minLength": 1 }, + "commit": { "type": "string", "pattern": "^[0-9a-f]{40}$" }, + "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "source": { "type": "string", "minLength": 1 } + } + }, + "approved_executable": { + "type": "object", + "properties": { + "name": { "$ref": "#/$defs/non_placeholder" }, + "release_path": { "$ref": "#/$defs/non_placeholder" }, + "version": { "$ref": "#/$defs/non_placeholder" }, + "tag": { "$ref": "#/$defs/non_placeholder" }, + "commit": { "$ref": "#/$defs/commit" }, + "sha256": { "$ref": "#/$defs/sha256" }, + "source": { "$ref": "#/$defs/non_placeholder" } + } + }, + "container_image": { + "type": "object", + "additionalProperties": false, + "required": ["artifact", "digest", "source"], + "properties": { + "artifact": { "type": "string", "minLength": 1 }, + "digest": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" }, + "source": { "type": "string", "minLength": 1 } + } + }, + "approved_container_image": { + "type": "object", + "properties": { + "artifact": { "$ref": "#/$defs/non_placeholder" }, + "digest": { "type": "string", "pattern": "^sha256:(?!0{64}$)[0-9a-f]{64}$" }, + "source": { "$ref": "#/$defs/non_placeholder" } + } + }, + "pr2_implementation": { + "type": "object", + "additionalProperties": false, + "required": ["name", "argv", "tag", "commit", "sha256", "source", "no_echo_contract"], + "properties": { + "name": { "type": "string", "minLength": 1 }, + "argv": { + "type": "array", + "minItems": 1, + "prefixItems": [{ "$ref": "#/$defs/absolute_path" }], + "items": { "type": "string", "minLength": 1 } + }, + "tag": { "type": "string", "minLength": 1 }, + "commit": { "type": "string", "pattern": "^[0-9a-f]{40}$" }, + "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "source": { "type": "string", "minLength": 1 }, + "no_echo_contract": { "const": "hidden-tty-or-protected-fd;never-argv;never-echo;no-xtrace" } + } + }, + "approved_pr2_implementation": { + "allOf": [ + { "$ref": "#/$defs/pr2_implementation" }, + { + "properties": { + "name": { "$ref": "#/$defs/non_placeholder" }, + "argv": { + "type": "array", + "minItems": 1, + "prefixItems": [{ + "allOf": [ + { "$ref": "#/$defs/absolute_path" }, + { "$ref": "#/$defs/non_placeholder" } + ] + }], + "items": { "$ref": "#/$defs/non_placeholder_argv" } + }, + "tag": { "$ref": "#/$defs/non_placeholder" }, + "commit": { "$ref": "#/$defs/commit" }, + "sha256": { "$ref": "#/$defs/sha256" }, + "source": { "$ref": "#/$defs/non_placeholder" } + } + } + ] + }, + "check": { + "type": "object", + "additionalProperties": false, + "required": ["status", "evidence"], + "properties": { + "status": { "enum": ["pass", "fail", "postponed"] }, + "evidence": { "type": "string" } + } + }, + "passing_check": { + "type": "object", + "properties": { + "status": { "const": "pass" }, + "evidence": { "$ref": "#/$defs/non_placeholder" } + } + }, + "approved_signature": { + "type": "object", + "properties": { + "certificate_identity": { "$ref": "#/$defs/non_placeholder" }, + "certificate_oidc_issuer": { "$ref": "#/$defs/non_placeholder" } + } + }, + "approved_artifacts": { + "type": "object", + "properties": { + "chain_executable": { "$ref": "#/$defs/approved_executable" }, + "supernode_executable": { "$ref": "#/$defs/approved_executable" }, + "sncli_executable": { "$ref": "#/$defs/approved_executable" }, + "relayer_executable": { "$ref": "#/$defs/approved_executable" }, + "container_image": { "$ref": "#/$defs/approved_container_image" }, + "bound_files": { + "properties": { + "scripts/evmigration-common.sh": { "$ref": "#/$defs/approved_file" }, + "scripts/migrate-account.sh": { "$ref": "#/$defs/approved_file" }, + "scripts/migrate-validator.sh": { "$ref": "#/$defs/approved_file" }, + "scripts/migrate-multisig.sh": { "$ref": "#/$defs/approved_file" }, + "scripts/migrate-batch.sh": { "$ref": "#/$defs/approved_file" }, + "docs/evm-integration/user-guides/operator-migration-runbook.md": { "$ref": "#/$defs/approved_file" }, + "docs/evm-integration/user-guides/migration.md": { "$ref": "#/$defs/approved_file" }, + "docs/evm-integration/user-guides/migration-scripts.md": { "$ref": "#/$defs/approved_file" }, + "docs/evm-integration/user-guides/validator-migration.md": { "$ref": "#/$defs/approved_file" }, + "docs/evm-integration/user-guides/supernode-migration.md": { "$ref": "#/$defs/approved_file" }, + "docs/evm-integration/user-guides/relayer-migration.md": { "$ref": "#/$defs/approved_file" }, + "docs/evm-integration/evmigration/legacy-migration.md": { "$ref": "#/$defs/approved_file" } + } + } + } + } + } +} diff --git a/docs/evm-integration/operator-artifacts/compatibility-manifest.template.json b/docs/evm-integration/operator-artifacts/compatibility-manifest.template.json new file mode 100644 index 00000000..fe41cb8f --- /dev/null +++ b/docs/evm-integration/operator-artifacts/compatibility-manifest.template.json @@ -0,0 +1,187 @@ +{ + "$schema": "./compatibility-manifest.schema.json", + "schema_version": "1.0.0", + "release": { + "tag": "REPLACE_WITH_RELEASE_TAG", + "commit": "0000000000000000000000000000000000000000", + "release_url": "REPLACE_WITH_RELEASE_URL" + }, + "chain": { + "chain_id": "REPLACE_WITH_CHAIN_ID", + "evm_chain_id": "REPLACE_WITH_EVM_CHAIN_ID", + "minimum_upgrade": "REPLACE_WITH_UPGRADE_NAME" + }, + "artifacts": { + "chain_executable": { + "name": "lumerad", + "release_path": "/REPLACE_WITH_TARBALL_PATH_TO_LUMERAD", + "version": "REPLACE_WITH_LUMERAD_VERSION_LONG", + "tag": "REPLACE_WITH_CHAIN_RELEASE_TAG", + "commit": "0000000000000000000000000000000000000000", + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "source": "REPLACE_WITH_CHAIN_RELEASE_ASSET_URL" + }, + "supernode_executable": { + "name": "supernode", + "release_path": "/REPLACE_WITH_TARBALL_PATH_TO_SUPERNODE", + "version": "REPLACE_WITH_SUPERNODE_VERSION", + "tag": "REPLACE_WITH_SUPERNODE_RELEASE_TAG", + "commit": "0000000000000000000000000000000000000000", + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "source": "REPLACE_WITH_SUPERNODE_RELEASE_ASSET_URL" + }, + "sncli_executable": { + "name": "sncli", + "release_path": "/REPLACE_WITH_TARBALL_PATH_TO_SNCLI", + "version": "REPLACE_WITH_SNCLI_VERSION", + "tag": "REPLACE_WITH_SNCLI_RELEASE_TAG", + "commit": "0000000000000000000000000000000000000000", + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "source": "REPLACE_WITH_SNCLI_RELEASE_ASSET_URL" + }, + "relayer_executable": { + "name": "hermes", + "release_path": "/REPLACE_WITH_TARBALL_PATH_TO_HERMES", + "version": "REPLACE_WITH_HERMES_VERSION", + "tag": "REPLACE_WITH_HERMES_RELEASE_TAG", + "commit": "0000000000000000000000000000000000000000", + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "source": "REPLACE_WITH_HERMES_RELEASE_ASSET_URL" + }, + "container_image": { + "artifact": "REPLACE_WITH_CONTAINER_IMAGE_REPOSITORY_AND_TAG", + "digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "source": "REPLACE_WITH_CONTAINER_IMAGE_REGISTRY_URL" + }, + "bound_files": { + "scripts/evmigration-common.sh": { + "release_path": "/REPLACE_WITH_RUNTIME_PATH_TO_scripts/evmigration-common.sh", + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "source": "REPLACE_WITH_CHAIN_RELEASE_ASSET#scripts/evmigration-common.sh" + }, + "scripts/migrate-account.sh": { + "release_path": "/REPLACE_WITH_RUNTIME_PATH_TO_scripts/migrate-account.sh", + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "source": "REPLACE_WITH_CHAIN_RELEASE_ASSET#scripts/migrate-account.sh" + }, + "scripts/migrate-validator.sh": { + "release_path": "/REPLACE_WITH_RUNTIME_PATH_TO_scripts/migrate-validator.sh", + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "source": "REPLACE_WITH_CHAIN_RELEASE_ASSET#scripts/migrate-validator.sh" + }, + "scripts/migrate-multisig.sh": { + "release_path": "/REPLACE_WITH_RUNTIME_PATH_TO_scripts/migrate-multisig.sh", + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "source": "REPLACE_WITH_CHAIN_RELEASE_ASSET#scripts/migrate-multisig.sh" + }, + "scripts/migrate-batch.sh": { + "release_path": "/REPLACE_WITH_RUNTIME_PATH_TO_scripts/migrate-batch.sh", + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "source": "REPLACE_WITH_CHAIN_RELEASE_ASSET#scripts/migrate-batch.sh" + }, + "docs/evm-integration/user-guides/operator-migration-runbook.md": { + "release_path": "/REPLACE_WITH_RUNTIME_PATH_TO_docs/evm-integration/user-guides/operator-migration-runbook.md", + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "source": "REPLACE_WITH_CHAIN_RELEASE_ASSET#docs/evm-integration/user-guides/operator-migration-runbook.md" + }, + "docs/evm-integration/user-guides/migration.md": { + "release_path": "/REPLACE_WITH_RUNTIME_PATH_TO_docs/evm-integration/user-guides/migration.md", + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "source": "REPLACE_WITH_CHAIN_RELEASE_ASSET#docs/evm-integration/user-guides/migration.md" + }, + "docs/evm-integration/user-guides/migration-scripts.md": { + "release_path": "/REPLACE_WITH_RUNTIME_PATH_TO_docs/evm-integration/user-guides/migration-scripts.md", + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "source": "REPLACE_WITH_CHAIN_RELEASE_ASSET#docs/evm-integration/user-guides/migration-scripts.md" + }, + "docs/evm-integration/user-guides/validator-migration.md": { + "release_path": "/REPLACE_WITH_RUNTIME_PATH_TO_docs/evm-integration/user-guides/validator-migration.md", + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "source": "REPLACE_WITH_CHAIN_RELEASE_ASSET#docs/evm-integration/user-guides/validator-migration.md" + }, + "docs/evm-integration/user-guides/supernode-migration.md": { + "release_path": "/REPLACE_WITH_RUNTIME_PATH_TO_docs/evm-integration/user-guides/supernode-migration.md", + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "source": "REPLACE_WITH_CHAIN_RELEASE_ASSET#docs/evm-integration/user-guides/supernode-migration.md" + }, + "docs/evm-integration/user-guides/relayer-migration.md": { + "release_path": "/REPLACE_WITH_RUNTIME_PATH_TO_docs/evm-integration/user-guides/relayer-migration.md", + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "source": "REPLACE_WITH_CHAIN_RELEASE_ASSET#docs/evm-integration/user-guides/relayer-migration.md" + }, + "docs/evm-integration/evmigration/legacy-migration.md": { + "release_path": "/REPLACE_WITH_RUNTIME_PATH_TO_docs/evm-integration/evmigration/legacy-migration.md", + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "source": "REPLACE_WITH_CHAIN_RELEASE_ASSET#docs/evm-integration/evmigration/legacy-migration.md" + } + } + }, + "operator_contracts": { + "binary_provenance": "absolute-path+version+sha256", + "keyring_provenance": "backend-and-location-separated", + "destination_prestage_no_echo": { + "status": "blocked", + "implementation": null + }, + "single_broadcast": true + }, + "validation": { + "bats": { + "status": "postponed", + "evidence": "REPLACE_WITH_COMMAND_AND_SANITIZED_RESULT" + }, + "shellcheck": { + "status": "postponed", + "evidence": "REPLACE_WITH_COMMAND_AND_SANITIZED_RESULT" + }, + "cli_help": { + "status": "postponed", + "evidence": "REPLACE_WITH_HELP_COMMANDS_AND_BINARY_HASHES" + }, + "schema": { + "status": "postponed", + "evidence": "REPLACE_WITH_SCHEMA_TEMPLATE_AND_UNSAFE_APPROVAL_RESULTS" + }, + "json_parse": { + "status": "postponed", + "evidence": "REPLACE_WITH_JSON_PARSE_RESULT" + }, + "docs_links": { + "status": "postponed", + "evidence": "REPLACE_WITH_LINK_CHECK_RESULT" + }, + "owned_testnet": { + "status": "postponed", + "evidence": "owned-testnet-baseline.template.json" + }, + "portal_live": { + "status": "postponed", + "evidence": "REPLACE_WITH_REDACTED_LIVE_BROWSER_EVIDENCE_BUNDLE" + } + }, + "approval": { + "status": "blocked", + "release_owner_approved": false, + "canonicalization": "RFC8785-JCS", + "detached_signature": { + "format": "sigstore-bundle", + "bundle": "compatibility-manifest.sigstore.json", + "certificate_identity": null, + "certificate_oidc_issuer": null + } + }, + "_verification": [ + "Release owner: replace every REPLACE_WITH value and every all-zero commit/hash; approval.status=approved is schema-invalid while any remain.", + "For destination_prestage_no_echo, bind the full implementation.argv with its sole executable path in argv[0]. Require argv[0] to be absolute, verify implementation.sha256 against argv[0] immediately before execution, and execute the argv array without eval or secret material in argv.", + "Extract each release asset into a new mode-0700 directory. Verify the chain, SuperNode, sncli, and Hermes executable release path, tag, commit, version, source, and SHA-256; verify the container image artifact, registry source, and immutable digest; then verify every artifacts.bound_files release_path individually with sha256sum.", + "Require operator_contracts.destination_prestage_no_echo.status=verified and bind its implementation name, tag, commit, SHA-256, source, and exact no-echo contract. Do not approve an unfinished or narrative-only PR-2 dependency.", + "Run every validation command and attach sanitized evidence. Every validation status, including owned_testnet and portal_live, must be pass before approval is schema-valid.", + "Validate the schema under draft 2020-12, validate this manifest against it, and confirm a copy changed to approval.status=approved while retaining any blocked/postponed/placeholder field is rejected.", + "Before canonicalization, the release owner MUST set approval.release_owner_approved=true, approval.status=approved, and replace approval.detached_signature certificate_identity/certificate_oidc_issuer with the trusted release identity and issuer. Revalidate the completed draft against the schema; do not edit approval or signature metadata after canonicalization.", + "Create the final immutable manifest bytes with RFC 8785 JCS, for example: python3 -c 'import json,sys,rfc8785; sys.stdout.buffer.write(rfc8785.dumps(json.load(sys.stdin)))' < compatibility-manifest.draft.json > compatibility-manifest.json. Do not edit compatibility-manifest.json after canonicalization or signing.", + "Use keyless Sigstore signing (no long-lived signing secret): cosign sign-blob --yes --bundle compatibility-manifest.sigstore.json compatibility-manifest.json", + "Verify without modifying either file, using the same trusted identity and issuer already embedded before canonicalization: cosign verify-blob --bundle compatibility-manifest.sigstore.json --certificate-identity 'RELEASE_OWNER_MUST_REPLACE_CERTIFICATE_IDENTITY' --certificate-oidc-issuer 'RELEASE_OWNER_MUST_REPLACE_OIDC_ISSUER' compatibility-manifest.json", + "Publish compatibility-manifest.json and compatibility-manifest.sigstore.json together. Approval is effective only when schema validation passes and the detached bundle verifies against the release-owner-approved identity and issuer." + ], + "_signing_blocker": "No release-owner trusted Sigstore certificate identity/OIDC issuer is currently supplied. Keep approval blocked until the release owner supplies them, all external gates pass, the final canonical manifest is signed, and the detached bundle verifies." +} diff --git a/docs/evm-integration/operator-artifacts/owned-testnet-baseline.template.json b/docs/evm-integration/operator-artifacts/owned-testnet-baseline.template.json new file mode 100644 index 00000000..e6f029be --- /dev/null +++ b/docs/evm-integration/operator-artifacts/owned-testnet-baseline.template.json @@ -0,0 +1,76 @@ +{ + "schema_version": "1.0.0", + "classification": "SANITIZED_OWNED_TESTNET_BASELINE", + "provenance_status": "POSTPONED", + "reason": "Replace placeholders only after an owned-testnet rehearsal; never insert credentials or secret values.", + "observed_at_utc": null, + "operator": { + "organization": "REPLACE_WITH_NON_SECRET_OWNER", + "environment": "owned-testnet", + "evidence_bundle_sha256": null + }, + "network": { + "chain_id": null, + "evm_chain_id": null, + "genesis_sha256": null, + "upgrade_name": null, + "height_before": null, + "height_after": null + }, + "artifacts": { + "release_tag": null, + "commit": null, + "lumerad_version": null, + "lumerad_sha256": null, + "helper_sha256": null, + "compatibility_manifest_sha256": null, + "compatibility_manifest_signature_verified": false + }, + "runtime": { + "supervisor": null, + "service_user_uid": null, + "base_directory_redacted": true, + "config_sha256_before": null, + "config_sha256_after": null, + "keyring_backend": null, + "keyring_location_redacted": true + }, + "public_addresses": { + "legacy": null, + "destination": null, + "destination_key_type": null, + "migration_record_destination": null + }, + "gates": { + "process_discovered": "POSTPONED", + "config_backup_mode_0600": "POSTPONED", + "destination_no_echo_prestage": "POSTPONED", + "destination_freshness": "POSTPONED", + "service_stopped_verified": "POSTPONED", + "dry_run": "POSTPONED", + "single_broadcast": "POSTPONED", + "public_chain_queries": "POSTPONED", + "grpc_health_serving": "POSTPONED", + "logs_reviewed": "POSTPONED", + "portal_live_browser_evidence": "POSTPONED" + }, + "transaction": { + "tx_hash": null, + "height": null, + "code": null, + "broadcast_attempt_count": 0 + }, + "result": "POSTPONED", + "redaction_attestation": { + "mnemonics_absent": true, + "private_keys_absent": true, + "passphrases_absent": true, + "tokens_cookies_auth_headers_absent": true, + "raw_environment_absent": true + }, + "notes": [ + "Use public chain addresses and hashes only.", + "Do not include keyring files, Secret objects, mnemonic argv, terminal history, HAR auth headers, or raw logs before redaction.", + "POSTPONED is not PASS and must not be used as release approval evidence." + ] +} diff --git a/docs/evm-integration/user-guides/migration.md b/docs/evm-integration/user-guides/migration.md index 8eca3227..38f6105a 100644 --- a/docs/evm-integration/user-guides/migration.md +++ b/docs/evm-integration/user-guides/migration.md @@ -3,6 +3,8 @@ **Last updated**: 2026-06-24 **Applies to**: Lumera chain with `x/evmigration` module enabled (post-EVM upgrade) +> **Operator/custody gate:** Before any terminal or service migration, follow the canonical [EVM Migration Operator Runbook](operator-migration-runbook.md). It pins executable and keyring provenance, places destination proof before the irreversible boundary, defines stop/restart evidence for systemd/Docker/Kubernetes, and fails closed when release signing or the required PR-2 no-echo destination-prestage dependency is unresolved. + --- ## Why Migration Is Needed @@ -127,7 +129,7 @@ record count. in [validator-migration.md](validator-migration.md) — your own node is stopped during broadcast, so this change goes on the **trusted external RPC** node. -For step-by-step instructions see [§ Single-sig validator migration](#single-sig-validator-migration) (Method 2) and [§ Post-Migration for Validators](#5-post-migration-for-validators) (Method 3). For maintenance-window planning, consensus-key safety, and the multisig variant, see [validator-migration.md](validator-migration.md). +For the release-pinned one-shot procedure, maintenance-window planning, consensus-key safety, post-migration restart, and the multisig variant, see [validator-migration.md](validator-migration.md) and the mandatory [Operator Runbook](operator-migration-runbook.md). --- @@ -285,7 +287,7 @@ The result screen now embeds the complete post-migration checklist so you can fi - **3. Switch the Portal to Lumera EVM, reconnect Keplr, then add an existing wallet with the same recovery phrase** — the ordered sub-steps (a–e) that the Claim/EVM Migration page also walks you through after you close the dialog. - **Tx** — the on-chain transaction hash, with copy and explorer-link buttons. -**For validators**: an urgent section shows the restart command (`systemctl start lumerad`). Restart your validator promptly to avoid missed blocks and jailing. +**For validators**: the Portal may display a host-specific restart hint. Ignore it unless it exactly matches the supervisor discovered in the mandatory runbook; restore the validator through [Operator Runbook §8](operator-migration-runbook.md#8-finalize-restart-and-verify). Click **DONE** to close the wizard. The **EVM Migration** page now switches into the post-migration follow-up flow described next. @@ -302,7 +304,7 @@ Click **DONE** to close the wizard. The **EVM Migration** page now switches into > 5. Repeat steps 3–4 for every legacy account you have. > 6. **Only once every legacy account is migrated**, follow the post-migration cleanup once: switch Portal to the EVM profile, refresh Keplr's chain registration, and then re-import the mnemonic(s) into fresh Keplr profile(s) to expose the migrated EVM-derived addresses for each account. > -> **Many accounts? Use the shell helpers instead.** Once you're past a handful of legacy accounts, clicking through the Portal+Keplr wizard for each one becomes the bottleneck — and Keplr's signature popups can't be automated. Switch to the bundled [`scripts/migrate-account.sh`](#method-2-shell-helper-scripts) (or `migrate-validator.sh` for validators), which run the same migration non-interactively from a keyring. They're easy to drop into a loop over a list of legacy key names, produce structured exit codes for each result, and capture pre/post balance snapshots — so a batch migration is auditable rather than something you have to retrace by hand. +> **Many accounts require a separately reviewed batch plan.** Use only the exact manifest-bound batch/helper `release_path` through [Method 2](#method-2-release-pinned-shell-helpers) and its matching method guide. Preserve the same destination-prestage, stopped-state, dry-run, one-broadcast, and query-before-retry gates for every account; do not improvise a loop around an abbreviated command. #### 5. Post-Migration Follow-Up on the EVM Migration Page @@ -433,223 +435,25 @@ You switched Keplr accounts or profiles between wizard steps. Go back to Step 1 --- -## Method 2: Shell Helper Scripts - -The repository ships two bash wrappers in [scripts/](../../../scripts/) that layer safety rails on top of the Method 3 CLI flow: - -- `scripts/migrate-account.sh` — regular account migration (`claim-legacy-account`) -- `scripts/migrate-validator.sh` — validator migration (`migrate-validator`) - -Both scripts: - -- Detect and reject multisig accounts (use the offline 4-step flow in[legacy-migration.md](../evmigration/legacy-migration.md#multisig-account-migration) for those). -- Run `migration-estimate` before broadcast so you see what moves and why it might fail. -- Compare post-migration balances against a pre-broadcast snapshot. - -The abbreviated invocations below cover the common cases. For the full reference — all flags, exit codes, troubleshooting keyed by exit code, fresh-destination-key flow, and non-interactive / CI usage — see [migration-scripts.md](migration-scripts.md). - -### Single-sig account migration - -```bash -./scripts/migrate-account.sh legacy-key new-key \ - --chain-id lumera-mainnet-1 \ - --node tcp://rpc.lumera:26657 \ - --keyring-backend test -``` - -Use `--mnemonic-file ` (mode `0600`) as an optional convenience when both derivations intentionally use one mnemonic. Otherwise import the keys separately. Add `--dry-run` to preview without broadcasting. +## Method 2: Release-Pinned Shell Helpers -### Single-sig validator migration +The release includes single-account and validator helpers, but this guide intentionally does not duplicate executable invocations. A shortened command can silently select the wrong binary, helper file, home, or keyring. -```bash -./scripts/migrate-validator.sh legacy-op-key new-evm-key \ - --chain-id lumera-mainnet-1 \ - --node tcp://rpc.lumera:26657 \ - --keyring-backend test \ - --i-have-stopped-the-node -``` - -`--i-have-stopped-the-node` acknowledges the jailing risk; omitting it makes the script prompt interactively. `--yes` does NOT satisfy this acknowledgement — that's deliberate. - -### Exit codes - -| Code | Meaning | -| ------ | ------------------------------------------------------------------------------------------- | -| `0` | Success, or dry-run completed cleanly | -| `1` | Usage error / bad flags / bad input file permissions / key name collision | -| `2` | Environment error: binary missing, jq missing, node unreachable, unsupported binary version | -| `3` | Multisig rejected; use offline flow | -| `4` | Pre-flight estimate returned `would_succeed=false` | -| `5` | Account already migrated (or new address already used) | -| `6` | Wrong-script or delegation-cap error | -| `7` | Broadcast succeeded but post-migration verification failed — investigate manually | -| `10` | User aborted at a confirmation prompt | +For any terminal migration, execute the exact manifest-bound helper `release_path` and chain executable through [Operator Runbook §2](operator-migration-runbook.md#2-pin-binary-home-and-keyring-provenance), then use the matching systemd/host, Docker, or Kubernetes one-shot branch in [Operator Runbook §6](operator-migration-runbook.md#6-re-run-dry-run-verify-destination-broadcast-once). The helper's flags, exit-code semantics, and account-specific preparation remain documented in [migration-scripts.md](migration-scripts.md), but the runbook's provenance, destination-prestage, stop, dry-run, and single-broadcast gates are mandatory. --- -## Method 3: Lumera CLI - -The CLI requires both keys (legacy and new) in the keyring. It handles address derivation, proof signing, gas simulation, and broadcasting automatically. - -### CLI Prerequisites - -- `lumerad` binary (post-EVM upgrade version) -- Your mnemonic (recovery phrase) -- Access to a running Lumera node (local or remote RPC endpoint) - -### CLI Step-by-Step - -Both keys must be in the keyring. The CLI extracts the public key, generates both proofs, and broadcasts automatically. - -#### 1. Pre-flight: Check Migration Eligibility - -```bash -# Check if migration is enabled -lumerad query evmigration params --node - -# Check migration estimate for your legacy address -lumerad query evmigration migration-estimate --node -``` - -The estimate response shows `would_succeed: true` if migration is possible. If `would_succeed: false`, the `rejection_reason` field explains why. - -```bash -# Check overall migration statistics -lumerad query evmigration migration-stats --node -``` - -#### 2. Prepare the EVM Destination Key - -**Import the legacy key (coin-type 118, secp256k1):** - -```bash -lumerad keys add legacy-key \ - --recover \ - --coin-type 118 \ - --key-type secp256k1 \ - --keyring-backend test -``` - -Enter your mnemonic when prompted. - -**Import the new EVM key (coin-type 60, eth_secp256k1):** - -```bash -lumerad keys add new-key \ - --coin-type 60 \ - --key-type eth_secp256k1 \ - --keyring-backend test -``` - -Securely back up the generated mnemonic. Alternatively, use `--recover` with an existing mnemonic; it may be the legacy mnemonic or a different one. - -**Verify the addresses:** - -```bash -lumerad keys show legacy-key -a --keyring-backend test -lumerad keys show new-key -a --keyring-backend test -``` - -The legacy address should match your known pre-EVM address on chain. - -#### 3. Run the Migration +## Method 3: Direct Lumera CLI -**For regular account migration:** +The direct CLI exposes the underlying query, key, proof, and broadcast semantics, but bare `lumerad` examples are not an approved production procedure. Production operators must use the manifest-pinned absolute chain executable and explicit service identity, home, keyring backend/location, chain ID, and trusted RPC established by the [Operator Runbook](operator-migration-runbook.md). -```bash -lumerad tx evmigration claim-legacy-account legacy-key new-key \ - --keyring-backend test \ - --chain-id lumera-mainnet-1 \ - --node tcp://localhost:26657 \ -``` - -**For validator migration:** - -```bash -lumerad tx evmigration migrate-validator legacy-validator-key new-validator-evm-key \ - --keyring-backend test \ - --chain-id lumera-mainnet-1 \ - --node tcp://localhost:26657 \ -``` - -The CLI will: - -1. Read both keys from the keyring, extract public keys, and derive bech32 addresses -2. Verify the legacy key is `secp256k1` (coin-type 118) -3. Build the migration payload and sign `SHA256(payload)` with the legacy key -4. Sign the new proof with the new key (must be `eth_secp256k1`) -5. Build an unsigned, fee-free Cosmos transaction -6. Simulate gas usage automatically -7. Prompt for confirmation (unless `--yes` flag is used) -8. Broadcast the transaction - -#### 4. Verify the Migration - -```bash -# Check that the migration record exists -lumerad query evmigration migration-record --node - -# Verify balances moved to the new address -lumerad query bank balances --node - -# Confirm legacy address has zero balance -lumerad query bank balances --node -``` - -#### 5. Post-Migration for Validators - -After a successful validator migration, update your node immediately: - -```bash -# 1. Import the new key into the node's production keyring if not already present -lumerad keys add new-operator-key \ - --recover \ - --coin-type 60 \ - --algo eth_secp256k1 \ - --keyring-backend file - -# 2. Restart the validator node (or however you supervise it: docker, cosmovisor, etc.) -systemctl start lumerad -``` - -> **Warning:** Your validator will miss blocks and may be jailed if you do not restart promptly after migration. Plan a maintenance window before initiating validator migration. - -#### 6. Clean Up - -After verifying the migration was successful: - -```bash -lumerad keys delete legacy-key --keyring-backend test -``` +For single-signature accounts and validators, prefer the release-pinned one-shot helper path in [Operator Runbook §6](operator-migration-runbook.md#6-re-run-dry-run-verify-destination-broadcast-once). For command semantics or troubleshooting, consult [legacy-migration.md](../evmigration/legacy-migration.md); do not copy its low-level examples without applying the runbook execution context. Validator stop/restart and final verification are defined in [validator-migration.md](validator-migration.md). --- -## Quick Reference: Query Commands - -These queries are useful before, during, and after migration: +## Quick Reference: Migration Queries -```bash -# Module parameters (is migration enabled? deadline?) -lumerad query evmigration params - -# Pre-flight estimate (what will be migrated, will it succeed?) -lumerad query evmigration migration-estimate - -# Migration record (has this address been migrated?) -lumerad query evmigration migration-record - -# Reverse lookup (find migration record by new address) -lumerad query evmigration migration-record-by-new-address - -# Global statistics (how many accounts migrated/remaining?) -lumerad query evmigration migration-stats - -# List legacy accounts still needing migration -lumerad query evmigration legacy-accounts --limit 100 - -# List completed migrations -lumerad query evmigration migrated-accounts --limit 100 -``` +The module exposes parameter, estimate, migration-record, reverse-record, global-statistics, remaining-account, and completed-account queries. Run those queries only with the manifest-pinned absolute executable and the explicit supported flags shown in [Operator Runbook §7](operator-migration-runbook.md#7-retry-boundary-query-before-any-retry); do not use an unpinned quick-reference command during a production campaign. --- @@ -681,13 +485,9 @@ Key facts (repeated here for quick reference): ## Supernode Operator Migration -Supernode operators have their own step-by-step walkthrough covering the automatic startup-migration path for single-sig supernodes and the manual `lumerad` CLI path for multisig supernodes — see [supernode-migration.md](supernode-migration.md). +This release approves only the manual one-shot migration path in the [Operator Runbook](operator-migration-runbook.md), followed by a supervised restart for local cleanup after the on-chain migration record is verified. Although the daemon contains an automatic startup-broadcast path, that path is **NOT APPROVED by this runbook or release campaign** because no exact supervisor-specific automatic-path rehearsal exists. Do not set `evm_key_name` and restart in order to broadcast. -Key facts: - -- The supernode daemon performs automatic migration on startup when `evm_key_name` is set in `config.yml` and the supernode's legacy key is single-sig. -- For multisig supernode accounts, the daemon refuses and directs you to the offline 4-step `lumerad` CLI ceremony (`generate-proof-payload` →`sign-proof` →`combine-proof` →`submit-proof`). Restart the supernode after the offline ceremony completes — the daemon detects the on-chain migration record and drives local cleanup. -- If you run a supernode on the same account as a validator operator, migrate the validator (`MsgMigrateValidator` handles the supernode side as a side-effect), then restart both `lumerad` and the supernode. +For multisig supernode accounts, use the manifest-pinned multisig helper ceremony described in [migration-scripts.md](migration-scripts.md#multisig-migration), then apply the same query-before-retry and supervised-cleanup rules. If the SuperNode account is also a validator operator, follow [validator-migration.md](validator-migration.md); `MsgMigrateValidator` handles the SuperNode record as a side effect. ## FAQ @@ -723,7 +523,7 @@ The `max_validator_delegations` parameter (default 2500) limits how many records ## Migrating a multisig account -> **Script wrapper available.** The bundled `scripts/migrate-multisig.sh` layers pre-flight, file-integrity, and post-broadcast verification onto each of the four steps below. For day-to-day use, prefer the script walkthrough at [migration-scripts.md → Multisig migration](migration-scripts.md#multisig-migration). The raw-CLI reference that follows is the canonical source for field semantics and remains useful when debugging. +> **Production execution:** Use the exact manifest-bound `migrate-multisig.sh` `release_path` through [migration-scripts.md → Multisig migration](migration-scripts.md#multisig-migration) and the [Operator Runbook](operator-migration-runbook.md). The conceptual notes below explain the proof invariants; they are not a substitute command source. Multisig legacy accounts (flat K-of-N `secp256k1`) use an offline, coordinator-driven flow with four commands. The portal wizard does not support multisig — use the CLI. @@ -755,11 +555,7 @@ The payload is identical across all co-signers; what differs is whose sub-key si **Why a multisig pubkey can be missing.** A Cosmos account only records its public key when the account *signs* an accepted transaction. An account funded at genesis, or one that has only ever *received* funds, exists on-chain with no pubkey stored. The bech32 address alone never reveals whether it was derived from a single key or a multisig — that becomes knowable only after the account signs once. This bites genesis-funded multisigs in particular: they hold a balance and look ready to migrate, but the chain has nothing to verify against. -**How to recognize the unseeded state.** Query the account: - -```bash -lumerad query auth account -``` +**How to recognize the unseeded state.** Query the account through the runbook's manifest-pinned chain executable and explicit trusted-node context. - `pub_key` is a `/cosmos.crypto.multisig.LegacyAminoPubKey` with a `public_keys` list → seeded; proceed with migration. - `pub_key: null` **and** `sequence: "0"` → the account has never signed; the multisig pubkey is not seeded. Seed it (below) before migrating. @@ -767,24 +563,7 @@ lumerad query auth account **Seeding is itself a K-of-N multisig transaction.** "Submit any transaction first" is the right idea, but for, say, a 2-of-3 multisig the seeding tx must itself be signed by at least K members and assembled as a multisig tx — a single member cannot seed it alone. A 1-ulume self-send (multisig → the same multisig address) is the cheapest option: the send amount returns to the account and only the fee is spent. -```bash -# 1. Build the unsigned self-send (use the multisig's keyring key name). -lumerad tx bank send 1ulume \ - --generate-only --chain-id > seed.json - -# 2. K members each sign independently (--multisig takes the multisig address). -lumerad tx sign seed.json --from --multisig \ - --chain-id --output-document sig1.json -lumerad tx sign seed.json --from --multisig \ - --chain-id --output-document sig2.json - -# 3. Combine the K signatures under the multisig key. -lumerad tx multisign seed.json sig1.json sig2.json \ - --chain-id > seed-signed.json - -# 4. Broadcast. Once included, the chain stores the multisig pubkey. -lumerad tx broadcast seed-signed.json --node -``` +The exact K-of-N self-send build, member-sign, multisign, and broadcast procedure must come from the manifest-pinned multisig method guide. Treat its broadcast as an irreversible boundary and apply query-before-retry. Re-run the `auth account` query and confirm `pub_key` is now a `LegacyAminoPubKey` listing all sub-keys. @@ -799,32 +578,12 @@ When using raw `lumerad tx broadcast`, inspect the returned JSON `code`. The CLI ### Step 1: Coordinator generates the proof payload template -The destination of a K-of-N legacy multisig is **also** a K-of-N multisig, built from fresh `eth_secp256k1` sub-keys (mirror-source rule — see [evmigration/main.md → Multisig account migration](../evmigration/main.md#multisig-account-migration)). Each co-signer generates their own eth sub-key; the coordinator collects the N eth pubkeys (or local key-names) and runs: - -```bash -lumerad tx evmigration generate-proof-payload \ - --legacy \ - --new-sub-pub-keys ,, \ - --new-threshold 2 \ - --kind claim \ - --chain-id \ - --keyring-backend \ - --out proof.json -``` +The destination of a K-of-N legacy multisig is **also** a K-of-N multisig, built from fresh `eth_secp256k1` sub-keys (mirror-source rule — see [evmigration/main.md → Multisig account migration](../evmigration/main.md#multisig-account-migration)). Each co-signer generates their own eth sub-key; the coordinator collects the N eth pubkeys (or local key names) and creates the payload with the manifest-pinned multisig helper and explicit runbook execution context. - `--new-sub-pub-keys` entries are either local keyring key names (eth_secp256k1) or base64-encoded 33-byte compressed eth pubkeys. Mix freely. `--new-threshold` is required with `--new-sub-pub-keys`. - **Member order is significant — pass `--nosort` when building the destination key.** `generate-proof-payload` preserves the order you list `--new-sub-pub-keys` (it does not sort), and the signer index is the position in that list. Because the mirror-source rule requires `legacy_proof.signer_indices == new_proof.signer_indices`, list the eth sub-keys in the **same member order as the legacy multisig's `public_keys`** (`lumerad query auth account `), so each co-signer holds the same signer index on both sides. - > **⚠️ When pre-creating the destination composite with `lumerad keys add --multisig`, you MUST pass `--nosort`.** The default behavior is to sort sub-pubkeys by bytes, and because legacy `secp256k1` and new `eth_secp256k1` pubkey bytes sort differently, the default sort produces a destination whose member order does not mirror the legacy side. Co-signers will then fail at `sign-proof` with a "signer index mismatch" error and you'll have to rebuild the destination key and regenerate `proof.json`. Always: - > - > ```bash - > lumerad keys add \ - > --multisig=,,..., \ - > --multisig-threshold= \ - > --nosort - > ``` - > - > where the `` order matches the legacy multisig's on-chain `public_keys` order. + > **⚠️ Destination construction must disable member sorting.** The default byte sort can reorder legacy and destination members differently. Follow the exact `--nosort` construction in the manifest-pinned multisig method guide, preserving the legacy on-chain `public_keys` order. > - For same-mnemonic migrations, signer index 0's legacy mnemonic should be used to recover signer index 0's EVM sub-key, signer index 1's legacy mnemonic should be used for signer index 1's EVM sub-key, and so on. Reordering the same EVM sub-keys produces a different destination multisig address. - `--new ` is optional; the CLI derives the new multisig address from the sub-keys/threshold and cross-checks `--new` if supplied. @@ -837,16 +596,7 @@ The output `proof.json` is a v2 `PartialProof` with two sibling `SideSpec`s (`le ### Step 2: Each co-signer signs both sides on their own machine -Each co-signer holds their legacy Cosmos sub-key **and** their destination-side eth sub-key in the same keyring, and signs both sides in one invocation: - -```bash -lumerad tx evmigration sign-proof proof.json \ - --from \ - --new-key \ - --keyring-backend \ - --chain-id \ - --out my-partial.json -``` +Each co-signer holds their legacy Cosmos sub-key **and** destination-side eth sub-key in the same explicit keyring and signs both sides through the manifest-pinned multisig helper. - `--from` signs the legacy half; `--new-key` signs the new half. At least one is required. A co-signer who holds only one sub-key may pass just that flag, but **one-sided partials do not count toward quorum by themselves** — the consensus mirror-source rule requires the same K signer positions to approve both halves, so combine-proof only counts an index that has a valid signature on *both* sides. One-sided partials contribute only when another co-signer supplies the other-side signature at the same index. - `sign-proof` is idempotent: re-running with the same key replaces that signer's entry on the corresponding side. @@ -857,32 +607,20 @@ Each co-signer sends their `*-partial.json` back to the coordinator. ### Step 3: Coordinator combines the partials -```bash -lumerad tx evmigration combine-proof \ - alice-partial.json bob-partial.json \ - --out tx.json -``` +The coordinator combines reviewed partials through the manifest-pinned multisig helper; no abbreviated combine invocation is approved here. `combine-proof` validates cross-file consistency — it rejects the set if any two partials disagree on `chain_id`, `evm_chain_id`, `legacy_address`, `new_address`, `payload_hex`, `kind`, or the per-side `threshold` / `sig_format` / `sub_pub_keys`. It verifies every partial signature cryptographically on **both** sides, drops invalid entries with a stderr warning, then **intersects** the valid signer-index sets across the two sides and selects the first K indices present on BOTH. This is what satisfies the consensus mirror-source rule (`legacy_proof.signer_indices == new_proof.signer_indices`). A one-sided partial (e.g. co-signer Alice signed only the legacy side) does not count toward quorum unless another co-signer supplied a new-side signature at the same index. If the intersection has fewer than K entries, it errors with `need valid partial signatures signed on BOTH sides at matching indices, have ` and writes nothing. ### Step 4: Broadcast the assembled transaction -```bash -lumerad tx evmigration submit-proof tx.json \ - --chain-id \ - --node -y -``` - -Migration messages declare **zero signers** — authorization is embedded in `legacy_proof` and `new_proof`, fees are waived by the evmigration ante handler, and replay is prevented by the keeper's migration-record check. There is no `--from` and no envelope signature; `submit-proof` loads `tx.json`, runs `ValidateBasic`, simulates gas via the migration-specific estimator, builds an unsigned tx, and broadcasts. On success, verify the migration record: +Submission uses the exact manifest-pinned helper and chain executable from the runbook, after the required stop proof and dry-run/review gates. Submit once. -```bash -lumerad query evmigration migration-record -``` +Migration messages declare **zero signers** — authorization is embedded in `legacy_proof` and `new_proof`, fees are waived by the evmigration ante handler, and replay is prevented by the keeper's migration-record check. There is no `--from` and no envelope signature. On success, verify the migration record with [Operator Runbook §7](operator-migration-runbook.md#7-retry-boundary-query-before-any-retry). ### Notes - **Legacy-side threshold and members** are defined by the on-chain `LegacyAminoPubKey` and read automatically; you don't pass them as flags. **New-side threshold and members** are supplied by `--new-sub-pub-keys` + `--new-threshold` because the destination multisig doesn't exist on-chain yet. - **Cold-wallet / nil-pubkey single-sig accounts**: if a *single-key* (non-multisig) legacy account has never signed a transaction, use `generate-proof-payload --legacy-key ` to seed the pubkey from a local key. This is distinct from the multisig flow — multisig accounts must have their multisig pubkey already populated on-chain. - **Non-EVM-addressable destination.** The new multisig bech32 can perform Cosmos-side operations (staking, supernode, IBC, authz) but cannot originate `MsgEthereumTx`. Operators who want EVM DeFi access for rewards should configure a separate single-EOA withdraw address via `MsgSetWithdrawAddress`. -- **Supernode operators** have their own step-by-step walkthrough for both the single-sig automatic path and the multisig manual path — see [supernode-migration.md](supernode-migration.md). +- **Supernode operators** must use the manual one-shot campaign path in [supernode-migration.md](supernode-migration.md); automatic startup broadcast is not approved by this release. - **After a successful migration** follow the same post-migration steps as for any other account (add the new Lumera EVM chain definition to Keplr, verify balances at the new address, etc.). diff --git a/docs/evm-integration/user-guides/operator-migration-runbook.md b/docs/evm-integration/user-guides/operator-migration-runbook.md new file mode 100644 index 00000000..75ac3133 --- /dev/null +++ b/docs/evm-integration/user-guides/operator-migration-runbook.md @@ -0,0 +1,408 @@ +# EVM Migration Operator Runbook + +**Audience:** validators, supernodes, relayers, and custodial account operators +**Scope:** executable safety gates around the existing migration helpers; this does not replace account-specific guides. + +Migration is irreversible after the transaction is included. Record commands and public addresses, but never record mnemonics, private keys, keyring passphrases, bearer tokens, or raw environment dumps. + +## 1. Discover the process before changing it + +Set an evidence directory on an encrypted operator-controlled volume: + +```bash +umask 077 +EVIDENCE_DIR="$HOME/evmigration-evidence/$(date -u +%Y%m%dT%H%M%SZ)" +mkdir -p "$EVIDENCE_DIR" +``` + +### systemd + +```bash +sudo systemctl show lumerad \ + -p User -p Group -p FragmentPath -p ExecStart -p WorkingDirectory \ + | tee "$EVIDENCE_DIR/systemd-lumerad.txt" +sudo systemctl cat lumerad | tee "$EVIDENCE_DIR/systemd-lumerad-unit.txt" +``` + +From `User`, `ExecStart`, and the unit environment, identify the service user, exact executable, `--home`/base directory, config directory, and keyring backend/location. Run all helper/keyring commands as that service user. Do not assume the interactive user's `$HOME` or keyring. + +### Docker + +```bash +docker inspect \ + --format '{{json .Config.User}} {{json .Path}} {{json .Args}} {{json .Mounts}} {{json .Config.Image}}' \ + | tee "$EVIDENCE_DIR/docker-lumerad.txt" +docker image inspect --format '{{json .RepoDigests}}' \ + | tee "$EVIDENCE_DIR/docker-image-digests.txt" +``` + +Identify the container user, command/arguments, mounted home/config/keyring paths, and immutable image digest. Run the helper in an image/container with those same mounts and identity. + +### Kubernetes + +```bash +kubectl -n get statefulset/ -o yaml \ + | tee "$EVIDENCE_DIR/kubernetes-workload.yaml" +kubectl -n get pod -o jsonpath='{range .spec.containers[*]}{.name}{" user="}{.securityContext.runAsUser}{" command="}{.command}{" args="}{.args}{" mounts="}{.volumeMounts}{"\n"}{end}' \ + | tee "$EVIDENCE_DIR/kubernetes-runtime.txt" +``` + +Identify `runAsUser`, command/args, PVC/config/secret mounts, base directory, and keyring location. The captured YAML is sensitive operational metadata even though it must not contain Secret values; restrict the evidence directory. + +## 2. Pin binary, home, and keyring provenance + +Use the exact release executables and explicit flags. The helpers resolve `--binary` once to an absolute canonical path, then print that path, the actual `version --long` version, SHA-256, and source before doing key work. They print keyring backend and keyring location separately, with each source. Independently verify all four executable records in the manifest: `artifacts.chain_executable`, `artifacts.supernode_executable`, `artifacts.sncli_executable`, and `artifacts.relayer_executable`, including each exact `release_path`, version, tag, commit, SHA-256, and source. Every `artifacts.bound_files` record also binds its exact runtime `release_path`; approval is deployment-specific, so those paths must be the paths this procedure actually executes. For container procedures, also pull and inspect the exact `artifacts.container_image.artifact@artifacts.container_image.digest` from `artifacts.container_image.source`; a mutable tag alone is not approved. + +```bash +MANIFEST=/absolute/path/to/approved/compatibility-manifest.json +LUMERAD=$(jq -er '.artifacts.chain_executable.release_path' "$MANIFEST") +MIGRATE_VALIDATOR=$(jq -er '.artifacts.bound_files["scripts/migrate-validator.sh"].release_path' "$MANIFEST") +test "${LUMERAD#/}" != "$LUMERAD" && test "${MIGRATE_VALIDATOR#/}" != "$MIGRATE_VALIDATOR" +printf '%s %s\n' \ + "$(jq -er '.artifacts.chain_executable.sha256' "$MANIFEST")" "$LUMERAD" \ + "$(jq -er '.artifacts.bound_files["scripts/migrate-validator.sh"].sha256' "$MANIFEST")" "$MIGRATE_VALIDATOR" \ + | sha256sum -c - +"$LUMERAD" version --long | tee "$EVIDENCE_DIR/lumerad-version.txt" +sha256sum "$LUMERAD" | tee "$EVIDENCE_DIR/lumerad.sha256" + +sudo -u "$MIGRATE_VALIDATOR" \ + \ + --binary "$LUMERAD" \ + --home /absolute/lumera/home \ + --keyring-backend \ + --keyring-dir /absolute/keyring/location \ + --chain-id \ + --node \ + --i-have-stopped-the-node \ + --dry-run +``` + +For a non-validator account, use `migrate-account.sh` and omit `--i-have-stopped-the-node`. Do not continue if the displayed binary, version, checksum, service user, home, backend, or location differs from the approved compatibility manifest. + +## 3. Back up configuration (mode 0600) + +Back up configuration, not mnemonics or private keys, before stopping: + +```bash +umask 077 +install -m 0600 /absolute/lumera/home/config/config.toml \ + "$EVIDENCE_DIR/config.toml.before" +install -m 0600 /absolute/lumera/home/config/app.toml \ + "$EVIDENCE_DIR/app.toml.before" +stat -c '%a %U:%G %n' "$EVIDENCE_DIR"/*.before +sha256sum "$EVIDENCE_DIR"/*.before > "$EVIDENCE_DIR/config-before.sha256" +``` + +For a supernode or Hermes relayer, use the discovered config path in place of these examples. Review copies before sharing: endpoints and topology may be sensitive. Never copy keyring contents into this evidence directory. + +## 4. Pre-stage and prove the destination before downtime + +**Required PR-2 compatibility dependency:** use only the release's approved destination pre-stage operation that implements the PR-2 no-echo contract. That operation must read the mnemonic from a hidden TTY or protected input file descriptor, never from argv, never echo it, never enable shell tracing, and print only non-secret key metadata. This PR-3 runbook does not claim that an unfinished PR-2 command exists in the current binary; if the release compatibility manifest does not name and hash an implementation of this contract, stop. + +The approved manifest binds the executable exactly once, as `argv[0]` of its full argv. Verify that absolute path's hash and execute the array without `eval` or operator substitutions: + +```bash +PRESTAGE_SHA256=$(jq -er '.operator_contracts.destination_prestage_no_echo.implementation.sha256' "$MANIFEST") +mapfile -t PRESTAGE_ARGV < <(jq -er '.operator_contracts.destination_prestage_no_echo.implementation.argv[]' "$MANIFEST") +PRESTAGE_IMPL=${PRESTAGE_ARGV[0]:-} +test "${PRESTAGE_IMPL#/}" != "$PRESTAGE_IMPL" +test "${#PRESTAGE_ARGV[@]}" -gt 0 +printf '%s %s\n' "$PRESTAGE_SHA256" "$PRESTAGE_IMPL" | sha256sum -c - + +# The implementation itself must obtain the mnemonic through its manifest-approved +# hidden-TTY or protected-FD contract. Never append mnemonic material to this argv. +sudo -u -- "${PRESTAGE_ARGV[@]}" +``` + +Record the exact argv and sanitized public result in approval evidence. If the release's protected-FD form requires input redirection, use only the separately approved custody procedure; redirection must not alter argv or expose the mnemonic. + +After PR-2 pre-staging, verify the destination locally using the same binary/home/backend/location: + +```bash +DEST_JSON=$( + sudo -u "$LUMERAD" keys show \ + --output json \ + --home /absolute/lumera/home \ + --keyring-backend \ + --keyring-dir /absolute/keyring/location +) +printf '%s\n' "$DEST_JSON" | jq '{name,address,type:(.type // .pubkey."@type" // .pubkey.type_url)}' +DEST_ADDR=$(printf '%s\n' "$DEST_JSON" | jq -er '.address') +``` + +The destination must be coin type 60 / `eth_secp256k1`, controlled and recoverable by the operator, and fresh on-chain. Run the helper dry-run and retain its public-address output. It checks key types, migration indexes, destination account freshness, and the migration estimate. A destination mismatch or unknown key type is a hard stop. + +## 5. Stop and prove stopped + +### systemd + +```bash +sudo systemctl stop lumerad +sudo systemctl is-active --quiet lumerad && { echo 'lumerad still active' >&2; exit 1; } || true +sudo systemctl show lumerad -p ActiveState -p SubState -p MainPID +pgrep -a -u -f '(^|/)lumerad( |$)' && { echo 'lumerad process remains' >&2; exit 1; } || true +``` + +### Docker + +```bash +docker stop --time 60 +test "$(docker inspect -f '{{.State.Running}}' )" = false +docker inspect -f '{{.State.Status}} {{.State.ExitCode}} {{.State.FinishedAt}}' +``` + +Disable or account for an external restart policy before manual replacement; do not start a second process with the same consensus key. + +### Kubernetes + +```bash +kubectl -n scale statefulset/ --replicas=0 +kubectl -n wait --for=delete pod/ --timeout=120s +! kubectl -n get pod -l app=