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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ Full EVM integration documentation: [docs/evm-integration/main.md](docs/evm-inte
- Added user-facing migration helper scripts (`scripts/migrate-account.sh`, `scripts/migrate-validator.sh`, `scripts/migrate-multisig.sh`) wrapping the full pre-flight estimate → key import → snapshot → submit → verify flow, with multisig-aware K/N partials, validator-specific cap checks and downtime acknowledgment, and fail-closed query handling so script-level success implies on-chain success.
- Added `devnet/scripts/lumera-helper.sh unjail-validator` helper plus downtime warnings in the validator migration guide for operators approaching the slashing window.
- Added fee-waiving ante decorator for migration txs (`ante/evmigration_fee_decorator.go`) since new addresses have zero balance pre-migration.
- Added migration-aware mempool signer extractor (`app/evmigration_signer_extraction_adapter.go`) wired into `ExperimentalEVMMempool.CosmosPoolConfig.SignerExtractor`. Without it, the SDK's default `DefaultSignerExtractionAdapter` rejects zero-signer migration txs (`MsgClaimLegacyAccount`, `MsgMigrateValidator`) with "tx must have at least one signer" during app-side mempool admission/proposal selection, blocking `submit-proof` broadcast. The adapter synthesizes a deterministic signer from the message's `legacy_address` for migration-only txs and delegates everything else to the EVM-aware default.
- Added regression coverage for zero-signer evmigration tx admission: unit tests pin the upstream SDK mempool rejection, adapter fallback and negative cases (malformed `legacy_address`, nonexistent legacy accounts, multi-message and mixed-message txs), app tests cover `PrepareProposal` inclusion and disabled-mempool wiring, and real-node integration tests broadcast `submit-proof`-style tx bytes through CometBFT `broadcast_tx_sync`.
- Hardened the migration ante (`x/evmigration/keeper/ante.go`) to enforce the migration admission window and cheap state plausibility before mempool admission: since migration txs are fee-free and signature-free, `VerifyMigrationProofsForAnte` now rejects them with `ErrMigrationDisabled`/`ErrMigrationWindowClosed` when `EnableMigration` is off or `MigrationEndTime` has passed, and rejects proof-valid but impossible migrations such as nonexistent legacy accounts, already-migrated sources, reused destination addresses, and non-validator `MsgMigrateValidator` sources. This bounds the zero-fee mempool-spam surface to the operator-defined window (no-op under default params; mainnet sets a concrete `MigrationEndTime`) and avoids retaining txs that would fail immediately at message execution.
- Added v1.20.0 upgrade handler with store additions for feemarket, precisebank, vm, erc20, and evmigration; post-migration finalization sets Lumera EVM params, feemarket params, and ERC20 defaults.
- Added Action module precompile (`0x0901`) and Supernode module precompile (`0x0902`) giving Solidity contracts native access to `MsgRequestAction`/`MsgFinalizeAction` (including LEP-5 cascade availability commitments) and supernode queries/registration respectively.
- Added CosmWasm ↔ EVM cross-runtime bridge (Phase 1, non-payable, depth-1 reentrancy guard): `WasmPrecompile` at `0x0903` exposes `execute`, `query`, `contractInfo`, `rawQuery` to Solidity, and a custom Wasm message handler + query handler decorator (`app/wasm_evm_plugin.go`) lets CosmWasm contracts invoke EVM contracts via `ApplyMessage` with an explicitly-constructed `statedb`. Cross-runtime gas is capped at `DefaultCrossRuntimeGasCap = 3,000,000` per call.
Expand Down
26 changes: 25 additions & 1 deletion app/evm/ante_evmigration_fee_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,20 @@ func TestNewAnteHandlerMigrationOnlyCosmosTxUsesReducedAntePath(t *testing.T) {
})

t.Run("migration-only unsigned zero-fee tx is accepted", func(t *testing.T) {
tx := newUnsignedMigrationTx(t, app, validMigrationMsg(t, anteMigrationTestChainID))
msg := validMigrationMsg(t, anteMigrationTestChainID)
seedLegacyAccountInCtx(t, app, ctx, msg.LegacyAddress)
tx := newUnsignedMigrationTx(t, app, msg)

_, err := anteHandler(ctx, tx, false)
require.NoError(t, err)
})

