Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8fd01da
fix(evmigration): enforce strict supernode ownership lookup
mateeullahmalik Jul 28, 2026
831adfe
fix(evmigration): preflight strict ownership before execution
mateeullahmalik Jul 28, 2026
744cfcb
fix(evmigration): preserve dual supernode relationships
mateeullahmalik Jul 28, 2026
ab5abbf
fix(evmigration): compare canonical supernode identities
mateeullahmalik Jul 28, 2026
e56dbf7
app: register v1.20.2 migration-only upgrade handler
mateeullahmalik Jul 28, 2026
022ac6f
fix(evmigration): reject destination supernode ownership collisions
mateeullahmalik Jul 28, 2026
4987917
test(devnet): isolate recursive make dry runs
mateeullahmalik Jul 28, 2026
a79d362
Revert "app: register v1.20.2 migration-only upgrade handler"
mateeullahmalik Jul 28, 2026
2132a0d
fix(evmigration): preserve Everlight distribution state across valida…
mateeullahmalik Aug 1, 2026
198a3ee
test(evmigration): cover continuity plan in validator migration mocks
mateeullahmalik Aug 1, 2026
b1db325
app: register v1.20.2 as the evmigration consensus activation boundary
mateeullahmalik Aug 1, 2026
1c08b89
test(upgrades): prove v1.20.2 on both live arrival shapes
mateeullahmalik Aug 1, 2026
d5a748b
test(upgrades): pin v1.20.2 module versions against live chain state
mateeullahmalik Aug 1, 2026
2acc17f
devnet+docs: add v1.20.2 upgrade target and fold operator runbook fin…
mateeullahmalik Aug 4, 2026
e357c6e
devnet: fix two bugs that prevented lumera-uploader from ever starting
mateeullahmalik Aug 4, 2026
94ce613
devnet: fix upgrade halt detection reporting a false alarm on a healt…
mateeullahmalik Aug 4, 2026
97f696d
feat(feemarket): raise base fee fivefold
akobrin1 Aug 4, 2026
37a4721
devnet: add mainnet-shaped pre-EVM config + genesis for one-hop rehea…
mateeullahmalik Aug 4, 2026
7709d58
devnet: add v1.20.1-shaped EVM genesis for a non-vacuous feemarket gate
mateeullahmalik Aug 4, 2026
d3f97e6
fix(tests): derive EVM fee expectations from config after 5x base-fee…
mateeullahmalik Aug 5, 2026
a95ed37
fix(tests): address Copilot review — real upgrade wiring, realistic f…
mateeullahmalik Aug 5, 2026
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
12 changes: 6 additions & 6 deletions devnet/tests/evmigration/migrate_validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -731,13 +731,13 @@ func verifySupernodeMigration(
return fmt.Errorf("PrevSupernodeAccounts last entry account mismatch: expected %s got %s",
newAddr, lastEntry.Account)
}
// Existing history entries matching old account should now reference new account.
// Every existing history entry is immutable; migration only appends the
// newly effective account marker.
for i, preHist := range preSN.PrevSupernodeAccounts {
if preHist.Account == legacyAddr {
if postSN.PrevSupernodeAccounts[i].Account != newAddr {
return fmt.Errorf("PrevSupernodeAccounts[%d] account not migrated: expected %s got %s",
i, newAddr, postSN.PrevSupernodeAccounts[i].Account)
}
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",
i, preHist.Account, preHist.Height, postHist.Account, postHist.Height)
}
}
log.Printf(" supernode account history: %d entries (including migration entry)", len(postSN.PrevSupernodeAccounts))
Expand Down
311 changes: 311 additions & 0 deletions tests/integration/evmigration/supernode_ownership_execution_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
package integration_test

import (
"bytes"
"strings"
"testing"

sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"

evmigrationkeeper "github.com/LumeraProtocol/lumera/x/evmigration/keeper"
evmigrationtypes "github.com/LumeraProtocol/lumera/x/evmigration/types"
sntypes "github.com/LumeraProtocol/lumera/x/supernode/v1/types"
)

func validMigrationSupernode(valAddr sdk.ValAddress, account sdk.AccAddress) sntypes.SuperNode {
return sntypes.SuperNode{
ValidatorAddress: valAddr.String(),
SupernodeAccount: account.String(),
Note: "1.0.0",
PrevIpAddresses: []*sntypes.IPAddressHistory{{Address: "127.0.0.1", Height: 1}},
States: []*sntypes.SuperNodeStateRecord{{State: sntypes.SuperNodeStateActive, Height: 1}},
P2PPort: "4445",
}
}

