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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Full EVM integration documentation: [docs/evm-integration/main.md](docs/evm-inte
- Added evmigration query endpoints for migration planning and monitoring: `MigrationEstimate` (pre-migration impact analysis with delegation/unbonding/redelegation/authz/feegrant counts), `MigrationStats` (on-chain progress tracking), `LegacyAccounts` (paginated unmigrated account listing), and `MigratedAccounts` (searchable migration history).
- Added dual signature verification in evmigration: legacy proofs accept both raw SHA-256 CLI signing and ADR-036 wallet signing (Keplr/Leap); new address proofs accept both raw Keccak-256 and EIP-191 `personal_sign` (MetaMask), ensuring compatibility across all major wallet types.
- Added `app.toml` auto-config migration (`cmd/lumera/cmd/config_migrate.go`) for nodes upgrading from pre-EVM binaries — automatically detects missing `[evm]`, `[json-rpc]`, `[tls]`, and `[lumera.*]` sections and regenerates `app.toml` with Lumera defaults while preserving existing operator settings.
- Updated app-side mempool defaults to keep fresh testnet/mainnet-style homes bounded at `mempool.max-txs = 10000`; config migration now rewrites legacy no-op `max-txs = -1` to `5000` on devnet and `10000` on testnet/mainnet, while preserving the real Cosmos EVM v0.6.0 `[evm.mempool]` defaults (`global-slots = 5120`, `global-queue = 1024`).
- Added EVM mempool Prometheus metrics (`app/evm_mempool_metrics.go`): gauges for mempool size, pending/queued counts, and broadcast queue depth; labeled rejection counter (`rejections_total{source,reason}`) for observability.
- Added `MsgSetRegistrationPolicy` governance message for ERC20 IBC auto-registration: operators can toggle policy between `all`, `allowlist`, and `none` modes; pre-populated genesis allowlist includes inert base denom traces for major tokens (uatom, uosmo, uusdc, inj) ready for governance channel binding.
- Added evmigration user guides under `docs/evm-integration/user-guides/`: `migration.md` (CLI/Keplr/MetaMask account migration), `validator-migration.md`, `supernode-migration.md`, and `migration-scripts.md` reference for the helper scripts above.
Expand Down
2 changes: 1 addition & 1 deletion cmd/lumera/cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ func initAppConfig() (string, interface{}) {
// Enable app-side mempool by default so EVM mempool integration paths
// (pending tx subscriptions, nonce-gap handling, replacement rules) work
// out-of-the-box without extra start flags.
srvCfg.Mempool.MaxTxs = 5000
srvCfg.Mempool.MaxTxs = 10000
evmCfg := cosmosevmserverconfig.DefaultEVMConfig()
evmCfg.EVMChainID = lcfg.EVMChainID

Expand Down
40 changes: 40 additions & 0 deletions cmd/lumera/cmd/config_migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/cosmos/cosmos-sdk/server"
serverconfig "github.com/cosmos/cosmos-sdk/server/config"
"github.com/cosmos/cosmos-sdk/x/genutil/types"
cosmosevmserverconfig "github.com/cosmos/evm/server/config"
"github.com/spf13/cobra"
"github.com/spf13/viper"
Expand Down Expand Up @@ -73,6 +75,9 @@ func doMigrateAppConfig(v *viper.Viper, appCfgPath string) error {
// Force the EVM chain ID to the Lumera constant — an operator should
// never have a different value.
fullCfg.EVM.EVMChainID = lcfg.EVMChainID
if fullCfg.Mempool.MaxTxs < 0 {
fullCfg.Mempool.MaxTxs = migratedMempoolMaxTxs(migrationChainID(v, appCfgPath))
}
Comment thread
akobrin1 marked this conversation as resolved.

// Only enable JSON-RPC and indexer when the section was never written
// (i.e. the key is not present in Viper at all). If an operator
Expand Down Expand Up @@ -130,6 +135,9 @@ func doMigrateAppConfig(v *viper.Viper, appCfgPath string) error {
// corrected by the v1.20.0 config migration. These keys are always force-set
// into the live Viper after migration, overriding any stale in-memory values.
func isEVMMigratedKey(key string) bool {
if key == "mempool.max-txs" {
return true
}
for _, prefix := range evmMigratedPrefixes {
if len(key) >= len(prefix) && key[:len(prefix)] == prefix {
return true
Expand All @@ -138,6 +146,33 @@ func isEVMMigratedKey(key string) bool {
return false
}

func migratedMempoolMaxTxs(chainID string) int {
if lcfg.IsDevnetChainID(chainID) {
return 5000
}
return 10000
}

func migrationChainID(v *viper.Viper, appCfgPath string) string {
if chainID := strings.TrimSpace(v.GetString("chain-id")); chainID != "" {
return chainID
}

genesisPath := filepath.Join(filepath.Dir(appCfgPath), "genesis.json")
reader, err := os.Open(genesisPath)
if err != nil {
return ""
}
defer func() { _ = reader.Close() }()

chainID, err := types.ParseChainIDFromGenesis(reader)
if err != nil {
return ""
}

return chainID
}

var evmMigratedPrefixes = []string{
"evm.",
"json-rpc.",
Expand All @@ -160,6 +195,10 @@ func needsConfigMigration(v viperGetter) bool {
return true
}

if v.GetInt("mempool.max-txs") < 0 {
return true
}

// [json-rpc] section absent — key was never written to app.toml.
// We use IsSet to distinguish "never written" from "explicitly disabled."
if !v.IsSet("json-rpc.enable") {
Expand All @@ -186,6 +225,7 @@ func needsConfigMigration(v viperGetter) bool {
type viperGetter interface {
GetUint64(key string) uint64
GetBool(key string) bool
GetInt(key string) int
GetString(key string) string
IsSet(key string) bool
}
138 changes: 138 additions & 0 deletions cmd/lumera/cmd/config_migrate_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"fmt"
"os"
"path/filepath"
"testing"
Expand Down Expand Up @@ -94,6 +95,19 @@ func TestNeedsConfigMigration_FullyMigrated(t *testing.T) {
assert.False(t, needsConfigMigration(v), "fully migrated config must not trigger migration")
}

func TestNeedsConfigMigration_DisabledMempool(t *testing.T) {
t.Parallel()

v := viper.New()
v.Set("evm.evm-chain-id", lcfg.EVMChainID)
v.Set("json-rpc.enable", true)
v.Set("lumera.json-rpc-ratelimit.proxy-address", "0.0.0.0:8547")
v.Set("tls.certificate-path", "")
v.Set("mempool.max-txs", -1)

assert.True(t, needsConfigMigration(v), "disabled app mempool must trigger migration repair")
}

// TestMigrateAppConfig_LegacyTomlOnDisk verifies the full migration flow:
// start with a legacy pre-EVM app.toml, run the migrator, and confirm both
// the disk file and in-memory Viper contain the correct EVM config.
Expand Down Expand Up @@ -177,3 +191,127 @@ max-txs = 3000
assert.False(t, needsConfigMigration(v),
"after migration, needsConfigMigration must return false")
}

func TestMigrateAppConfig_FullyMigratedNegativeMaxTxsTriggersRepair(t *testing.T) {
t.Parallel()

tmpDir := t.TempDir()
configDir := filepath.Join(tmpDir, "config")
require.NoError(t, os.MkdirAll(configDir, 0o755))

appToml := fmt.Sprintf(`
[mempool]
max-txs = -1

[evm]
evm-chain-id = %d

[json-rpc]
enable = false

[lumera.json-rpc-ratelimit]
proxy-address = "0.0.0.0:8547"

[tls]
certificate-path = ""
`, lcfg.EVMChainID)
appCfgPath := filepath.Join(configDir, "app.toml")
require.NoError(t, os.WriteFile(appCfgPath, []byte(appToml), 0o644))

v := viper.New()
v.SetConfigType("toml")
v.SetConfigName("app")
v.AddConfigPath(configDir)
v.Set("chain-id", "lumera-mainnet-1")
require.NoError(t, v.MergeInConfig())
require.True(t, needsConfigMigration(v), "precondition: disabled mempool must need repair")

require.NoError(t, doMigrateAppConfig(v, appCfgPath))

v2 := viper.New()
v2.SetConfigType("toml")
v2.SetConfigName("app")
v2.AddConfigPath(configDir)
require.NoError(t, v2.MergeInConfig())

assert.Equal(t, int64(10000), v.GetInt64("mempool.max-txs"),
"in-memory disabled mempool must be repaired")
assert.Equal(t, int64(10000), v2.GetInt64("mempool.max-txs"),
"disk disabled mempool must be repaired")
assert.False(t, v.GetBool("json-rpc.enable"),
"explicit operator-disabled json-rpc must remain disabled")
assert.False(t, needsConfigMigration(v),
"after repair, needsConfigMigration must return false")
}

func TestMigrateAppConfig_LegacyNegativeMaxTxsUsesNetworkDefault(t *testing.T) {
t.Parallel()

testCases := []struct {
name string
chainID string
chainIDInGenesis bool
wantMaxTxs int64
}{
{name: "devnet from viper", chainID: "lumera-devnet-1", wantMaxTxs: 5000},
{name: "devnet from genesis", chainID: "lumera-devnet-1", chainIDInGenesis: true, wantMaxTxs: 5000},
{name: "testnet from viper", chainID: "lumera-testnet-2", wantMaxTxs: 10000},
{name: "mainnet from viper", chainID: "lumera-mainnet-1", wantMaxTxs: 10000},
}

for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

tmpDir := t.TempDir()
configDir := filepath.Join(tmpDir, "config")
require.NoError(t, os.MkdirAll(configDir, 0o755))

legacyToml := `
[api]
enable = true

[mempool]
max-txs = -1
`
appCfgPath := filepath.Join(configDir, "app.toml")
require.NoError(t, os.WriteFile(appCfgPath, []byte(legacyToml), 0o644))
if tc.chainIDInGenesis {
genesis := `{"chain_id":"` + tc.chainID + `"}`
require.NoError(t, os.WriteFile(filepath.Join(configDir, "genesis.json"), []byte(genesis), 0o644))
}

v := viper.New()
v.SetConfigType("toml")
v.SetConfigName("app")
v.AddConfigPath(configDir)
if !tc.chainIDInGenesis {
v.Set("chain-id", tc.chainID)
}
require.NoError(t, v.MergeInConfig())
require.True(t, needsConfigMigration(v), "precondition: legacy config must need migration")

require.NoError(t, doMigrateAppConfig(v, appCfgPath))

v2 := viper.New()
v2.SetConfigType("toml")
v2.SetConfigName("app")
v2.AddConfigPath(configDir)
require.NoError(t, v2.MergeInConfig())

assert.Equal(t, tc.wantMaxTxs, v.GetInt64("mempool.max-txs"),
"in-memory legacy no-op mempool must be replaced for %s", tc.chainID)
assert.Equal(t, tc.wantMaxTxs, v2.GetInt64("mempool.max-txs"),
"disk legacy no-op mempool must be replaced for %s", tc.chainID)

migratedToml, err := os.ReadFile(appCfgPath)
require.NoError(t, err)
migratedTomlStr := string(migratedToml)
assert.Contains(t, migratedTomlStr, "[evm.mempool]")
assert.Contains(t, migratedTomlStr, "global-slots = 5120")
assert.Contains(t, migratedTomlStr, "global-queue = 1024")
assert.NotContains(t, migratedTomlStr, "insert-queue-size")
})
}
}
18 changes: 14 additions & 4 deletions cmd/lumera/cmd/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,28 @@ func TestInitAppConfigEVMDefaults(t *testing.T) {
evmCfg := cfgValue.FieldByName("EVM")
require.True(t, evmCfg.IsValid(), "EVM field not found")
require.Equal(t, uint64(lcfg.EVMChainID), evmCfg.FieldByName("EVMChainID").Uint(), "unexpected EVM chain ID")
evmMempoolCfg := evmCfg.FieldByName("Mempool")
require.True(t, evmMempoolCfg.IsValid(), "EVM.Mempool field not found")
require.EqualValues(t, 1, evmMempoolCfg.FieldByName("PriceLimit").Uint(), "unexpected evm mempool price limit")
require.EqualValues(t, 10, evmMempoolCfg.FieldByName("PriceBump").Uint(), "unexpected evm mempool price bump")
require.EqualValues(t, 16, evmMempoolCfg.FieldByName("AccountSlots").Uint(), "unexpected evm mempool account slots")
require.EqualValues(t, 5120, evmMempoolCfg.FieldByName("GlobalSlots").Uint(), "unexpected evm mempool global slots")
require.EqualValues(t, 64, evmMempoolCfg.FieldByName("AccountQueue").Uint(), "unexpected evm mempool account queue")
require.EqualValues(t, 1024, evmMempoolCfg.FieldByName("GlobalQueue").Uint(), "unexpected evm mempool global queue")
require.False(t, evmMempoolCfg.FieldByName("InsertQueueSize").IsValid(),
"Cosmos EVM v0.6.0 must not grow an unreviewed insert-queue-size config knob")

sdkCfg := cfgValue.FieldByName("Config")
require.True(t, sdkCfg.IsValid(), "Config field not found")
mempoolCfg := sdkCfg.FieldByName("Mempool")
require.True(t, mempoolCfg.IsValid(), "Mempool field not found")
require.EqualValues(t, 5000, mempoolCfg.FieldByName("MaxTxs").Int(), "unexpected app-side mempool max txs")
require.EqualValues(t, 10000, mempoolCfg.FieldByName("MaxTxs").Int(), "unexpected app-side mempool max txs")

lumeraCfg := cfgValue.FieldByName("Lumera")
require.True(t, lumeraCfg.IsValid(), "Lumera field not found")
evmMempoolCfg := lumeraCfg.FieldByName("EVMMempool")
require.True(t, evmMempoolCfg.IsValid(), "Lumera.EVMMempool field not found")
require.False(t, evmMempoolCfg.FieldByName("BroadcastDebug").Bool(), "broadcast debug must be disabled by default")
lumeraEVMMempoolCfg := lumeraCfg.FieldByName("EVMMempool")
require.True(t, lumeraEVMMempoolCfg.IsValid(), "Lumera.EVMMempool field not found")
require.False(t, lumeraEVMMempoolCfg.FieldByName("BroadcastDebug").Bool(), "broadcast debug must be disabled by default")
}

// TestInitCometBFTConfigRPCHardening locks in the RPC defense-in-depth
Expand Down
5 changes: 3 additions & 2 deletions cmd/lumera/cmd/testnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ func initTestnetFiles(
err := collectGenFiles(
clientCtx, nodeConfig, args.chainID, nodeIDs, valPubKeys, args.numValidators,
args.outputDir, args.nodeDirPrefix, args.nodeDaemonHome, genBalIterator, valAddrCodec,
rpcPort, p2pPortStart, args.singleMachine,
rpcPort, p2pPortStart, pprofPort, args.singleMachine,
)
if err != nil {
return err
Expand Down Expand Up @@ -557,7 +557,7 @@ func collectGenFiles(
clientCtx client.Context, nodeConfig *cmtconfig.Config, chainID string,
nodeIDs []string, valPubKeys []cryptotypes.PubKey, numValidators int,
outputDir, nodeDirPrefix, nodeDaemonHome string, genBalIterator banktypes.GenesisBalancesIterator, valAddrCodec runtime.ValidatorAddressCodec,
rpcPortStart, p2pPortStart int,
rpcPortStart, p2pPortStart, pprofPortStart int,
singleMachine bool,
) error {
var appState json.RawMessage
Expand All @@ -574,6 +574,7 @@ func collectGenFiles(
gentxsDir := filepath.Join(outputDir, "gentxs")
nodeConfig.Moniker = nodeDirName
nodeConfig.RPC.ListenAddress = fmt.Sprintf("tcp://0.0.0.0:%d", rpcPortStart+portOffset)
nodeConfig.RPC.PprofListenAddress = fmt.Sprintf("localhost:%d", pprofPortStart+portOffset)
nodeConfig.P2P.ListenAddress = fmt.Sprintf("tcp://0.0.0.0:%d", p2pPortStart+portOffset)

nodeConfig.SetRoot(nodeDir)
Expand Down
2 changes: 2 additions & 0 deletions docs/devnet/upgrade-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ The `make devnet-evm-upgrade` target automates a full upgrade from pre-EVM to EV
7. Wait for chain to resume producing blocks
```

Use this upgrade pipeline, not a fresh EVM init, for release qualification. It preserves the pre-EVM `app.toml` shape and exercises the startup config migration that adds `[evm]`, `[evm.mempool]`, `[json-rpc]`, `[tls]`, and `[lumera.*]`; this is the path that catches legacy no-op mempool settings such as `mempool.max-txs = -1`.

### Running the full EVM migration test

```bash
Expand Down
3 changes: 1 addition & 2 deletions docs/evm-integration/architecture/app-changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ Changes:
- Added`RegisterTxService` override in`app/evm_runtime.go` to capture the`client.Context` with the local CometBFT client that cosmos/evm creates after CometBFT starts — the default`SetClientCtx` call happens before CometBFT starts and only provides an HTTP client.
- Added`Close()` override to stop the broadcast worker before runtime shutdown.
- Added configurable`[lumera.evm-mempool]` section in`app.toml` with`broadcast-debug` toggle for detailed async broadcast logging.
- Enabled app-side mempool by default in app config (`max_txs=5000`).
- Enabled app-side mempool by default in app config (`max_txs=10000`).

Benefits/new features:

Expand Down Expand Up @@ -362,4 +362,3 @@ Benefits/new features:
- Wallet/tooling clients can discover method catalogs consistently from the running node.
- OpenRPC playground/browser clients can fetch the spec cross-origin without manual proxy setup.
- Generated docs and embedded docs stay synchronized with built binaries, reducing stale-spec deployments.

2 changes: 1 addition & 1 deletion docs/evm-integration/architecture/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ EVM-aware app-side mempool with deadlock prevention.
| [x] | Broadcast worker `RegisterTxService` override | `app/evm_runtime.go` — local CometBFT client |
| [x] | `Close()` override for graceful shutdown | `app/evm_runtime.go` |
| [x] | `broadcast-debug` app.toml toggle | `cmd/lumera/cmd/config.go` |
| [x] | Default `max_txs=5000` | App config defaults |
| [x] | Default `max_txs=10000` | App config defaults |
| [x] | Mempool eviction / capacity pressure testing | `tests/integration/evm/mempool/capacity_pressure_test.go` |
| [x] | Mempool metrics / observability | `app/evm_mempool_metrics.go` — Prometheus gauges (size, pending, queued, broadcast\_queue\_depth) + labeled rejection counter (`rejections_total{source,reason}`) |

Expand Down
2 changes: 2 additions & 0 deletions docs/evm-integration/evmigration/devnet-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,8 @@ The `make devnet-evm-upgrade` target runs the **complete end-to-end EVM upgrade

Each stage has error handling — if any stage fails, the pipeline aborts with a clear error message identifying which stage failed. Validators are migrated before regular accounts because `MsgMigrateValidator` atomically re-keys the validator record and all its delegations, which must happen before delegators attempt their own migration.

For release qualification, prefer this upgrade pipeline over fresh EVM devnet init. The upgrade path keeps the legacy `app.toml` on disk until `lumerad start` runs the config migration, so it exercises production-like startup behavior including `[evm.mempool]` section creation and legacy `mempool.max-txs = -1` repair.

Usage:

```bash
Expand Down
1 change: 1 addition & 0 deletions docs/evm-integration/testing/tests/integration-mempool.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Suites:
| `TestPrometheusMetricsExposeMempoolGauges` | E2E: starts node with Prometheus telemetry, scrapes /metrics, verifies gauges. |
| `TestPrometheusRejectionsCountedViaCometCheckTx` | E2E: submits malformed bytes via CometBFT broadcast_tx_sync, verifies rejection counter. |
| `TestEVMigrationZeroSignerTxBroadcastSyncWithMempoolEnabled` | Real-node `broadcast_tx_sync`: a valid zero-signer `MsgClaimLegacyAccount` passes CheckTx with the app-side EVM mempool enabled. |
| `TestEVMigrationZeroSignerTxBroadcastSyncAfterLegacyMainnetConfigMigration` | Real-node upgrade-profile check: starts from a pre-EVM `app.toml` with `mempool.max-txs = -1`, verifies migration rewrites it to `10000`, emits the real Cosmos EVM mempool defaults, and still admits a valid zero-signer `MsgClaimLegacyAccount`. |
| `TestEVMigrationProofValidNonexistentLegacyAccountRejectedByAnte` | Real-node `broadcast_tx_sync`: a proof-valid zero-signer migration tx is rejected by ante state admission when the legacy account does not exist. |
| `TestEVMigrationMalformedLegacyAddressRejectedByValidateBasic` | Real-node `broadcast_tx_sync`: malformed migration `legacy_address` is rejected by `ValidateBasic` in the ante chain, before mempool admission. |
| `TestZeroSignerNonMigrationBroadcastSyncStillRejected` | Negative control: a zero-signer non-migration tx is still rejected, proving the evmigration adapter does not widen signer bypass behavior. |
Loading
Loading