Skip to content
Open
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
6 changes: 3 additions & 3 deletions Makefile.devnet
Original file line number Diff line number Diff line change
Expand Up @@ -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-<VERSION>/ to already contain the binaries.
Expand Down Expand Up @@ -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; \
Expand Down Expand Up @@ -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; \
Expand Down
2 changes: 1 addition & 1 deletion devnet/tests/evmigration/migrate_validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
16 changes: 11 additions & 5 deletions devnet/tests/evmigration/prepare.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand Down
58 changes: 18 additions & 40 deletions devnet/tests/evmigration/status_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
}

Expand Down
116 changes: 116 additions & 0 deletions devnet/tests/evmigration/status_registry_test.go
Original file line number Diff line number Diff line change
@@ -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 <tmp>/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)
}
}
Loading