diff --git a/CHANGELOG.md b/CHANGELOG.md index d9f20a98..3a8cbf4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/cmd/lumera/cmd/config.go b/cmd/lumera/cmd/config.go index 43be4f20..b77c8b39 100644 --- a/cmd/lumera/cmd/config.go +++ b/cmd/lumera/cmd/config.go @@ -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 diff --git a/cmd/lumera/cmd/config_migrate.go b/cmd/lumera/cmd/config_migrate.go index e64d7f7e..65dae7c5 100644 --- a/cmd/lumera/cmd/config_migrate.go +++ b/cmd/lumera/cmd/config_migrate.go @@ -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" @@ -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)) + } // 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 @@ -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 @@ -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.", @@ -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") { @@ -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 } diff --git a/cmd/lumera/cmd/config_migrate_test.go b/cmd/lumera/cmd/config_migrate_test.go index 84647794..8ea6b414 100644 --- a/cmd/lumera/cmd/config_migrate_test.go +++ b/cmd/lumera/cmd/config_migrate_test.go @@ -1,6 +1,7 @@ package cmd import ( + "fmt" "os" "path/filepath" "testing" @@ -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. @@ -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") + }) + } +} diff --git a/cmd/lumera/cmd/config_test.go b/cmd/lumera/cmd/config_test.go index 5df972dd..70d8f66f 100644 --- a/cmd/lumera/cmd/config_test.go +++ b/cmd/lumera/cmd/config_test.go @@ -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 diff --git a/cmd/lumera/cmd/testnet.go b/cmd/lumera/cmd/testnet.go index 4c5d20b0..d12545bb 100644 --- a/cmd/lumera/cmd/testnet.go +++ b/cmd/lumera/cmd/testnet.go @@ -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 @@ -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 @@ -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) diff --git a/docs/devnet/upgrade-testing.md b/docs/devnet/upgrade-testing.md index d267de08..5361a75d 100644 --- a/docs/devnet/upgrade-testing.md +++ b/docs/devnet/upgrade-testing.md @@ -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 diff --git a/docs/evm-integration/architecture/app-changes.md b/docs/evm-integration/architecture/app-changes.md index 444acf1b..c15c5779 100644 --- a/docs/evm-integration/architecture/app-changes.md +++ b/docs/evm-integration/architecture/app-changes.md @@ -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: @@ -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. - diff --git a/docs/evm-integration/architecture/roadmap.md b/docs/evm-integration/architecture/roadmap.md index e4b478b9..42163574 100644 --- a/docs/evm-integration/architecture/roadmap.md +++ b/docs/evm-integration/architecture/roadmap.md @@ -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}`) | diff --git a/docs/evm-integration/evmigration/devnet-tests.md b/docs/evm-integration/evmigration/devnet-tests.md index 6ebe1fe9..fa23b79f 100644 --- a/docs/evm-integration/evmigration/devnet-tests.md +++ b/docs/evm-integration/evmigration/devnet-tests.md @@ -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 diff --git a/docs/evm-integration/testing/tests/integration-mempool.md b/docs/evm-integration/testing/tests/integration-mempool.md index 5e1d039e..c1630e4c 100644 --- a/docs/evm-integration/testing/tests/integration-mempool.md +++ b/docs/evm-integration/testing/tests/integration-mempool.md @@ -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. | diff --git a/docs/evm-integration/testing/tests/unit-app-wiring.md b/docs/evm-integration/testing/tests/unit-app-wiring.md index 2e6a6c07..2d1233e4 100644 --- a/docs/evm-integration/testing/tests/unit-app-wiring.md +++ b/docs/evm-integration/testing/tests/unit-app-wiring.md @@ -62,7 +62,7 @@ Primary files: | `TestIsInterchainAccountAddr` | Verifies ICA detection by address lookup through account keeper. | | `TestEVMAddPreinstallsMatrix` | Verifies preinstall contract registration matrix in VM keeper setup paths. | | `TestRegisterLumeraLegacyAminoCodecEnablesEthSecp256k1StdSignature` | Verifies legacy Amino registration covers eth_secp256k1 so SDK ante tx-size signature marshaling does not panic. | -| `TestInitAppConfigEVMDefaults` | Verifies default app config enables EVM/JSON-RPC values expected by Lumera. | +| `TestInitAppConfigEVMDefaults` | Verifies default app config enables EVM/JSON-RPC values expected by Lumera, including `mempool.max-txs = 10000` and the real Cosmos EVM v0.6.0 `[evm.mempool]` defaults (`global-slots = 5120`, `global-queue = 1024`, no `insert-queue-size`). | | `TestNeedsConfigMigration_LegacyConfig` | Empty Viper (pre-EVM app.toml with no EVM sections) triggers config migration. (Bug #19) | | `TestNeedsConfigMigration_UpstreamDefault` | Upstream cosmos/evm default chain ID (262144) triggers config migration even when other sections exist. (Bug #19) | | `TestNeedsConfigMigration_PartialManualEdit` | Correct evm-chain-id but missing [json-rpc] section still triggers migration. (Bug #19) | @@ -70,6 +70,7 @@ Primary files: | `TestNeedsConfigMigration_OperatorDisabledJSONRPC` | Operator who explicitly set `json-rpc.enable = false` does NOT trigger migration — choice is respected. (Bug #19) | | `TestNeedsConfigMigration_FullyMigrated` | Fully migrated config with all sentinel keys set does NOT trigger migration. (Bug #19) | | `TestMigrateAppConfig_LegacyTomlOnDisk` | Full migration flow: writes legacy app.toml, runs migrator, verifies disk and in-memory Viper state contain correct EVM config while preserving operator settings. (Bug #19) | +| `TestMigrateAppConfig_LegacyNegativeMaxTxsUsesNetworkDefault` | Verifies config migration rewrites legacy `mempool.max-txs = -1` to `5000` for devnet and `10000` for testnet/mainnet, while emitting only real Cosmos EVM mempool keys. | | `TestNewRootCmdStartWiresEVMFlags` | Verifies start/root command exposes key EVM JSON-RPC flags. | | `TestNewRootCmdDefaultKeyTypeOverridden` | Verifies root command default key algorithm is overridden to `eth_secp256k1`. | | `TestRevertToSnapshot_ProcessedEventsInvariant` | Adapted from cosmos/evm v0.6.0: verifies StateDB event-tracking invariant after snapshot reverts during precompile calls. | diff --git a/docs/evm-integration/user-guides/node-evm-config-guide.md b/docs/evm-integration/user-guides/node-evm-config-guide.md index 0b11d537..e93c53fb 100644 --- a/docs/evm-integration/user-guides/node-evm-config-guide.md +++ b/docs/evm-integration/user-guides/node-evm-config-guide.md @@ -20,10 +20,11 @@ Nodes upgrading from a pre-EVM binary (< v1.20.0) will have an `app.toml` that l Starting with v1.20.0, `lumerad` includes a **config migration helper** (`cmd/lumera/cmd/config_migrate.go`) that runs on every startup: -1. Checks whether `evm.evm-chain-id` in the loaded config matches the Lumera constant (`76857769`). -2. If it does not match (absent section defaults to the upstream cosmos/evm value `262144`, or `0` for entirely missing keys): +1. Checks whether the loaded config has the required EVM-era sections and sentinel values: the Lumera EVM chain ID (`76857769`), `[json-rpc]`, `[tls]`, and `[lumera.*]`. +2. If any required section is missing, or if `evm.evm-chain-id` is absent/wrong (absent section defaults to the upstream cosmos/evm value `262144`, or `0` for entirely missing keys): - Reads all existing settings from the current `app.toml` via Viper. - Merges them with Lumera's EVM defaults (correct chain ID, JSON-RPC enabled, indexer enabled, `rpc` namespace for OpenRPC). + - Rewrites legacy `mempool.max-txs = -1` no-op settings to an enabled mempool default (`5000` on devnet, `10000` on testnet/mainnet). - Regenerates `app.toml` with the full template, preserving all operator customizations. 3. Logs an `INFO` message when migration occurs. @@ -50,8 +51,8 @@ max-tx-gas-wanted = 0 # Only useful for certain debugging/tracing scenarios. cache-preimage = false -# EIP-155 chain ID. Must match the network's genesis chain ID. -# Do NOT change this on an existing chain. +# Numeric EIP-155 chain ID. This is separate from the Cosmos chain-id +# string (for example, "lumera-mainnet-1"). Do NOT change this. evm-chain-id = 76857769 # Minimum priority fee (tip) for mempool acceptance, in wei. @@ -100,7 +101,8 @@ lifetime = "3h0m0s" ### Tuning notes -- **`global-slots`**: The primary knob for mempool capacity. Increase for high-throughput validators; decrease on resource-constrained sentries. The app-level `mempool.max-txs` (default `5000`) also bounds total mempool size. +- **`global-slots`**: The primary knob for mempool capacity. Increase for high-throughput validators; decrease on resource-constrained sentries. The app-level `mempool.max-txs` (default `10000`) also bounds total mempool size. +- Cosmos EVM v0.6.0 exposes the keys listed above. There is no `[evm.mempool] insert-queue-size` setting. - **`account-slots`**: Increase if you expect DeFi bots or relayers sending many txs per block from a single account. - **`price-bump`**: The 10% default means a replacement tx must pay ≥110% of the original gas price. Increase to reduce churn from frequent replacements. - **`lifetime`**: Shorten on public RPC nodes to reduce stale tx accumulation; lengthen on private validators that batch txs. @@ -127,7 +129,7 @@ ws-address = "127.0.0.1:8546" ws-origins = ["127.0.0.1", "localhost"] # Enabled JSON-RPC namespaces (comma-separated). -# Available: eth, net, web3, rpc, debug, personal, admin, txpool, miner +# Available: eth, net, web3, rpc, debug, personal, txpool, miner api = "eth,net,web3,rpc" # Gas cap for eth_call and eth_estimateGas. 0 = unlimited. @@ -188,7 +190,7 @@ enable-profiling = false - **`address` / `ws-address`**: Bind to `0.0.0.0` only if behind a reverse proxy or firewall. Never expose raw JSON-RPC to the public internet without rate limiting. - **`ws-origins`**: Controls allowed origins for both WebSocket connections **and** the `/openrpc.json` HTTP endpoint CORS. On production nodes, set this to your specific domains (e.g., `["https://explorer.lumera.io", "https://app.lumera.io"]`). The default `["127.0.0.1", "localhost"]` is safe but will block browser-based dApps on other origins. An empty list or `["*"]` allows all origins (suitable for dev/testnet only). -- **`api`**: On mainnet, `debug`, `personal`, and `admin` namespaces are **automatically rejected** at startup by `jsonrpc_policy.go`. On testnets all namespaces are allowed. To enable tracing on testnet, use `api = "eth,net,web3,rpc,debug"` and set `[evm] tracer`. +- **`api`**: On mainnet, `debug`, `personal`, and `admin` entries are **automatically rejected** at startup by `jsonrpc_policy.go` (`admin` is not implemented by Cosmos EVM, but is still rejected if present). On testnets all implemented namespaces are allowed. To enable tracing on testnet, use `api = "eth,net,web3,rpc,debug"` and set `[evm] tracer`. - **`gas-cap`**: Limits compute for `eth_call`. Reduce if public-facing nodes are hit with expensive view calls. - **`evm-timeout`**: Reduce to `2s` or `3s` on public RPC nodes to prevent slow `eth_call` from tying up resources. - **`logs-cap` / `block-range-cap`**: Reduce on public nodes to prevent expensive `eth_getLogs` scans. Values of `1000`–`2000` are common for public endpoints. @@ -209,8 +211,8 @@ Lumera-specific reverse proxy that sits in front of JSON-RPC with per-IP token b # Enable the rate-limiting proxy. enable = false -# Address the proxy listens on. -# Clients connect here; proxy forwards to [json-rpc] address. +# Standalone fallback proxy listen address. In the default alias-proxy +# topology, rate limiting wraps [json-rpc] address directly and this is unused. proxy-address = "0.0.0.0:8547" # Sustained requests per second per IP. @@ -230,7 +232,7 @@ trusted-proxies = "" ### Tuning notes -- **Recommended for public RPC nodes**: Enable this and point external traffic to the proxy port (`8547`), while keeping the real JSON-RPC port (`8545`) on localhost. +- **Recommended for public RPC nodes**: Enable this before exposing JSON-RPC. In the default startup topology, rate limiting is injected directly into the public `json-rpc.address` listener. Keep that listener behind a firewall or reverse proxy if you do not want it publicly reachable. - **`requests-per-second`**: 50 rps is generous for individual users. Reduce to `10`–`20` for heavily loaded public endpoints. - **`burst`**: Allows short spikes. Set to 2–3× `requests-per-second` for a reasonable burst window. - **`entry-ttl`**: Controls memory usage. Shorter TTL frees memory faster but may re-admit recently limited IPs sooner. @@ -244,7 +246,7 @@ When the JSON-RPC alias proxy is active (the default), rate limiting is injected Internet → [alias proxy + rate-limit @ :8545] → [internal cosmos/evm server @ loopback] ``` -When the alias proxy is disabled, a standalone rate-limit proxy listens on `proxy-address`: +When the alias proxy is not active, a standalone rate-limit proxy listens on `proxy-address`: ``` Internet → [lumera.json-rpc-ratelimit @ :8547] → [json-rpc @ 127.0.0.1:8545] @@ -352,8 +354,8 @@ Lumera enforces namespace restrictions based on chain ID at node startup (`cmd/l | Chain type | Allowed | Blocked | |-----------|---------|---------| -| Mainnet (`lumera-mainnet*`) | `eth`, `net`, `web3`, `rpc`, `txpool`, `miner` | `admin`, `debug`, `personal` | -| Testnet / Local | All namespaces | None | +| Mainnet (`lumera-mainnet*`) | `eth`, `net`, `web3`, `rpc`, `txpool`, `miner` | `admin` entries, `debug`, `personal` | +| Testnet / Local | All implemented namespaces | None | If a mainnet node's `app.toml` includes a blocked namespace, the node **refuses to start** with a clear error message. This is a safety net — not a substitute for firewall rules. @@ -418,7 +420,7 @@ enable = false # not needed on localhost ```toml [json-rpc] enable = true -address = "127.0.0.1:8545" # behind rate-limit proxy +address = "127.0.0.1:8545" # reverse proxy should connect here ws-address = "127.0.0.1:8546" api = "eth,net,web3,rpc" gas-cap = 10000000 # reduced for public safety @@ -432,8 +434,8 @@ max-open-connections = 200 tracer = "" [lumera.json-rpc-ratelimit] -enable = true -proxy-address = "0.0.0.0:8547" # public-facing port +enable = true # wraps json-rpc.address in the default topology +proxy-address = "0.0.0.0:8547" # fallback only when alias proxy is inactive requests-per-second = 20 burst = 50 entry-ttl = "5m" diff --git a/precompiles/solidity/README.md b/precompiles/solidity/README.md index da276fbd..b858ab9b 100644 --- a/precompiles/solidity/README.md +++ b/precompiles/solidity/README.md @@ -29,6 +29,9 @@ test/ ## Quick Start +> **Prerequisites:** Node.js **≥ 20** — required by the Hardhat 3 toolchain +> (`engines.node` in `package.json`). The project is ESM (`"type": "module"`). + ```bash # Install dependencies npm install diff --git a/tests/integration/evm/jsonrpc/openrpc_test.go b/tests/integration/evm/jsonrpc/openrpc_test.go index 89abece0..c6489a75 100644 --- a/tests/integration/evm/jsonrpc/openrpc_test.go +++ b/tests/integration/evm/jsonrpc/openrpc_test.go @@ -28,8 +28,6 @@ type openRPCDoc struct { } `json:"methods"` } -const defaultAPIURL = "http://127.0.0.1:1317" - // testOpenRPCDiscoverMethodCatalog verifies `rpc_discover` returns a populated // method catalog with expected namespace coverage. // @@ -135,11 +133,11 @@ func TestOpenRPCHTTPDocumentEndpoint(t *testing.T) { t.Helper() node := evmtest.NewEVMNode(t, "lumera-openrpc-http", 120) - node.AppendStartArgs("--api.enable=true") + evmtest.EnableAPIInAppToml(t, node.HomeDir(), node.APIListenAddress()) node.StartAndWaitRPC() defer node.Stop() - httpDoc := mustFetchOpenRPCDocOverHTTP(t, defaultAPIURL+appopenrpc.HTTPPath, 20*time.Second) + httpDoc := mustFetchOpenRPCDocOverHTTP(t, node.APIURL()+appopenrpc.HTTPPath, 20*time.Second) rpcDoc := mustDiscoverOpenRPCDoc(t, node) httpMethods := methodNameSet(httpDoc) @@ -164,7 +162,7 @@ func TestOpenRPCHTTPPOSTProxy(t *testing.T) { t.Helper() node := evmtest.NewEVMNode(t, "lumera-openrpc-http-proxy", 120) - node.AppendStartArgs("--api.enable=true") + evmtest.EnableAPIInAppToml(t, node.HomeDir(), node.APIListenAddress()) node.StartAndWaitRPC() defer node.Stop() @@ -172,7 +170,7 @@ func TestOpenRPCHTTPPOSTProxy(t *testing.T) { node.MustJSONRPC(t, "eth_chainId", []any{}, &directChainID) body := bytes.NewBufferString(`{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}`) - req, err := http.NewRequest(http.MethodPost, defaultAPIURL+appopenrpc.HTTPPath, body) + req, err := http.NewRequest(http.MethodPost, node.APIURL()+appopenrpc.HTTPPath, body) if err != nil { t.Fatalf("build /openrpc.json POST request: %v", err) } @@ -207,12 +205,12 @@ func TestOpenRPCHTTPPOSTProxyRPCDiscoverAlias(t *testing.T) { t.Helper() node := evmtest.NewEVMNode(t, "lumera-openrpc-http-discover-alias", 120) - node.AppendStartArgs("--api.enable=true") + evmtest.EnableAPIInAppToml(t, node.HomeDir(), node.APIListenAddress()) node.StartAndWaitRPC() defer node.Stop() body := bytes.NewBufferString(`{"jsonrpc":"2.0","id":1,"method":"rpc.discover","params":[]}`) - req, err := http.NewRequest(http.MethodPost, defaultAPIURL+appopenrpc.HTTPPath, body) + req, err := http.NewRequest(http.MethodPost, node.APIURL()+appopenrpc.HTTPPath, body) if err != nil { t.Fatalf("build /openrpc.json rpc.discover request: %v", err) } diff --git a/tests/integration/evm/mempool/evmigration_zero_signer_test.go b/tests/integration/evm/mempool/evmigration_zero_signer_test.go index 327ee876..feb3dbb9 100644 --- a/tests/integration/evm/mempool/evmigration_zero_signer_test.go +++ b/tests/integration/evm/mempool/evmigration_zero_signer_test.go @@ -45,6 +45,31 @@ func TestEVMigrationZeroSignerTxBroadcastSyncWithMempoolEnabled(t *testing.T) { require.NotContains(t, res.Log, "tx must have at least one signer") } +func TestEVMigrationZeroSignerTxBroadcastSyncAfterLegacyMainnetConfigMigration(t *testing.T) { + node := evmtest.NewEVMNode(t, "lumera-mainnet-1", 20) + evmtest.WriteLegacyPreEVMAppToml(t, node.HomeDir(), -1) + legacyPriv := secp256k1.GenPrivKey() + addGenesisLegacyAccount(t, node, sdk.AccAddress(legacyPriv.PubKey().Address().Bytes())) + node.StartAndWaitRPC() + defer node.Stop() + node.WaitForBlockNumberAtLeast(t, 1, 20*time.Second) + + appTomlBytes, err := os.ReadFile(filepath.Join(node.HomeDir(), "config", "app.toml")) + require.NoError(t, err) + appToml := string(appTomlBytes) + require.Contains(t, appToml, "max-txs = 10000") + require.Contains(t, appToml, "[evm.mempool]") + require.Contains(t, appToml, "global-slots = 5120") + require.Contains(t, appToml, "global-queue = 1024") + require.NotContains(t, appToml, "insert-queue-size") + + txBytes := validZeroSignerMigrationTxBytes(t, node.ChainID(), legacyPriv) + res := broadcastSync(t, node, txBytes) + + require.Zero(t, res.Code, "zero-signer migration tx must pass CheckTx after legacy config migration: %s", res.Log) + require.NotContains(t, res.Log, "tx must have at least one signer") +} + func TestEVMigrationProofValidNonexistentLegacyAccountRejectedByAnte(t *testing.T) { node := evmtest.NewEVMNode(t, "lumera-evmigration-no-legacy", 20) node.StartAndWaitRPC() diff --git a/tests/integration/evm/mempool/metrics_prometheus_e2e_test.go b/tests/integration/evm/mempool/metrics_prometheus_e2e_test.go index d50ceb34..e26fc606 100644 --- a/tests/integration/evm/mempool/metrics_prometheus_e2e_test.go +++ b/tests/integration/evm/mempool/metrics_prometheus_e2e_test.go @@ -26,16 +26,14 @@ import ( // /metrics?format=prometheus endpoint exposes the lumera_evm_mempool_* gauge // metrics end-to-end: a real node reads live mempool state on each HTTP GET. func TestPrometheusMetricsExposeMempoolGauges(t *testing.T) { - apiPort := evmtest.FreePort(t) - apiAddr := fmt.Sprintf("tcp://127.0.0.1:%d", apiPort) - metricsURL := fmt.Sprintf("http://127.0.0.1:%d/metrics?format=prometheus", apiPort) - node := evmtest.NewEVMNode(t, "lumera-mempool-prom", 600) node.AppendStartArgs("--json-rpc.api", "eth,txpool,net,web3") - evmtest.EnablePrometheusMetrics(t, node.HomeDir(), apiAddr) + evmtest.EnablePrometheusMetrics(t, node.HomeDir(), node.APIListenAddress()) node.StartAndWaitRPC() defer node.Stop() + metricsURL := node.APIURL() + "/metrics?format=prometheus" + // Wait for the API/telemetry server to be ready. waitForHTTP(t, metricsURL, 30*time.Second) @@ -121,16 +119,14 @@ func TestPrometheusMetricsExposeMempoolGauges(t *testing.T) { // increases when malformed tx bytes are submitted via CometBFT broadcast_tx_sync. // This exercises the exact path instrumented in the wrapped CheckTxHandler. func TestPrometheusRejectionsCountedViaCometCheckTx(t *testing.T) { - apiPort := evmtest.FreePort(t) - apiAddr := fmt.Sprintf("tcp://127.0.0.1:%d", apiPort) - metricsURL := fmt.Sprintf("http://127.0.0.1:%d/metrics?format=prometheus", apiPort) - node := evmtest.NewEVMNode(t, "lumera-mempool-rej-prom", 600) node.AppendStartArgs("--json-rpc.api", "eth,txpool,net,web3") - evmtest.EnablePrometheusMetrics(t, node.HomeDir(), apiAddr) + evmtest.EnablePrometheusMetrics(t, node.HomeDir(), node.APIListenAddress()) node.StartAndWaitRPC() defer node.Stop() + metricsURL := node.APIURL() + "/metrics?format=prometheus" + waitForHTTP(t, metricsURL, 30*time.Second) // At baseline, the counter label pair should not exist yet (CounterVec diff --git a/tests/integration/evmtest/exports.go b/tests/integration/evmtest/exports.go index db6935cc..69db54c1 100644 --- a/tests/integration/evmtest/exports.go +++ b/tests/integration/evmtest/exports.go @@ -34,6 +34,10 @@ func SetMempoolMaxTxsInAppToml(t *testing.T, homeDir string, maxTxs int) { setMempoolMaxTxsInAppToml(t, homeDir, maxTxs) } +func WriteLegacyPreEVMAppToml(t *testing.T, homeDir string, maxTxs int) { + writeLegacyPreEVMAppToml(t, homeDir, maxTxs) +} + func SetCometMempoolSize(t *testing.T, homeDir string, size int) { setCometMempoolSize(t, homeDir, size) } @@ -46,6 +50,11 @@ func EnablePrometheusMetrics(t *testing.T, homeDir string, apiAddress string) { enablePrometheusMetrics(t, homeDir, apiAddress) } +func EnableAPIInAppToml(t *testing.T, homeDir string, apiAddress string) { + appToml := enableAPIInAppToml(t, homeDir, apiAddress) + writeAppToml(t, homeDir, appToml) +} + func FreePort(t *testing.T) int { return freePort(t) } diff --git a/tests/integration/evmtest/node_helpers.go b/tests/integration/evmtest/node_helpers.go index dc1b3548..2915d43b 100644 --- a/tests/integration/evmtest/node_helpers.go +++ b/tests/integration/evmtest/node_helpers.go @@ -30,6 +30,7 @@ type nodePorts struct { CometRPC int // CometBFT RPC (default 26657) GRPC int // gRPC (default 9090) GRPCWeb int // gRPC-Web (default 9091) + API int // Cosmos REST API (default 1317) ABCI int // ABCI (default 26658) P2P int // P2P (default 26656) } @@ -47,6 +48,7 @@ type evmNode struct { rpcURL string // HTTP JSON-RPC endpoint. wsURL string // WebSocket JSON-RPC endpoint. + apiURL string // Cosmos REST API endpoint. cometRPCURL string // Comet RPC endpoint for Cosmos CLI commands. startArgs []string // Cached `lumerad start` arguments (base args + extraStartArgs). @@ -82,6 +84,7 @@ func newEVMNode(t *testing.T, chainID string, haltHeight int) *evmNode { haltHeight: haltHeight, rpcURL: fmt.Sprintf("http://127.0.0.1:%d", ports.JSONRPC), wsURL: fmt.Sprintf("ws://127.0.0.1:%d", ports.JSONWSRPC), + apiURL: fmt.Sprintf("http://127.0.0.1:%d", ports.API), cometRPCURL: fmt.Sprintf("tcp://127.0.0.1:%d", ports.CometRPC), startArgs: buildStartArgs(homeDir, ports, haltHeight), } @@ -96,6 +99,7 @@ func (n *evmNode) refreshPorts() { ports := reserveNodePorts(n.t) n.rpcURL = fmt.Sprintf("http://127.0.0.1:%d", ports.JSONRPC) n.wsURL = fmt.Sprintf("ws://127.0.0.1:%d", ports.JSONWSRPC) + n.apiURL = fmt.Sprintf("http://127.0.0.1:%d", ports.API) n.cometRPCURL = fmt.Sprintf("tcp://127.0.0.1:%d", ports.CometRPC) n.startArgs = append(buildStartArgs(n.homeDir, ports, n.haltHeight), n.extraStartArgs...) } @@ -176,6 +180,10 @@ func (n *evmNode) RPCURL() string { return n.rpcURL } func (n *evmNode) WSURL() string { return n.wsURL } +func (n *evmNode) APIURL() string { return n.apiURL } + +func (n *evmNode) APIListenAddress() string { return strings.Replace(n.apiURL, "http://", "tcp://", 1) } + func (n *evmNode) CometRPCURL() string { return n.cometRPCURL } func (n *evmNode) HomeDir() string { return n.homeDir } @@ -279,6 +287,7 @@ func reserveNodePorts(t *testing.T) nodePorts { CometRPC: freePort(t), GRPC: freePort(t), GRPCWeb: freePort(t), + API: freePort(t), ABCI: freePort(t), P2P: freePort(t), } @@ -365,7 +374,7 @@ func setupGenesisWithGentx(t *testing.T, repoRoot, binPath, homeDir, chainID str t.Fatalf("json-rpc defaults not written to app.toml:\n%s", appTomlStr) } if !strings.Contains(appTomlStr, "[mempool]") || - !strings.Contains(appTomlStr, "max-txs = 5000") { + !strings.Contains(appTomlStr, "max-txs = 10000") { t.Fatalf("app-side mempool defaults not written to app.toml:\n%s", appTomlStr) } @@ -476,6 +485,30 @@ func setMempoolMaxTxsInAppToml(t *testing.T, homeDir string, maxTxs int) { } } +// writeLegacyPreEVMAppToml replaces app.toml with the minimal shape an +// upgraded pre-EVM home can have: normal SDK sections, but no [evm], +// [evm.mempool], [json-rpc], [tls], or [lumera.*] sections. +func writeLegacyPreEVMAppToml(t *testing.T, homeDir string, maxTxs int) { + t.Helper() + + appTomlPath := filepath.Join(homeDir, "config", "app.toml") + legacyToml := fmt.Sprintf(` +[api] +enable = true +address = "tcp://127.0.0.1:1317" + +[grpc] +enable = false + +[mempool] +max-txs = %d +`, maxTxs) + + if err := os.WriteFile(appTomlPath, []byte(legacyToml), 0o644); err != nil { + t.Fatalf("write legacy app.toml: %v", err) + } +} + // setCometMempoolSize sets `size` under the `[mempool]` section in config.toml. // This controls how many txs CometBFT accepts into its mempool before rejecting. func setCometMempoolSize(t *testing.T, homeDir string, size int) { @@ -508,6 +541,20 @@ func setCometMempoolSize(t *testing.T, homeDir string, size int) { func enablePrometheusMetrics(t *testing.T, homeDir string, apiAddress string) { t.Helper() + s := enableAPIInAppToml(t, homeDir, apiAddress) + + // Enable telemetry (section-aware to avoid matching other "enabled" keys). + s = regexp.MustCompile(`(?m)(^\[telemetry\]\n(?:[^\[].*\n)*?)^enabled = (true|false)`). + ReplaceAllString(s, "${1}enabled = true") + s = regexp.MustCompile(`(?m)^prometheus-retention-time = [0-9]+`). + ReplaceAllString(s, "prometheus-retention-time = 60") + + writeAppToml(t, homeDir, s) +} + +func enableAPIInAppToml(t *testing.T, homeDir string, apiAddress string) string { + t.Helper() + appTomlPath := filepath.Join(homeDir, "config", "app.toml") appToml, err := os.ReadFile(appTomlPath) if err != nil { @@ -515,25 +562,23 @@ func enablePrometheusMetrics(t *testing.T, homeDir string, apiAddress string) { } s := string(appToml) - // Enable the API server (hosts /metrics). - s = regexp.MustCompile(`(?m)(^\[api\]\n(?:.*\n)*?)^enable = false`). + updated := regexp.MustCompile(`(?m)(^\[api\]\n(?:[^\[].*\n)*?)^enable = (true|false)`). ReplaceAllString(s, "${1}enable = true") + updated = regexp.MustCompile(`(?m)(^\[api\]\n(?:[^\[].*\n)*?)^address = "[^"]+"`). + ReplaceAllString(updated, fmt.Sprintf(`${1}address = "%s"`, apiAddress)) - // Set API listen address to the allocated port. - s = regexp.MustCompile(`(?m)^address = "tcp://localhost:1317"`). - ReplaceAllString(s, fmt.Sprintf(`address = "%s"`, apiAddress)) + if updated == s { + t.Fatalf("failed to enable API in app.toml") + } - // Enable telemetry (section-aware to avoid matching other "enabled" keys). - s = regexp.MustCompile(`(?m)(^\[telemetry\]\n(?:.*\n)*?)^enabled = false`). - ReplaceAllString(s, "${1}enabled = true") - s = regexp.MustCompile(`(?m)^prometheus-retention-time = [0-9]+`). - ReplaceAllString(s, "prometheus-retention-time = 60") + return updated +} - if s == string(appToml) { - t.Fatalf("failed to enable Prometheus metrics in app.toml") - } +func writeAppToml(t *testing.T, homeDir string, content string) { + t.Helper() - if err := os.WriteFile(appTomlPath, []byte(s), 0o644); err != nil { + appTomlPath := filepath.Join(homeDir, "config", "app.toml") + if err := os.WriteFile(appTomlPath, []byte(content), 0o644); err != nil { t.Fatalf("write app.toml: %v", err) } } diff --git a/tests/systemtests/go.mod b/tests/systemtests/go.mod index b86a639d..5d81abb8 100644 --- a/tests/systemtests/go.mod +++ b/tests/systemtests/go.mod @@ -186,7 +186,7 @@ require ( golang.org/x/crypto v0.48.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.41.0 // indirect + golang.org/x/sys v0.42.0 // indirect golang.org/x/term v0.40.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 // indirect diff --git a/tests/systemtests/go.sum b/tests/systemtests/go.sum index 4a026675..b453d68a 100644 --- a/tests/systemtests/go.sum +++ b/tests/systemtests/go.sum @@ -1265,6 +1265,8 @@ golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=