Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs/evm-integration/user-guides/migration-scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Comment thread
mateeullahmalik marked this conversation as resolved.

> **⚠️ 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 <legacy-multisig-address> -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 <member>_evm to keep the mapping obvious.

# 3. Build the destination multisig in matching order, WITH --nosort.
lumerad keys add <new-multisig-key> \
--multisig=<member-1>_evm,<member-2>_evm,...,<member-N>_evm \
--multisig-threshold=<K> \
--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
Expand Down
13 changes: 12 additions & 1 deletion docs/evm-integration/user-guides/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <multisig-bech32>`), 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 <multisig-bech32>`), 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 <new-multisig-key> \
> --multisig=<eth-sub-1>,<eth-sub-2>,...,<eth-sub-N> \
> --multisig-threshold=<K> \
> --nosort
> ```
>
> where the `<eth-sub-i>` order matches the legacy multisig's on-chain `public_keys` order.
- `--new <bech32>` 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:<chain-id>:<evm-chain-id>:<kind>:<legacy>:<new>` embeds the chain ID. An empty or wrong `--chain-id` makes every sub-signature fail verification with `sub-sig 0 invalid`.
Expand Down
41 changes: 41 additions & 0 deletions scripts/migrate-multisig.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
Expand All @@ -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
Comment thread
mateeullahmalik marked this conversation as resolved.

# 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 <legacy-multisig-address> -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 <new-multisig-key> -y # if it already exists"
log_error " lumerad keys add <new-multisig-key> \\"
log_error " --multisig=<eth-sub-1>,<eth-sub-2>,...,<eth-sub-N> \\"
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
Expand Down
Loading