fix(evmigration): mempool-accept zero-signer migration txs#167
Conversation
The cosmos/evm ExperimentalEVMMempool routes non-EVM txs through a PriorityNonceMempool that, by default, uses DefaultSignerExtractionAdapter. That adapter calls tx.GetSignaturesV2() and refuses any tx with an empty signature set, returning 'tx must have at least one signer' from PriorityNonceMempool.Insert. MsgClaimLegacyAccount and MsgMigrateValidator are zero-signer by design: authorization lives in the proof bytes, fees are waived by EVMigrationFeeDecorator, and the migration-aware ante chain (app/evm/ante.go: migrationCosmosAnte) accepts the shape. That ante chain, however, runs AFTER mempool insert. Without a migration-aware signer extractor, every submit-proof broadcast is rejected at the mempool layer before ante ever sees it -- including the canonical combined-tx.json shape produced by the offline multisig flow. This change: * Adds app/evmigration_signer_extraction_adapter.go: a SignerExtractionAdapter that returns a synthetic SignerData built from the message's legacy_address for IsEVMigrationOnlyTx, and delegates everything else to a fallback (default for the Cosmos pool, EVM-aware for proposal building). * Wires the adapter into ExperimentalEVMMempool.CosmosPoolConfig and into NewDefaultProposalHandler's signer extraction adapter so it applies on both Insert and PrepareProposal paths. * Replicates upstream's default PriorityNonceMempoolConfig (priority by gas-price) locally so the adapter override is the only behavior change. Short-circuits priority calc for zero-fee/zero-gas txs so it doesn't touch EVM keeper state for migration txs. Tests: * app/evmigration_signer_extraction_adapter_test.go: 7 unit tests pinning synthetic-signer derivation, fallback delegation for non-migration and mixed txs, empty/invalid legacy_address rejection, and nil-fallback safety. * app/evm_mempool_evmigration_test.go: 2 integration tests on the real App + real ExperimentalEVMMempool. One asserts Insert accepts a zero-signer MsgClaimLegacyAccount and CountTx() increments. The other pins the regression: the SDK default adapter still returns zero signers for the same tx, which is precisely what makes PriorityNonceMempool reject without this fix. Docs: * CHANGELOG.md entry under v1.20.0 explaining the fix. * docs/evm-integration/user-guides/migration.md zero-signer-submit callout updated to point at the adapter file. Discovered during v1.20.0-rc4 multisig migration rehearsal (PR #163 migrate-batch.sh end-to-end). Reproduces with migrate-multisig.sh submit, migrate-account.sh, and hand-built lumerad tx broadcast. Verified: go test -tags=test ./app/... green, app package tests pass (14.8s).
|
✅ Confirmed on live binaries — both sides provenResolved the contradiction. The bug is real, the fix does work, and I have end-to-end proof on the live devnet. What I did
Why earlier walkthroughs didn't catch thisOur remote devnet at 3.133.124.60 — where I previously verified validator migration in a June 15 walkthrough and where if cosmosPoolMaxTx < 0 {
logger.Debug("app-side mempool is disabled, skipping EVM mempool configuration")
return nil
}With the mempool disabled, the buggy default-signer-extractor code path is never reached, and CheckTx admits zero-signer migration txs straight to the ante chain. Local devnet uses the default Demoting back to readyRestoring this PR to ready-for-review. Test plan in the body still applies. Verified on live chain in addition to the in-process tests. |
…urity pin Addresses Kay's review feedback on PR #167 ("need more tests for all that"). Three new tests replace the prior thin mempool.Insert direct call: 1. TestEVMMempool_CheckTxAcceptsZeroSignerMigrationTx Drives a valid zero-signer migration tx through the SAME app.CheckTx entry point that 'lumerad tx evmigration submit-proof' hits on live mainnet. Asserts response code 0 and that the log NEVER contains 'at least one signer'. This is the production regression pin. 2. TestEVMMempool_CheckTxRejectsZeroSignerNonMigrationTx Security pin for the worry that the SignerExtractionAdapter widens the hole: submits a zero-signer banktypes.MsgSend through the same CheckTx entry point and asserts it is REJECTED. Proves the adapter only synthesizes signers for migration-only txs and that all other message types still require envelope signatures. 3. TestEVMigrationSignerAdapter_DefaultExtractor_PinsFailureMode Documents the upstream SDK behavior that necessitates the custom adapter — default extractor returns empty []SignerData on a zero-signer migration tx. If this ever changes upstream, we can remove the workaround. Regression-pin verified locally by temporarily reverting app/evm_mempool.go to master: test #1 fails with the exact production error, test #2 still passes (confirming no widening), then restored.
|
@andkay Pushed test coverage you asked for in 236b7aa. Three new tests: 1. 2. 3. Regression-pin verified by reverting: locally swapped All app tests green: Bats coverage for the script-level submit-proof path against a real binary is a bigger change — happy to do it in a follow-up PR so we can land this fix in time for testnet/mainnet windows. |
… + harden mempool tests
Admitting zero-signer migration txs to the app mempool (the signer-extraction
adapter) also opened a zero-fee spam vector: migration txs carry no fee and no
envelope signature, so anyone could flood the mempool/proposals with proof-valid
txs that only fail at message execution. Enforce the migration admission window
at the ante so these are rejected before mempool insertion.
- x/evmigration/keeper/ante.go: VerifyMigrationProofsForAnte now rejects with
ErrMigrationDisabled / ErrMigrationWindowClosed when EnableMigration is off or
MigrationEndTime has passed (mirrors preChecks steps 1-2). Single param read,
no per-account state; no-op under default params (enabled, no deadline). On
mainnet a concrete MigrationEndTime bounds the exposure to the migration
window and closes it automatically. Message execution still re-checks.
- ante_test.go: TestVerifyMigrationProofsForAnte_AdmissionGate (disabled /
window-closed / open-window).
Review hardening of the mempool test suite:
- Renamed TestEVMigrationMalformedLegacyAddressRejected* -> ...ByValidateBasic
and documented that it pins the ante ValidateBasic layer, not the adapter.
- Made CheckTxRejectsZeroSignerNonMigrationTx assert the rejecting layer
("no signatures supplied") instead of just code != 0, and added
InsertRejectsZeroSignerNonMigrationTx as the true adapter-layer pin (drives
mempool.Insert directly, bypassing the ante).
- Documented the gas==0 div-by-zero hardening in defaultCosmosPoolConfig.
- Track the previously-untracked real-node integration test so the branch
matches the docs/CHANGELOG references.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…talog The adapter rows in unit-evmigration.md referenced names that no longer match the actual test functions in app/evmigration_signer_extraction_adapter_test.go: _ClaimLegacyAccount -> _MigrationOnlyTx_SyntheticSigner _MigrateValidator -> _MigrationOnlyTx_MigrateValidator _NonMigration_DelegatesToFallback -> _NonMigrationTx_DelegatesToFallback _InvalidLegacyAddress_Rejected -> _InvalidBech32_Rejected and the _NilFallback_FallsBackToDefault test was missing entirely. Names now match the source 1:1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…+ align tests/docs
Builds on the migration-window gate by adding the cheap state-plausibility
checks to the ante, so proof-valid-but-impossible zero-fee migration txs are
rejected before mempool admission rather than only at message execution. This
closes the in-window leg of the zero-fee mempool-spam vector: a fabricated
keypair has no on-chain legacy account, so it is now rejected at the ante.
Implementation (x/evmigration/keeper/ante.go):
- VerifyMigrationProofsForAnte now runs verifyMigrationAdmissionState after the
enable/window gate and before proof verification. It mirrors the cheap subset
of msgServer.preChecks: addresses-differ, source-not-already-migrated,
new-address-not-a-migrated-legacy, destination-not-reused, legacy-account
exists and is not a module account, and (for MsgMigrateValidator) the source
is a validator operator. The per-block rate limit is intentionally omitted
from the ante (it is block-stateful and belongs only at execution).
- Ordering matters: state checks run before proof verification, so an invalid
proof on a nonexistent account surfaces the state error first. The keeper
still re-checks everything at execution; the ante is a best-effort mempool
filter.
Tests:
- x/evmigration/keeper/ante_test.go: TestVerifyMigrationProofsForAnte_AdmissionGate
(disabled / window-closed / open-window) and _CheapStateAdmission (nonexistent
legacy, already-migrated, reused destination, non-validator source), with mock
expectations pinning the check ordering. Existing subtests seed the legacy
account so they exercise the proof paths they intend to.
- app/evm_mempool_evmigration_test.go: seed the legacy account into the check-tx
state (NewContext(true) — the state CheckTx reads; NewContext(false) targets
finalizeBlockState and is invisible to CheckTx) using NewAccountWithAddress to
assign a fresh account number; PrepareProposal test uses a genesis-seeded
legacy account so the proposal-time ante verify passes.
- app/evm/ante_evmigration_fee_test.go: add seedLegacyAccountInCtx and seed the
legacy account in the accept / invalid-proof / CheckTx cases so the state gate
passes and the proof-rejection path is what is actually asserted.
- tests/integration/evm/mempool/evmigration_zero_signer_test.go: seed the legacy
account into the node genesis before broadcasting (real-node path).
Docs:
- docs/.../tests/unit-evmigration.md: remap 27 stale signature/multisig test
names to the current functions (TestVerifyCosmosSecp256k1_* /
TestVerifyEthSecp256k1_* for signature verification, TestVerifyMigrationProof_
NewSide_Multisig_* for the multisig verifier, NonSecp256k1SubKey,
MigrationProof_ValidateBasic_Dispatch). Every referenced name now resolves to
a real func.
- docs/.../tests.md: correct counters to actual values — EVMigration keeper
118+ -> 124+, EVMigration integration "15+ core" -> "14 core + 4 mempool
broadcast regressions" (18 rows), comparison row 117+/19 -> 150+/18, totals
Unit ~401/Int ~151/Total ~564 -> ~407/~150/~569, headline ~560 -> ~570.
- CHANGELOG.md, bugs.md, integration-{evmigration,mempool}.md: describe the
state-plausibility checks and the new negative tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Enables Lumera’s app-side EVM mempool (ExperimentalEVMMempool) to accept intentionally zero-signer EVM migration transactions (MsgClaimLegacyAccount, MsgMigrateValidator) by synthesizing deterministic signer data from legacy_address, so the migration-aware ante chain can run as designed.
Changes:
- Add an evmigration-aware
SignerExtractionAdapterand wire it into the Cosmos-side mempool config and proposal signer extractor path. - Harden
x/evmigrationante admission to bound zero-fee/zero-sig migration traffic to the configured migration window and reject obviously-impossible migrations early. - Add unit + integration regression coverage (including real-node
broadcast_tx_sync) and update migration/testing docs + changelog.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
x/evmigration/keeper/ante.go |
Adds migration window gating + cheap state admission checks in ante for unsigned, zero-fee migration txs. |
x/evmigration/keeper/ante_test.go |
Extends keeper ante tests to cover the new admission gate and state plausibility checks. |
tests/integration/evm/mempool/evmigration_zero_signer_test.go |
Real-node integration coverage ensuring zero-signer migration broadcasts pass CheckTx while non-migration zero-signer txs still fail. |
docs/evm-integration/user-guides/migration.md |
Documents the zero-signer mempool requirement and points to the adapter wiring. |
docs/evm-integration/testing/tests/unit-evmigration.md |
Updates/expands test catalog to include new mempool signer-adapter and ante-admission tests. |
docs/evm-integration/testing/tests/unit-app-wiring.md |
Adds a test entry documenting mempool-disabled wiring behavior. |
docs/evm-integration/testing/tests/integration-mempool.md |
Registers the new integration test file and its scenarios. |
docs/evm-integration/testing/tests/integration-evmigration.md |
Cross-links the new broadcast-path coverage in the mempool suite. |
docs/evm-integration/testing/tests.md |
Updates testing totals and coverage matrix to reflect new tests. |
docs/evm-integration/testing/bugs.md |
Records the bug, root cause, fix, hardening, and test coverage references. |
docs/evm-integration/architecture/rollout.md |
Updates approximate test counts used in rollout readiness documentation. |
CHANGELOG.md |
Adds release notes for the mempool signer adapter and ante hardening changes. |
app/test_helpers.go |
Introduces setupWithAppOptionOverrides to allow app option overrides in tests. |
app/evmigration_signer_extraction_adapter.go |
New adapter: synthesizes a signer from legacy_address for migration-only txs, delegates otherwise. |
app/evmigration_signer_extraction_adapter_test.go |
Unit tests for synthetic signer derivation, delegation, and invalid legacy address cases. |
app/evm/ante_evmigration_fee_test.go |
Updates ante tests to seed legacy accounts so new ante admission checks don’t mask proof verification assertions. |
app/evm_mempool.go |
Wires the migration-aware signer adapter into CosmosPoolConfig and proposal signer extraction; replicates upstream priority logic with a zero-fee short-circuit. |
app/evm_mempool_test.go |
Adds coverage ensuring mempool.max-txs = -1 leaves app-side mempool disabled (BaseApp stays NoOp). |
app/evm_mempool_evmigration_test.go |
App-level regression tests asserting CheckTx/mempool/proposal paths accept valid zero-signer migration txs and preserve rejection of non-migration zero-signer txs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
fix(evmigration): mempool-accept zero-signer migration txs
TL;DR
submit-proofbroadcasts a zero-signerMsgClaimLegacyAccount/MsgMigrateValidator. Onv1.20.0-rc4, every such broadcast was rejected by the EVM mempool with:Root cause:
ExperimentalEVMMempool.cosmosPooluses the SDK'sDefaultSignerExtractionAdapter, which callstx.GetSignaturesV2()and rejects empty sig sets inPriorityNonceMempool.Insert— before the migration-aware ante chain (app/evm/ante.go: migrationCosmosAnte) ever runs.This PR teaches the mempool to recognize migration-only txs and synthesize a deterministic signer from
legacy_address, so the ante chain gets a chance to run as designed.Discovered during the v1.20.0-rc4 multisig migration rehearsal in #163 (
migrate-batch.shend-to-end). Reproduces withmigrate-multisig.sh submit,migrate-account.sh, and a hand-builtlumerad tx broadcast.What's in the diff
app/evmigration_signer_extraction_adapter.goevmigrationSignerExtractionAdapter— synthesizes aSignerData{AccAddress(legacy_address), 0}forIsEVMigrationOnlyTx; delegates everything else to a configurable fallback.app/evm_mempool.goCosmosPoolConfigexplicitly with our adapter; wraps thePrepareProposalsigner extractor asEthSignerExtractionAdapter(migrationAdapter(default)). Replicates upstream's default priority function locally with a zero-fee short-circuit so the adapter override is the only behavior change.app/evmigration_signer_extraction_adapter_test.goapp/evm_mempool_evmigration_test.goApp+ realExperimentalEVMMempool. Asserts (a)Insertaccepts a zero-signerMsgClaimLegacyAccountandCountTx()increments, and (b) the SDK default adapter still returns zero signers for the same tx (regression pin).docs/evm-integration/user-guides/migration.mdCHANGELOG.mdv1.20.0.Why it's safe
legacy_addressis the canonical on-chain identity of the source account and is the same field the keeper uses to keymigrate state. UsingAccAddress(legacy_address)for nonce-mempool ordering preserves (sender, nonce) ordering and dedupe semantics for the natural unit of work (one migration per legacy address).(sender, sequence)to be stable within a block window.defaultCosmosPoolConfigmirrorscosmos/evm@v0.6.0mempool.go:152exactly; onlySignerExtractordiffers. The zero-fee short-circuit is a defensive addition that lets migration txs avoid an unnecessaryEVMKeeper.GetEvmCoinInforead (which the keeper currently panics on for freshsdk.Context{}lacking a KVStore — observed in the integration test path).Tests
The new mempool integration test in particular fails on master (with the exact
tx must have at least one signerrejection) and passes on this branch — it is a literal regression pin.Field findings (advisory for Alexey's manual MD)
Surfaced while running the script-side rehearsal against the same procedure. These are in Alexey's standalone "Multisig migration Lumera EVM.md", not in this repo's docs (the repo's
migration.mdandmigration-scripts.mdalready document both correctly):migrate-multisig.sh signmust pass both--fromAND--new-keyfor every co-signer who holds both halves of a key pair. The "--new-key-only" pattern in Alexey's MD produces one-sided partials that fail quorum atcombinewith "Matching-index threshold satisfied: no (0 < 2)". The repo docs already state this clearly (migration-scripts.mdlines 586–608).lumerad keys add --multisigmust pass--nosortwhen reconstructing the new EVM multisig. The default address-sort permutes the canonical pubkey order foreth_secp256k1keys and breakssigner_indicesalignment between legacy and new halves. The repo docs already require this (migration.mdline 680,migration-scripts.mdline 918). It affects 23 of the 28 foundation multisigs on mainnet — they'd silently produce wrong reconstructed addresses without--nosort.Follow-up
migrate-batch.sh(scripts: add migrate-batch.sh — batch driver for EVM migration #163) end-to-end on devnet, then flip that PR from draft to ready-for-review.v1.20.0-rc5is cut with this fix, repeat the testnet rehearsal before Monday's upgrade window.