func (s *MigrationIntegrationSuite) supernodeStoreSnapshot() map[string][]byte {
store := s.ctx.KVStore(s.app.GetKey(sntypes.StoreKey))
it := store.Iterator(nil, nil)
s.Require().NoError(it.Error())
defer it.Close()

snapshot := make(map[string][]byte)
for ; it.Valid(); it.Next() {
snapshot[string(bytes.Clone(it.Key()))] = bytes.Clone(it.Value())
}
return snapshot
}

func (s *MigrationIntegrationSuite) putSupernodePrimary(valAddr sdk.ValAddress, sn sntypes.SuperNode) {
store := s.ctx.KVStore(s.app.GetKey(sntypes.StoreKey))
store.Set(sntypes.GetSupernodeKey(valAddr), s.app.AppCodec().MustMarshal(&sn))
}

func (s *MigrationIntegrationSuite) putSupernodeIndex(account sdk.AccAddress, valAddr sdk.ValAddress) {
store := s.ctx.KVStore(s.app.GetKey(sntypes.StoreKey))
key := append(bytes.Clone(sntypes.SuperNodeByAccountKey), []byte(account.String())...)
store.Set(key, valAddr)
}

func (s *MigrationIntegrationSuite) putSupernodeIndexText(account string, valAddr sdk.ValAddress) {
store := s.ctx.KVStore(s.app.GetKey(sntypes.StoreKey))
key := append(bytes.Clone(sntypes.SuperNodeByAccountKey), []byte(account)...)
store.Set(key, valAddr)
}

func (s *MigrationIntegrationSuite) assertClaimOwnershipCorruptionRejected(
setup func(legacyAddr sdk.AccAddress),
wantErr string,
) {
s.enableMigration()
coins := sdk.NewCoins(sdk.NewInt64Coin("ulume", 123_456))
legacyPrivKey, legacyAddr := s.createFundedLegacyAccount(coins)
newPrivKey, newAddr := createNewEVMAddress(s.T())
setup(legacyAddr)

beforeStore := s.supernodeStoreSnapshot()
beforeLegacy := s.app.BankKeeper.GetAllBalances(s.ctx, legacyAddr)
beforeNew := s.app.BankKeeper.GetAllBalances(s.ctx, newAddr)

_, err := s.msgServer.ClaimLegacyAccount(s.ctx, newClaimMsg(s.T(), legacyPrivKey, legacyAddr, newPrivKey, newAddr))
s.Require().Error(err)
s.Require().Contains(err.Error(), wantErr)
s.Require().Equal(beforeStore, s.supernodeStoreSnapshot(), "failed DeliverTx must not mutate supernode primary/index state")
s.Require().Equal(beforeLegacy, s.app.BankKeeper.GetAllBalances(s.ctx, legacyAddr), "strict ownership failure must precede balance migration")
s.Require().Equal(beforeNew, s.app.BankKeeper.GetAllBalances(s.ctx, newAddr), "strict ownership failure must precede destination writes")
hasRecord, recordErr := s.keeper.MigrationRecords.Has(s.ctx, legacyAddr.String())
s.Require().NoError(recordErr)
s.Require().False(hasRecord)
}

func (s *MigrationIntegrationSuite) TestClaimLegacyAccount_RejectsMissingSupernodeAccountIndexBeforeWrites() {
s.assertClaimOwnershipCorruptionRejected(func(legacyAddr sdk.AccAddress) {
valAddr := sdk.ValAddress(testAddressBytes("missing-index-val"))
s.putSupernodePrimary(valAddr, validMigrationSupernode(valAddr, legacyAddr))
}, "missing account index")
}

func (s *MigrationIntegrationSuite) TestClaimLegacyAccount_RejectsStaleSupernodeAccountIndexBeforeWrites() {
s.assertClaimOwnershipCorruptionRejected(func(legacyAddr sdk.AccAddress) {
s.putSupernodeIndex(legacyAddr, sdk.ValAddress(testAddressBytes("stale-index-val")))
}, "does not resolve to a primary record")
}

