diff --git a/Makefile.devnet b/Makefile.devnet index 2f193d4c..1692255b 100644 --- a/Makefile.devnet +++ b/Makefile.devnet @@ -662,7 +662,7 @@ devnet-update-scripts: .PHONY: devnet-new-1120 .PHONY: devnet-upgrade-1110 devnet-upgrade-1111 devnet-upgrade-1120 devnet-upgrade-1201 -.PHONY: devnet-evm-upgrade +.PHONY: devnet-evm-upgrade devnet-upgrade-1202 # Upgrade a running devnet to a pre-downloaded lumera version. # Expects devnet/bin-/ to already contain the binaries. @@ -726,7 +726,7 @@ devnet-evm-upgrade: @echo "Logging to $(DEVNET_EVM_UPGRADE_LOG)" @bash -c 'set -euo pipefail; { \ BASE_VERSION=v1.12.0; \ - EVM_VERSION=v1.20.1; \ + EVM_VERSION=v1.20.2; \ echo "==> Stage: install $$BASE_VERSION devnet"; \ if ! $(MAKE) devnet-down; then \ echo "ERROR: stage install $$BASE_VERSION devnet failed during devnet-down" >&2; \ @@ -768,7 +768,7 @@ devnet-evm-upgrade: exit 1; \ fi; \ echo "==> Stage: upgrade to $$EVM_VERSION"; \ - if ! $(MAKE) devnet-upgrade-1201; then \ + if ! $(MAKE) devnet-upgrade-1202; then \ echo "ERROR: stage upgrade to $$EVM_VERSION failed" >&2; \ exit 1; \ fi; \ diff --git a/devnet/tests/evmigration/migrate_validators.go b/devnet/tests/evmigration/migrate_validators.go index b25e9d30..582c1988 100644 --- a/devnet/tests/evmigration/migrate_validators.go +++ b/devnet/tests/evmigration/migrate_validators.go @@ -736,7 +736,7 @@ func verifySupernodeMigration( for i, preHist := range preSN.PrevSupernodeAccounts { postHist := postSN.PrevSupernodeAccounts[i] if postHist.Account != preHist.Account || postHist.Height != preHist.Height { - return fmt.Errorf("PrevSupernodeAccounts[%d] changed: expected account=%s height=%d got account=%s height=%d", + return fmt.Errorf("PrevSupernodeAccounts[%d] changed: expected account=%s height=%s got account=%s height=%s", i, preHist.Account, preHist.Height, postHist.Account, postHist.Height) } } diff --git a/devnet/tests/evmigration/prepare.go b/devnet/tests/evmigration/prepare.go index d949961b..dd847f03 100644 --- a/devnet/tests/evmigration/prepare.go +++ b/devnet/tests/evmigration/prepare.go @@ -1303,18 +1303,24 @@ const infrastructureCandidateReadyTimeout = 90 * time.Second // exist on this host (e.g. governance_key on a secondary validator) it just // returns false after the first quick check without sleeping. func waitForInfrastructureKeyReady(name string, timeout time.Duration) bool { - if keyExists(name) && readStatusRegistryMnemonic(name) != "" { + // Probe with the silent lookup: absence is the expected outcome for + // candidates that don't apply to this host and must not log WARN. + registryMnemonic := func() string { + mnemonic, _ := lookupStatusRegistryMnemonic(name) + return mnemonic + } + if keyExists(name) && registryMnemonic() != "" { return true } // If neither the keyring nor the registry knows about this name at all, // there's nothing to wait for — it's a candidate that doesn't apply to // this host (e.g. governance_key on a secondary validator). - if !keyExists(name) && readStatusRegistryMnemonic(name) == "" { + if !keyExists(name) && registryMnemonic() == "" { return false } deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { - if keyExists(name) && readStatusRegistryMnemonic(name) != "" { + if keyExists(name) && registryMnemonic() != "" { return true } time.Sleep(3 * time.Second) @@ -1344,8 +1350,8 @@ func recordInfrastructureLegacyAccounts(af *AccountsFile, existingByName map[str if _, ok := existingByName[addr]; ok { continue } - mnemonic := readStatusRegistryMnemonic(name) - if mnemonic == "" { + mnemonic, found := lookupStatusRegistryMnemonic(name) + if !found || mnemonic == "" { log.Printf(" WARN: %s has no mnemonic in status registry; skipping", name) continue } diff --git a/devnet/tests/evmigration/status_registry.go b/devnet/tests/evmigration/status_registry.go index 67fdf81d..48c36d2e 100644 --- a/devnet/tests/evmigration/status_registry.go +++ b/devnet/tests/evmigration/status_registry.go @@ -30,56 +30,32 @@ func loadStatusRegistryAccounts() ([]statusRegistryAccount, error) { return accounts, nil } -func readStatusRegistryMnemonic(name string) string { +// lookupStatusRegistryMnemonic reports whether `name` is tracked in the +// status registry, without logging when it isn't. Absence is a normal outcome +// when probing infrastructure-key candidates that don't apply to this host +// (e.g. governance_key on a secondary validator). +func lookupStatusRegistryMnemonic(name string) (string, bool) { accounts, err := loadStatusRegistryAccounts() if err != nil { log.Printf(" WARN: cannot read account registry %s: %v", statusRegistryFile(), err) - return "" + return "", false } for _, account := range accounts { if account.Name == name { - return strings.TrimSpace(account.Mnemonic) + return strings.TrimSpace(account.Mnemonic), true } } - log.Printf(" WARN: account %q not found in status registry %s", name, statusRegistryFile()) - return "" + return "", false } -// appendStatusRegistryAccount adds a {name, address, mnemonic} entry to the -// shared status registry if it isn't already present. Idempotent by name. -func appendStatusRegistryAccount(name, address, mnemonic string) { - registryFile := statusRegistryFile() - data, err := os.ReadFile(registryFile) - if err != nil { - log.Printf(" WARN: cannot read account registry %s: %v", registryFile, err) - return - } - var accounts []map[string]any - if err := json.Unmarshal(data, &accounts); err != nil { - log.Printf(" WARN: cannot parse account registry %s: %v", registryFile, err) - return - } - for _, account := range accounts { - if fmtName, _ := account["name"].(string); fmtName == name { - return - } - } - accounts = append(accounts, map[string]any{ - "name": name, - "address": address, - "mnemonic": mnemonic, - }) - encoded, err := json.MarshalIndent(accounts, "", " ") - if err != nil { - log.Printf(" WARN: cannot encode updated account registry %s: %v", registryFile, err) - return - } - encoded = append(encoded, '\n') - if err := os.WriteFile(registryFile, encoded, 0o644); err != nil { - log.Printf(" WARN: failed to append to account registry %s: %v", registryFile, err) - return +// readStatusRegistryMnemonic is the lookup for accounts that are expected to +// be registered (validator keys); it warns when the entry is missing. +func readStatusRegistryMnemonic(name string) string { + mnemonic, found := lookupStatusRegistryMnemonic(name) + if !found { + log.Printf(" WARN: account %q not found in status registry %s", name, statusRegistryFile()) } - log.Printf(" appended %s to account registry %s", name, registryFile) + return mnemonic } func updateStatusRegistryAddress(name, newAddr string) { @@ -105,7 +81,9 @@ func updateStatusRegistryAddress(name, newAddr string) { } } if !updated { - log.Printf(" WARN: account %q not found in status registry %s", name, registryFile) + // Not tracked: the registry only holds infrastructure keys (validator, + // governance, funders); generated pre-evm-* fixtures live solely in + // accounts-devnet.json, so skipping them silently is the normal case. return } diff --git a/devnet/tests/evmigration/status_registry_test.go b/devnet/tests/evmigration/status_registry_test.go new file mode 100644 index 00000000..d795aa9c --- /dev/null +++ b/devnet/tests/evmigration/status_registry_test.go @@ -0,0 +1,116 @@ +package main + +import ( + "bytes" + "encoding/json" + "log" + "os" + "path/filepath" + "strings" + "testing" +) + +// writeTestStatusRegistry points *flagFile at a temp accounts file so +// statusRegistryFile() resolves to /accounts.json, then writes the given +// entries there. Restores the flag on cleanup. +func writeTestStatusRegistry(t *testing.T, accounts []statusRegistryAccount) string { + t.Helper() + dir := t.TempDir() + prev := *flagFile + *flagFile = filepath.Join(dir, "accounts-devnet.json") + t.Cleanup(func() { *flagFile = prev }) + + registryFile := filepath.Join(dir, "accounts.json") + data, err := json.Marshal(accounts) + if err != nil { + t.Fatalf("marshal registry: %v", err) + } + if err := os.WriteFile(registryFile, data, 0o644); err != nil { + t.Fatalf("write registry: %v", err) + } + return registryFile +} + +// captureLog redirects the standard logger to a buffer for the duration of +// the test and returns it. +func captureLog(t *testing.T) *bytes.Buffer { + t.Helper() + var buf bytes.Buffer + log.SetOutput(&buf) + t.Cleanup(func() { log.SetOutput(os.Stderr) }) + return &buf +} + +func TestUpdateStatusRegistryAddressUpdatesTrackedAccount(t *testing.T) { + registryFile := writeTestStatusRegistry(t, []statusRegistryAccount{ + {Name: "governance_key", Address: "lumera1old", Mnemonic: "m"}, + }) + + updateStatusRegistryAddress("governance_key", "lumera1new") + + data, err := os.ReadFile(registryFile) + if err != nil { + t.Fatalf("read registry: %v", err) + } + var accounts []statusRegistryAccount + if err := json.Unmarshal(data, &accounts); err != nil { + t.Fatalf("parse registry: %v", err) + } + if len(accounts) != 1 || accounts[0].Address != "lumera1new" { + t.Fatalf("expected governance_key address updated to lumera1new, got %+v", accounts) + } +} + +// Generated pre-evm-* fixtures are tracked in accounts-devnet.json, never in +// the per-host status registry; skipping them must not spam WARN logs. +func TestUpdateStatusRegistryAddressSilentlySkipsUntrackedAccount(t *testing.T) { + registryFile := writeTestStatusRegistry(t, []statusRegistryAccount{ + {Name: "governance_key", Address: "lumera1old", Mnemonic: "m"}, + }) + before, err := os.ReadFile(registryFile) + if err != nil { + t.Fatalf("read registry: %v", err) + } + buf := captureLog(t) + + updateStatusRegistryAddress("pre-evm-val5-003", "lumera1new") + + if out := buf.String(); strings.Contains(out, "WARN") { + t.Fatalf("expected no WARN for untracked account, got log output: %q", out) + } + after, err := os.ReadFile(registryFile) + if err != nil { + t.Fatalf("read registry: %v", err) + } + if !bytes.Equal(before, after) { + t.Fatalf("registry file changed for untracked account:\nbefore: %s\nafter: %s", before, after) + } +} + +func TestLookupStatusRegistryMnemonicFound(t *testing.T) { + writeTestStatusRegistry(t, []statusRegistryAccount{ + {Name: "sncli-account", Address: "lumera1abc", Mnemonic: " word1 word2 "}, + }) + + mnemonic, found := lookupStatusRegistryMnemonic("sncli-account") + if !found || mnemonic != "word1 word2" { + t.Fatalf("lookupStatusRegistryMnemonic = (%q, %v), want (\"word1 word2\", true)", mnemonic, found) + } +} + +// Infrastructure-key probes check hosts that legitimately don't have the key +// (e.g. governance_key on a secondary validator); the lookup must stay silent. +func TestLookupStatusRegistryMnemonicNotFoundIsSilent(t *testing.T) { + writeTestStatusRegistry(t, []statusRegistryAccount{ + {Name: "supernova_validator_5_key", Address: "lumera1abc", Mnemonic: "m"}, + }) + buf := captureLog(t) + + mnemonic, found := lookupStatusRegistryMnemonic("governance_key") + if found || mnemonic != "" { + t.Fatalf("lookupStatusRegistryMnemonic = (%q, %v), want (\"\", false)", mnemonic, found) + } + if out := buf.String(); strings.Contains(out, "WARN") { + t.Fatalf("expected no WARN for absent probe candidate, got log output: %q", out) + } +}