From 5717000ba665460cffacb69fd3a7bef2b2666465 Mon Sep 17 00:00:00 2001 From: Matee ullah Malik Date: Tue, 28 Jul 2026 03:28:08 +0000 Subject: [PATCH 01/11] feat(evmigration): harden operator provenance safeguards --- .../compatibility-manifest.schema.json | 342 +++++++++++++++++ .../compatibility-manifest.template.json | 151 ++++++++ .../owned-testnet-baseline.template.json | 76 ++++ docs/evm-integration/user-guides/migration.md | 2 + .../user-guides/operator-migration-runbook.md | 363 ++++++++++++++++++ .../user-guides/relayer-migration.md | 77 ++-- .../user-guides/supernode-migration.md | 17 +- .../user-guides/validator-migration.md | 31 +- scripts/evmigration-common.sh | 88 ++++- scripts/migrate-account.sh | 2 +- scripts/migrate-validator.sh | 2 +- tests/scripts/common.bats | 121 ++++++ tests/scripts/fixtures/lumerad-shim.sh | 7 + tests/scripts/migrate-account.bats | 41 ++ tests/scripts/migrate-batch.bats | 5 + 15 files changed, 1257 insertions(+), 68 deletions(-) create mode 100644 docs/evm-integration/operator-artifacts/compatibility-manifest.schema.json create mode 100644 docs/evm-integration/operator-artifacts/compatibility-manifest.template.json create mode 100644 docs/evm-integration/operator-artifacts/owned-testnet-baseline.template.json create mode 100644 docs/evm-integration/user-guides/operator-migration-runbook.md 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..bcc6da0a --- /dev/null +++ b/docs/evm-integration/operator-artifacts/compatibility-manifest.schema.json @@ -0,0 +1,342 @@ +{ + "$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", "bound_files"], + "properties": { + "chain_executable": { "$ref": "#/$defs/executable" }, + "supernode_executable": { "$ref": "#/$defs/executable" }, + "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": { + "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" } + } + } + } + } + } + ], + "$defs": { + "non_placeholder": { + "type": "string", + "minLength": 3, + "pattern": "^(?!REPLACE(?:_|$))(?!.*<[^>]+>)(?!PLACEHOLDER$)(?!TBD$)(?!TODO$)(?!UNKNOWN$).+\\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": ["sha256", "source"], + "properties": { + "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "source": { "type": "string", "minLength": 1 } + } + }, + "approved_file": { + "type": "object", + "properties": { + "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": { "type": "string", "minLength": 1 }, + "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" } + } + }, + "pr2_implementation": { + "type": "object", + "additionalProperties": false, + "required": ["name", "tag", "commit", "sha256", "source", "no_echo_contract"], + "properties": { + "name": { "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" }, + "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" }, + "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..5cdbf69d --- /dev/null +++ b/docs/evm-integration/operator-artifacts/compatibility-manifest.template.json @@ -0,0 +1,151 @@ +{ + "$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" + }, + "bound_files": { + "scripts/evmigration-common.sh": { + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "source": "REPLACE_WITH_CHAIN_RELEASE_ASSET#scripts/evmigration-common.sh" + }, + "scripts/migrate-account.sh": { + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "source": "REPLACE_WITH_CHAIN_RELEASE_ASSET#scripts/migrate-account.sh" + }, + "scripts/migrate-validator.sh": { + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "source": "REPLACE_WITH_CHAIN_RELEASE_ASSET#scripts/migrate-validator.sh" + }, + "scripts/migrate-multisig.sh": { + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "source": "REPLACE_WITH_CHAIN_RELEASE_ASSET#scripts/migrate-multisig.sh" + }, + "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": { + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "source": "REPLACE_WITH_CHAIN_RELEASE_ASSET#docs/evm-integration/user-guides/operator-migration-runbook.md" + }, + "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": { + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "source": "REPLACE_WITH_CHAIN_RELEASE_ASSET#docs/evm-integration/user-guides/migration-scripts.md" + }, + "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": { + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "source": "REPLACE_WITH_CHAIN_RELEASE_ASSET#docs/evm-integration/user-guides/supernode-migration.md" + }, + "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": { + "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.", + "Extract each release asset into a new mode-0700 directory. Verify the chain and SuperNode executable tag, commit, version, source, and SHA-256, then verify every artifacts.bound_files entry 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..aebe3177 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 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..fa015a65 --- /dev/null +++ b/docs/evm-integration/user-guides/operator-migration-runbook.md @@ -0,0 +1,363 @@ +# 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 executable 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. + +```bash +LUMERAD=/absolute/path/to/approved/lumerad +"$LUMERAD" version --long | tee "$EVIDENCE_DIR/lumerad-version.txt" +sha256sum "$LUMERAD" | tee "$EVIDENCE_DIR/lumerad.sha256" + +sudo -u ./scripts/migrate-validator.sh \ + \ + --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. + +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=