func (s *MigrationIntegrationSuite) TestClaimLegacyAccount_RejectsDuplicateSupernodeOwnershipBeforeWrites() {
s.assertClaimOwnershipCorruptionRejected(func(legacyAddr sdk.AccAddress) {
first := sdk.ValAddress(testAddressBytes("duplicate-owner-one"))
second := sdk.ValAddress(testAddressBytes("duplicate-owner-two"))
s.putSupernodePrimary(first, validMigrationSupernode(first, legacyAddr))
s.putSupernodePrimary(second, validMigrationSupernode(second, legacyAddr))
s.putSupernodeIndex(legacyAddr, first)
}, "multiple primary records claim")
}

func (s *MigrationIntegrationSuite) TestClaimLegacyAccount_RejectsCanonicalDestinationSupernodeOwnerBeforeWrites() {
s.enableMigration()
coins := sdk.NewCoins(sdk.NewInt64Coin("ulume", 123_456))
legacyPrivKey, legacyAddr := s.createFundedLegacyAccount(coins)
newPrivKey, newAddr := createNewEVMAddress(s.T())

sourceVal := sdk.ValAddress(testAddressBytes("claim-source-val"))
s.Require().NoError(s.app.SupernodeKeeper.SetSuperNode(s.ctx, validMigrationSupernode(sourceVal, legacyAddr)))
destinationVal := sdk.ValAddress(testAddressBytes("claim-dest-val"))
destinationSN := validMigrationSupernode(destinationVal, newAddr)
destinationSN.SupernodeAccount = strings.ToUpper(newAddr.String())
s.putSupernodePrimary(destinationVal, destinationSN)
s.putSupernodeIndexText(destinationSN.SupernodeAccount, destinationVal)

beforeStore := s.supernodeStoreSnapshot()
beforeLegacy := s.app.BankKeeper.GetAllBalances(s.ctx, legacyAddr)
beforeNew := s.app.BankKeeper.GetAllBalances(s.ctx, newAddr)

_, err := s.msgServer.ClaimLegacyAccount(s.ctx, newClaimMsg(s.T(), legacyPrivKey, legacyAddr, newPrivKey, newAddr))
s.Require().Error(err)
s.Require().Contains(err.Error(), "destination supernode account")
s.Require().Equal(beforeStore, s.supernodeStoreSnapshot())
s.Require().Equal(beforeLegacy, s.app.BankKeeper.GetAllBalances(s.ctx, legacyAddr))
s.Require().Equal(beforeNew, s.app.BankKeeper.GetAllBalances(s.ctx, newAddr))
}

func (s *MigrationIntegrationSuite) TestClaimLegacyAccount_RejectsMalformedSupernodePrimaryBeforeWrites() {
s.assertClaimOwnershipCorruptionRejected(func(_ sdk.AccAddress) {
valAddr := sdk.ValAddress(testAddressBytes("malformed-primary"))
store := s.ctx.KVStore(s.app.GetKey(sntypes.StoreKey))
store.Set(sntypes.GetSupernodeKey(valAddr), []byte{0xff, 0xff, 0xff})
}, "unmarshal supernode")
}

func (s *MigrationIntegrationSuite) TestClaimLegacyAccount_RejectsMalformedEmbeddedSupernodeAccountBeforeWrites() {
s.assertClaimOwnershipCorruptionRejected(func(_ sdk.AccAddress) {
valAddr := sdk.ValAddress(testAddressBytes("malformed-account"))
sn := validMigrationSupernode(valAddr, sdk.AccAddress(testAddressBytes("valid-account")))
sn.SupernodeAccount = "not-a-lumera-address"
s.putSupernodePrimary(valAddr, sn)
}, "invalid embedded supernode account")
}

func (s *MigrationIntegrationSuite) TestMigrateValidator_RejectsSourceOwnershipCorruptionBeforeValidatorMutation() {
s.enableMigration()
operatorCoins := sdk.NewCoins(sdk.NewInt64Coin("ulume", 2_000_000))
legacyPrivKey, legacyAddr := s.createFundedLegacyAccount(operatorCoins)
oldValAddr, _ := s.createTestValidator(legacyAddr, sdkmath.NewInt(1_000_000))
newPrivKey, newAddr := createNewEVMAddress(s.T())

// A valid primary claiming the source account without its mandatory account
// index must abort before V1 reward withdrawal or V2 validator re-keying.
s.putSupernodePrimary(oldValAddr, validMigrationSupernode(oldValAddr, legacyAddr))
beforeStore := s.supernodeStoreSnapshot()
beforeValidator, err := s.app.StakingKeeper.GetValidator(s.ctx, oldValAddr)
s.Require().NoError(err)

_, err = s.msgServer.MigrateValidator(s.ctx, newValidatorMsg(s.T(), legacyPrivKey, legacyAddr, newPrivKey, newAddr))
s.Require().Error(err)
s.Require().Contains(err.Error(), "missing account index")
s.Require().Equal(beforeStore, s.supernodeStoreSnapshot())
afterValidator, getErr := s.app.StakingKeeper.GetValidator(s.ctx, oldValAddr)
s.Require().NoError(getErr)
s.Require().Equal(beforeValidator, afterValidator, "ownership corruption must be rejected before validator mutation")
_, newValidatorErr := s.app.StakingKeeper.GetValidator(s.ctx, sdk.ValAddress(newAddr))
s.Require().Error(newValidatorErr)
}

