diff --git a/docs/evm-integration/user-guides/migration-scripts.md b/docs/evm-integration/user-guides/migration-scripts.md index bc13123c..1d47745d 100644 --- a/docs/evm-integration/user-guides/migration-scripts.md +++ b/docs/evm-integration/user-guides/migration-scripts.md @@ -538,6 +538,31 @@ The coordinator does not need signing keys. Co-signers sign locally. - The new multisig destination address must be fresh. - For multisig validators, stop the validator node before`submit`. +> **⚠️ Critical: build the destination multisig with `--nosort`.** Cosmos SDK's `keys add --multisig` sorts sub-pubkeys by bytes by default. Because legacy `secp256k1` and new `eth_secp256k1` pubkey bytes sort differently, the default sort breaks the mirror-source invariant — co-signers will fail at `sign` with a "signer index mismatch" error and you'll have to rebuild the destination key and regenerate `proof.json`. Always pass `--nosort` and list members in the same order as the legacy multisig's on-chain `public_keys`. See [Step 0](#0-build-the-destination-evm-multisig) below. + +### 0. Build the destination EVM multisig + +Run this before `generate`. Each member who holds a legacy sub-key must already have created an `eth_secp256k1` sub-key in their local keyring; collect the N member key names (or base64 pubkeys) and assemble the composite **in the same order as the legacy multisig** and **with `--nosort`**: + +```bash +# 1. Read legacy member order from chain (this is the canonical reference order). +lumerad query auth account -o json \ + | jq -r '.account.value.pub_key.public_keys[].key' + +# 2. For each legacy member at index i, identify their eth_secp256k1 sub-key. +# Convention: name them _evm to keep the mapping obvious. + +# 3. Build the destination multisig in matching order, WITH --nosort. +lumerad keys add \ + --multisig=_evm,_evm,...,_evm \ + --multisig-threshold= \ + --nosort +``` + +`--nosort` preserves the order you list, so each member ends up at the same signer index on both sides — which is what the on-chain mirror-source rule requires and what `combine` will check. + +If you skip `--nosort` (or list members in a different order than the legacy multisig), `migrate-multisig.sh sign` will reject the partial with a clear "signer-index mismatch" error and the exact rebuild command — fix it, then re-run `generate`. + ### 1. Coordinator: Generate ```bash diff --git a/docs/evm-integration/user-guides/migration.md b/docs/evm-integration/user-guides/migration.md index 27baa83e..e8131dca 100644 --- a/docs/evm-integration/user-guides/migration.md +++ b/docs/evm-integration/user-guides/migration.md @@ -677,7 +677,18 @@ lumerad tx evmigration generate-proof-payload \ ``` - `--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.** `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. If you also pre-create the destination composite with `lumerad keys add --multisig`, pass `--nosort` so its derived address matches this order-preserving derivation. +- **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. - `--new ` is optional; the CLI derives the new multisig address from the sub-keys/threshold and cross-checks `--new` if supplied. - `--kind claim` targets `MsgClaimLegacyAccount`; `--kind validator` targets `MsgMigrateValidator`. - `--chain-id` is **required**: the payload string `lumera-evm-migration:::::` embeds the chain ID. An empty or wrong `--chain-id` makes every sub-signature fail verification with `sub-sig 0 invalid`. diff --git a/scripts/migrate-multisig.sh b/scripts/migrate-multisig.sh index 30062c4e..84fda57e 100755 --- a/scripts/migrate-multisig.sh +++ b/scripts/migrate-multisig.sh @@ -338,6 +338,7 @@ S_USAGE local pjson pjson=$(read_proof_file "$input") + local from_idx="" new_idx="" if [[ -n "$from" ]]; then local from_pubkey listed from_pubkey=$(key_pubkey_b64 "$from") @@ -346,6 +347,7 @@ S_USAGE log_error "--from '$(legacy_value "$from")' pubkey is not among legacy.sub_pub_keys in $input" exit 1 fi + from_idx=$(jq -r --arg pk "$from_pubkey" '.legacy.sub_pub_keys | index($pk) // empty' <<<"$pjson") fi if [[ -n "$new_key" ]]; then local new_pubkey listed_new @@ -355,6 +357,45 @@ S_USAGE log_error "--new-key '$(new_value "$new_key")' pubkey is not among new.sub_pub_keys in $input" exit 1 fi + new_idx=$(jq -r --arg pk "$new_pubkey" '.new.sub_pub_keys | index($pk) // empty' <<<"$pjson") + fi + + # Multisig-to-multisig signer-index alignment pre-check. When a co-signer + # passes BOTH --from and --new-key, the two keys must occupy the same signer + # position in their respective multisigs (mirror-source rule). The on-chain + # consensus check enforces this, and `lumerad sign-proof` rejects mismatches + # with a terse error. We catch it earlier with an actionable remediation so + # the operator doesn't have to round-trip through cryptic CLI output. + # + # Root cause when this fires: the destination EVM multisig was built without + # --nosort, so cosmos-sdk sorted its eth_secp256k1 sub-pubkeys by bytes — + # and that byte-order differs from the legacy secp256k1 byte-order. Result: + # member N's legacy key and EVM key land at different indices. + if [[ -n "$from_idx" && -n "$new_idx" && "$from_idx" != "$new_idx" ]]; then + local legacy_threshold + legacy_threshold=$(jq -r '.legacy.threshold // .new.threshold // "K"' <<<"$pjson") + log_error "signer-index mismatch: legacy key '$(legacy_value "$from")' is at index $from_idx," + log_error " but new key '$(new_value "$new_key")' is at index $new_idx in $input" + log_error "" + log_error "Multisig migration requires the same signer position on both sides." + log_error "The destination EVM multisig was almost certainly built WITHOUT --nosort," + log_error "so its sub-pub-keys were re-sorted by bytes and no longer mirror the legacy" + log_error "member order." + log_error "" + log_error "Remediation — rebuild the destination multisig in legacy member order:" + log_error " 1. Get legacy member order from chain:" + log_error " lumerad query auth account -o json \\" + log_error " | jq -r '.account.value.pub_key.public_keys[].key'" + log_error " 2. Identify each member's eth_secp256k1 sub-key (in the SAME order)." + log_error " 3. Recreate the destination key WITH --nosort:" + log_error " lumerad keys delete -y # if it already exists" + log_error " lumerad keys add \\" + log_error " --multisig=,,..., \\" + log_error " --multisig-threshold=${legacy_threshold} \\" + log_error " --nosort" + log_error " 4. Re-run 'migrate-multisig.sh generate' with the rebuilt key, then redistribute" + log_error " the fresh proof.json to co-signers." + exit 11 fi # Pass through to lumerad tx evmigration sign-proof. At least one of