t.Run("migration-only invalid embedded proof is rejected in ante", func(t *testing.T) {
msg := validMigrationMsg(t, anteMigrationTestChainID)
// Seed the legacy account so the admission state-check passes and the
// corrupted proof is what actually triggers the rejection (the state
// check runs before proof verification).
seedLegacyAccountInCtx(t, app, ctx, msg.LegacyAddress)
msg.LegacyProof.GetSingle().Signature[0] ^= 0x01
tx := newUnsignedMigrationTx(t, app, msg)

Expand Down Expand Up @@ -116,6 +122,10 @@ func TestEVMigrationInvalidEmbeddedProofRejectedInCheckTx(t *testing.T) {
app := lumeraapp.Setup(t)

msg := validMigrationMsg(t, anteMigrationAppChainID)
// Seed the legacy account into the check-tx state so the admission
// state-check passes and the corrupted proof is what triggers rejection
// (the state check runs before proof verification).
seedLegacyAccountInCtx(t, app, app.BaseApp.NewContext(true), msg.LegacyAddress)
msg.NewProof.GetSingle().Signature[0] ^= 0x01

tx := newUnsignedMigrationTx(t, app, msg)
Expand Down Expand Up @@ -143,6 +153,20 @@ func newUnsignedMigrationTx(t *testing.T, app *lumeraapp.App, msgs ...sdk.Msg) s
return txBuilder.GetTx()
}

// seedLegacyAccountInCtx creates the legacy base account in the given ctx's
// state so the migration ante's legacy-account-exists admission gate
// (VerifyMigrationProofsForAnte) passes, letting a test exercise the proof /
// acceptance paths. NewAccountWithAddress assigns a fresh account number,
// avoiding the uniqueness conflict a bare base account (number 0) would hit
// against the genesis account.
func seedLegacyAccountInCtx(t *testing.T, app *lumeraapp.App, ctx sdk.Context, legacyAddress string) {
t.Helper()

addr, err := sdk.AccAddressFromBech32(legacyAddress)
require.NoError(t, err)
app.AuthKeeper.SetAccount(ctx, app.AuthKeeper.NewAccountWithAddress(ctx, addr))
}

// validMigrationMsg builds a MsgClaimLegacyAccount whose embedded proofs pass
// ante-level cryptographic verification.
func validMigrationMsg(t *testing.T, chainID string) *evmigrationtypes.MsgClaimLegacyAccount {
Expand Down
82 changes: 80 additions & 2 deletions app/evm_mempool.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package app

import (
"context"

"cosmossdk.io/log"
sdkmath "cosmossdk.io/math"

abci "github.com/cometbft/cometbft/abci/types"
"github.com/cosmos/cosmos-sdk/baseapp"
Expand Down Expand Up @@ -35,13 +38,29 @@ func (app *App) configureEVMMempool(appOpts servertypes.AppOptions, logger log.L
app.configureEVMBroadcastOptions(appOpts, broadcastLogger)
app.startEVMBroadcastWorker(broadcastLogger)

// Build the Cosmos-side mempool config explicitly so we can install a
// migration-aware SignerExtractionAdapter. Without this override, the
// upstream PriorityNonceMempool falls back to
// DefaultSignerExtractionAdapter, which calls tx.GetSignaturesV2() and
// refuses zero-signer migration txs with "tx must have at least one
// signer" during mempool admission and proposal selection.
//
// Priority / Compare / MinValue mirror upstream defaults from
// evmmempool.NewExperimentalEVMMempool (mempool.go ~line 152) so this
// override changes only signer extraction, nothing else.
cosmosPoolConfig := defaultCosmosPoolConfig(app)
cosmosPoolConfig.SignerExtractor = newEVMigrationSignerExtractionAdapter(
sdkmempool.NewDefaultSignerExtractionAdapter(),
)

// Use cosmos/evm config readers so app.toml/flags values map 1:1
// with upstream EVM behavior.
// BroadCastTxFn is overridden to use app.clientCtx at runtime (after
// server startup) rather than a static context captured during app.New().
mempoolConfig := &evmmempool.EVMMempoolConfig{
AnteHandler: app.AnteHandler(),
LegacyPoolConfig: evmconfig.GetLegacyPoolConfig(appOpts, logger),
CosmosPoolConfig: cosmosPoolConfig,
BlockGasLimit: evmconfig.GetBlockGasLimit(appOpts, logger),
MinTip: evmconfig.GetMinTip(appOpts, logger),
BroadCastTxFn: app.broadcastEVMTransactions,
Expand Down Expand Up @@ -90,11 +109,17 @@ func (app *App) configureEVMMempool(appOpts servertypes.AppOptions, logger log.L
})

// PrepareProposal must use EVM-aware signer extraction so Ethereum txs are
// ordered by (sender, nonce) correctly in proposal selection.
// ordered by (sender, nonce) correctly in proposal selection. The
// evmigration-aware adapter is layered underneath so migration-only txs
// — which have zero envelope signers and would otherwise be skipped
// during proposal building — get a synthetic signer derived from
// legacy_address.
abciProposalHandler := baseapp.NewDefaultProposalHandler(evmMempool, app)
abciProposalHandler.SetSignerExtractionAdapter(
evmmempool.NewEthSignerExtractionAdapter(
sdkmempool.NewDefaultSignerExtractionAdapter(),
newEVMigrationSignerExtractionAdapter(
sdkmempool.NewDefaultSignerExtractionAdapter(),
),
),
)
app.SetPrepareProposal(abciProposalHandler.PrepareProposalHandler())
Expand All @@ -113,3 +138,56 @@ func (app *App) configureEVMMempool(appOpts servertypes.AppOptions, logger log.L

return nil
}

// defaultCosmosPoolConfig replicates the upstream default Cosmos-side mempool
// config that evmmempool.NewExperimentalEVMMempool builds when
// EVMMempoolConfig.CosmosPoolConfig is nil (cosmos/evm mempool.go ~line 152).
//
// We reproduce it here so we can inject our own SignerExtractionAdapter
// (newEVMigrationSignerExtractionAdapter) without changing the priority,
// compare, or min-value semantics. Keep this function aligned with upstream
// when bumping the cosmos/evm dependency.
func defaultCosmosPoolConfig(app *App) *sdkmempool.PriorityNonceMempoolConfig[sdkmath.Int] {
return &sdkmempool.PriorityNonceMempoolConfig[sdkmath.Int]{
TxPriority: sdkmempool.TxPriority[sdkmath.Int]{
GetTxPriority: func(goCtx context.Context, tx sdk.Tx) sdkmath.Int {
ctx := sdk.UnwrapSDKContext(goCtx)
cosmosTxFee, ok := tx.(sdk.FeeTx)
if !ok {
return sdkmath.ZeroInt()
}
// Short-circuit zero-fee / zero-gas txs without touching
// EVM keeper state. This matters for three reasons:
// 1. Migration-only txs (MsgClaimLegacyAccount) carry no
// fee — their priority is unambiguously zero and we
// avoid an unnecessary KVStore read.
// 2. The SDK PriorityNonceMempool may invoke this with
// a ctx that has no KVStore attached (e.g. some test
// paths), in which case a state read panics.
// 3. The gas == 0 guard also hardens against a
// division-by-zero panic in the final
// coin.Amount.Quo(gas): upstream's default priority
// function (cosmos/evm mempool.go ~line 152) divides by
// GetGas() with no zero guard, so this is strictly safer
// than the code it replicates.
fee := cosmosTxFee.GetFee()
gas := cosmosTxFee.GetGas()
if gas == 0 || fee.IsZero() {
return sdkmath.ZeroInt()
}
if app.EVMKeeper == nil {
return sdkmath.ZeroInt()
}
found, coin := fee.Find(app.EVMKeeper.GetEvmCoinInfo(ctx).Denom)
if !found {
return sdkmath.ZeroInt()
}
return coin.Amount.Quo(sdkmath.NewIntFromUint64(gas))
},
Compare: func(a, b sdkmath.Int) int {
return a.BigInt().Cmp(b.BigInt())
},
MinValue: sdkmath.ZeroInt(),
},
}
}
Loading
Loading