func (s *MigrationIntegrationSuite) TestMigrateValidator_RejectsCanonicalDestinationSupernodeOwnerBeforeValidatorMutation() {
s.enableMigration()
operatorCoins := sdk.NewCoins(sdk.NewInt64Coin("ulume", 2_000_000))
legacyPrivKey, legacyAddr := s.createFundedLegacyAccount(operatorCoins)
oldValAddr, _ := s.createTestValidator(legacyAddr, sdkmath.NewInt(1_000_000))
newPrivKey, newAddr := createNewEVMAddress(s.T())

s.Require().NoError(s.app.SupernodeKeeper.SetSuperNode(s.ctx, validMigrationSupernode(oldValAddr, legacyAddr)))
destinationVal := sdk.ValAddress(testAddressBytes("validator-dest-val"))
destinationSN := validMigrationSupernode(destinationVal, newAddr)
destinationSN.SupernodeAccount = strings.ToUpper(newAddr.String())
s.putSupernodePrimary(destinationVal, destinationSN)
s.putSupernodeIndexText(destinationSN.SupernodeAccount, destinationVal)

beforeStore := s.supernodeStoreSnapshot()
beforeValidator, err := s.app.StakingKeeper.GetValidator(s.ctx, oldValAddr)
s.Require().NoError(err)

_, err = s.msgServer.MigrateValidator(s.ctx, newValidatorMsg(s.T(), legacyPrivKey, legacyAddr, newPrivKey, newAddr))
s.Require().Error(err)
s.Require().Contains(err.Error(), "destination supernode account")
s.Require().Equal(beforeStore, s.supernodeStoreSnapshot())
afterValidator, getErr := s.app.StakingKeeper.GetValidator(s.ctx, oldValAddr)
s.Require().NoError(getErr)
s.Require().Equal(beforeValidator, afterValidator)
}

func (s *MigrationIntegrationSuite) TestMigrateValidatorSupernode_TwoDistinctRecordsRealStore() {
legacyAddr := sdk.AccAddress(testAddressBytes("legacy-owner"))
newAddr := sdk.AccAddress(testAddressBytes("new-owner"))
oldValAddr := sdk.ValAddress(legacyAddr)
newValAddr := sdk.ValAddress(newAddr)
accountOwnedVal := sdk.ValAddress(testAddressBytes("account-owned-val"))
independentAccount := sdk.AccAddress(testAddressBytes("independent-owner"))

accountOwned := validMigrationSupernode(accountOwnedVal, legacyAddr)
accountOwned.PrevSupernodeAccounts = []*sntypes.SupernodeAccountHistory{{Account: legacyAddr.String(), Height: 7}}
validatorAssociated := validMigrationSupernode(oldValAddr, independentAccount)
validatorAssociated.PrevSupernodeAccounts = []*sntypes.SupernodeAccountHistory{{Account: independentAccount.String(), Height: 9}}

s.Require().NoError(s.app.SupernodeKeeper.SetSuperNode(s.ctx, accountOwned))
s.Require().NoError(s.app.SupernodeKeeper.SetSuperNode(s.ctx, validatorAssociated))
s.Require().NoError(s.keeper.MigrateValidatorSupernode(s.ctx, oldValAddr, newValAddr, legacyAddr, newAddr))

migratedOwned, found := s.app.SupernodeKeeper.QuerySuperNode(s.ctx, accountOwnedVal)
s.Require().True(found)
s.Require().Equal(newAddr.String(), migratedOwned.SupernodeAccount)
s.Require().Len(migratedOwned.PrevSupernodeAccounts, 2)
s.Require().Equal(legacyAddr.String(), migratedOwned.PrevSupernodeAccounts[0].Account)
s.Require().Equal(newAddr.String(), migratedOwned.PrevSupernodeAccounts[1].Account)

_, found = s.app.SupernodeKeeper.QuerySuperNode(s.ctx, oldValAddr)
s.Require().False(found)
migratedValidator, found := s.app.SupernodeKeeper.QuerySuperNode(s.ctx, newValAddr)
s.Require().True(found)
s.Require().Equal(independentAccount.String(), migratedValidator.SupernodeAccount)
s.Require().Equal(validatorAssociated.PrevSupernodeAccounts, migratedValidator.PrevSupernodeAccounts)

byOldOwner, found, err := s.app.SupernodeKeeper.GetSuperNodeByAccount(s.ctx, legacyAddr.String())
s.Require().NoError(err)
s.Require().False(found)
s.Require().Empty(byOldOwner.ValidatorAddress)
byNewOwner, found, err := s.app.SupernodeKeeper.GetSuperNodeByAccount(s.ctx, newAddr.String())
s.Require().NoError(err)
s.Require().True(found)
s.Require().Equal(accountOwnedVal.String(), byNewOwner.ValidatorAddress)
byIndependentOwner, found, err := s.app.SupernodeKeeper.GetSuperNodeByAccount(s.ctx, independentAccount.String())
s.Require().NoError(err)
s.Require().True(found)
s.Require().Equal(newValAddr.String(), byIndependentOwner.ValidatorAddress)
}

func (s *MigrationIntegrationSuite) TestMigrateValidatorSupernode_AlternateEncodingSelfOwnedRealStore() {
legacyAddr := sdk.AccAddress(testAddressBytes("alternate-owner"))
newAddr := sdk.AccAddress(testAddressBytes("alternate-new"))
oldValAddr := sdk.ValAddress(legacyAddr)
newValAddr := sdk.ValAddress(newAddr)
sn := validMigrationSupernode(oldValAddr, legacyAddr)
sn.ValidatorAddress = strings.ToUpper(sn.ValidatorAddress)
sn.SupernodeAccount = strings.ToUpper(sn.SupernodeAccount)
sn.PrevSupernodeAccounts = []*sntypes.SupernodeAccountHistory{{Account: sn.SupernodeAccount, Height: 7}}
s.putSupernodePrimary(oldValAddr, sn)
store := s.ctx.KVStore(s.app.GetKey(sntypes.StoreKey))
store.Set(append(bytes.Clone(sntypes.SuperNodeByAccountKey), []byte(sn.SupernodeAccount)...), oldValAddr)

s.Require().NoError(s.keeper.MigrateValidatorSupernode(s.ctx, oldValAddr, newValAddr, legacyAddr, newAddr))

_, found := s.app.SupernodeKeeper.QuerySuperNode(s.ctx, oldValAddr)
s.Require().False(found)
migrated, found := s.app.SupernodeKeeper.QuerySuperNode(s.ctx, newValAddr)
s.Require().True(found)
s.Require().Equal(newAddr.String(), migrated.SupernodeAccount)
s.Require().Len(migrated.PrevSupernodeAccounts, 2)
s.Require().Equal(sn.SupernodeAccount, migrated.PrevSupernodeAccounts[0].Account)
s.Require().Equal(newAddr.String(), migrated.PrevSupernodeAccounts[1].Account)

_, found, err := s.app.SupernodeKeeper.GetSuperNodeByAccount(s.ctx, legacyAddr.String())
s.Require().NoError(err)
s.Require().False(found, "legacy owner must not be restored under canonical encoding")
byNewOwner, found, err := s.app.SupernodeKeeper.GetSuperNodeByAccount(s.ctx, newAddr.String())
s.Require().NoError(err)
s.Require().True(found)
s.Require().Equal(newValAddr.String(), byNewOwner.ValidatorAddress)
}

func (s *MigrationIntegrationSuite) TestMigrationEstimate_ValidatorPrimaryOnlyHasSupernodeParity() {
s.enableMigration()
_, legacyAddr := s.createFundedLegacyAccount(sdk.NewCoins(sdk.NewInt64Coin("ulume", 2_000_000)))
oldValAddr, _ := s.createTestValidator(legacyAddr, sdkmath.NewInt(1_000_000))
independentAccount := sdk.AccAddress(testAddressBytes("estimate-independent"))
s.Require().NoError(s.app.SupernodeKeeper.SetSuperNode(s.ctx, validMigrationSupernode(oldValAddr, independentAccount)))

queryServer := evmigrationkeeper.NewQueryServerImpl(s.keeper)
estimate, err := queryServer.MigrationEstimate(s.ctx, &evmigrationtypes.QueryMigrationEstimateRequest{
LegacyAddress: legacyAddr.String(),
})
s.Require().NoError(err)
s.Require().True(estimate.IsValidator)
s.Require().True(estimate.HasSupernode, "B-only validator primary must be visible to estimate just as it is to execution")

_, newAddr := createNewEVMAddress(s.T())
s.Require().NoError(s.keeper.MigrateValidatorSupernode(s.ctx, oldValAddr, sdk.ValAddress(newAddr), legacyAddr, newAddr))
_, found := s.app.SupernodeKeeper.QuerySuperNode(s.ctx, oldValAddr)
s.Require().False(found)
migrated, found := s.app.SupernodeKeeper.QuerySuperNode(s.ctx, sdk.ValAddress(newAddr))
s.Require().True(found)
s.Require().Equal(independentAccount.String(), migrated.SupernodeAccount)
}

func testAddressBytes(seed string) []byte {
out := make([]byte, 20)
copy(out, []byte(seed))
return out
}

func TestOwnershipIntegrityHelpersUseTwentyByteAddresses(t *testing.T) {
require.Len(t, testAddressBytes("short"), 20)
}
2 changes: 2 additions & 0 deletions tests/scripts/devnet-makefile.bats
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ teardown() {

@test "external version staging uses downloaded binaries without requiring claims" {
run make -C "$REPO_ROOT" -n devnet-stage-external-version \
MAKE=/bin/true \
VERSION=v1.12.0 \
EXTERNAL_GENESIS_FILE="$EXTERNAL_GENESIS"

Expand All @@ -36,6 +37,7 @@ teardown() {

@test "remote version target syncs staged runtime and runs docker remotely" {
run make -C "$REPO_ROOT" -n devnet-new-remote-version \
MAKE=/bin/true \
VERSION=v1.12.0 \
EXTERNAL_GENESIS_FILE="$EXTERNAL_GENESIS" \
REMOTE_DEVNET_HOST=example-devnet \
Expand Down
22 changes: 12 additions & 10 deletions x/evmigration/keeper/migrate_supernode.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package keeper

import (
"fmt"

sdk "github.com/cosmos/cosmos-sdk/types"

sntypes "github.com/LumeraProtocol/lumera/x/supernode/v1/types"
Expand All @@ -9,25 +11,25 @@ import (
// MigrateSupernode updates the SupernodeAccount field if legacyAddr is a supernode.
// Also records the migration in PrevSupernodeAccounts history.
func (k Keeper) MigrateSupernode(ctx sdk.Context, legacyAddr, newAddr sdk.AccAddress) error {
sn, found, err := k.supernodeKeeper.GetSuperNodeByAccount(ctx, legacyAddr.String())
sn, found, err := k.supernodeKeeper.StrictGetSuperNodeByAccount(ctx, legacyAddr.String())
if err != nil {
return err
return fmt.Errorf("resolve source supernode ownership: %w", err)
}
return k.migrateValidatedSupernode(ctx, newAddr, sn, found)
}

// migrateValidatedSupernode mutates the exact record returned by the strict
// pre-mutation ownership lookup performed by ClaimLegacyAccount.
func (k Keeper) migrateValidatedSupernode(ctx sdk.Context, newAddr sdk.AccAddress, sn sntypes.SuperNode, found bool) error {
if !found {
return nil
}

// Update the supernode account field to new address.
sn.SupernodeAccount = newAddr.String()

// Update legacy address references in existing history entries.
legacyAddrStr := legacyAddr.String()
for i := range sn.PrevSupernodeAccounts {
if sn.PrevSupernodeAccounts[i].Account == legacyAddrStr {
sn.PrevSupernodeAccounts[i].Account = newAddr.String()
}
}

// Preserve the existing account timeline verbatim. Migration changes the
// effective account, so append exactly one transition at this block height.
// Record the migration as a new account-history entry.
sn.PrevSupernodeAccounts = append(sn.PrevSupernodeAccounts, &sntypes.SupernodeAccountHistory{
Account: newAddr.String(),
Expand Down
Loading
Loading