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
2 changes: 0 additions & 2 deletions AGENTS.md

This file was deleted.

10 changes: 6 additions & 4 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,10 +324,12 @@ func (app *App) GetSubspace(moduleName string) paramstypes.Subspace {
// This needs to be called BEFORE app.Load()
func (app *App) setupUpgrades() {
params := appParams.AppUpgradeParams{
ChainID: app.ChainID(),
Logger: app.Logger(),
ModuleManager: app.ModuleManager,
Configurator: app.Configurator(),
ChainID: app.ChainID(),
Logger: app.Logger(),
ModuleManager: app.ModuleManager,
Configurator: app.Configurator(),
ActionKeeper: &app.ActionKeeper,
SupernodeKeeper: app.SupernodeKeeper,
}

allUpgrades := upgrades.AllUpgrades(params)
Expand Down
13 changes: 11 additions & 2 deletions app/upgrades/params/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,21 @@ package params
import (
"cosmossdk.io/log"
"github.com/cosmos/cosmos-sdk/types/module"

actionmodulekeeper "github.com/LumeraProtocol/lumera/x/action/v1/keeper"
sntypes "github.com/LumeraProtocol/lumera/x/supernode/v1/types"
)

type AppUpgradeParams struct {
ChainID string

Logger log.Logger
Logger log.Logger
ModuleManager *module.Manager
Configurator module.Configurator
Configurator module.Configurator

// Keepers required by custom upgrade handlers. These are populated by the app
// at startup (before state load) so upgrade handlers can safely perform
// bespoke store migrations beyond RunMigrations.
ActionKeeper *actionmodulekeeper.Keeper
SupernodeKeeper sntypes.SupernodeKeeper
}
8 changes: 8 additions & 0 deletions app/upgrades/upgrades.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import (
upgrade_v1_6_1 "github.com/LumeraProtocol/lumera/app/upgrades/v1_6_1"
upgrade_v1_8_0 "github.com/LumeraProtocol/lumera/app/upgrades/v1_8_0"
upgrade_v1_8_4 "github.com/LumeraProtocol/lumera/app/upgrades/v1_8_4"
upgrade_v1_8_8 "github.com/LumeraProtocol/lumera/app/upgrades/v1_8_8"
)

// ======================================================================================================================
// Upgrade overview:
// ======================================================================================================================
Expand All @@ -25,6 +27,7 @@ import (
// | v1.8.0 | standard | testnet/devnet: add PFM, drop NFT | Store upgrade gated to non-mainnet; handler is migrations only
// | v1.8.4 | standard | mainnet: add PFM, drop NFT | Store upgrade gated to mainnet; handler is migrations only
// | v1.8.5 | standard | none | Migrations only
// | v1.8.8 | custom | none | Backfills action/supernode secondary indices
// ======================================================================================================================

type UpgradeConfig struct {
Expand All @@ -46,6 +49,7 @@ var upgradeNames = []string{
upgrade_v1_8_0.UpgradeName,
upgrade_v1_8_4.UpgradeName,
upgradeNameV185,
upgrade_v1_8_8.UpgradeName,
}

var NoUpgradeConfig = UpgradeConfig{
Expand Down Expand Up @@ -92,6 +96,10 @@ func SetupUpgrades(upgradeName string, params appParams.AppUpgradeParams) (Upgra
return UpgradeConfig{
Handler: standardUpgradeHandler(upgradeNameV185, params),
}, true
case upgrade_v1_8_8.UpgradeName:
return UpgradeConfig{
Handler: upgrade_v1_8_8.CreateUpgradeHandler(params),
}, true

// add future upgrades here
default:
Expand Down
8 changes: 8 additions & 0 deletions app/upgrades/upgrades_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
upgrade_v1_6_1 "github.com/LumeraProtocol/lumera/app/upgrades/v1_6_1"
upgrade_v1_8_0 "github.com/LumeraProtocol/lumera/app/upgrades/v1_8_0"
upgrade_v1_8_4 "github.com/LumeraProtocol/lumera/app/upgrades/v1_8_4"
upgrade_v1_8_8 "github.com/LumeraProtocol/lumera/app/upgrades/v1_8_8"
actiontypes "github.com/LumeraProtocol/lumera/x/action/v1/types"
)

Expand All @@ -25,6 +26,7 @@ func TestUpgradeNamesOrder(t *testing.T) {
upgrade_v1_8_0.UpgradeName,
upgrade_v1_8_4.UpgradeName,
upgradeNameV185,
upgrade_v1_8_8.UpgradeName,
}
require.Equal(t, expected, upgradeNames, "upgradeNames should stay in ascending order")
}
Expand Down Expand Up @@ -64,6 +66,12 @@ func TestSetupUpgradesAndHandlers(t *testing.T) {
continue
}

// v1.8.8 requires full keeper wiring; exercising it here would require
// a full app harness. This test only verifies registration and gating.
if upgradeName == upgrade_v1_8_8.UpgradeName {
continue
}

vm, err := config.Handler(goCtx, upgradetypes.Plan{}, module.VersionMap{})
require.NoError(t, err, "handler should succeed for %s on %s", upgradeName, tt.chainID)
require.NotNil(t, vm, "handler should return a version map")
Expand Down
77 changes: 77 additions & 0 deletions app/upgrades/v1_8_8/upgrade.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package v1_8_8

import (
"context"
"fmt"

upgradetypes "cosmossdk.io/x/upgrade/types"

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

appParams "github.com/LumeraProtocol/lumera/app/upgrades/params"
actiontypes "github.com/LumeraProtocol/lumera/x/action/v1/types"
)

// UpgradeName is the on-chain name used for this upgrade.
const UpgradeName = "v1.8.8"

// CreateUpgradeHandler creates an upgrade handler for v1.8.8.
//
// This upgrade backfills secondary indices introduced in prior releases:
// - action module: state/creator/type/block/supernode indices
// - supernode module: supernodeAccount -> validator operator address index
//
// The index keys are derived from existing primary records, so no StoreUpgrades
// (added/removed module store keys) are required.
func CreateUpgradeHandler(p appParams.AppUpgradeParams) upgradetypes.UpgradeHandler {
return func(goCtx context.Context, _ upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) {
p.Logger.Info(fmt.Sprintf("Starting upgrade %s...", UpgradeName))

ctx := sdk.UnwrapSDKContext(goCtx)

p.Logger.Info("Running module migrations...")
newVM, err := p.ModuleManager.RunMigrations(ctx, p.Configurator, fromVM)
if err != nil {
p.Logger.Error("Failed to run migrations", "error", err)
return nil, fmt.Errorf("failed to run migrations: %w", err)
}
p.Logger.Info("Module migrations completed.")

if p.ActionKeeper == nil || p.SupernodeKeeper == nil {
return nil, fmt.Errorf("%s upgrade requires keepers to be wired (action=%v supernode=%v)", UpgradeName, p.ActionKeeper != nil, p.SupernodeKeeper != nil)
}

// Backfill action indices by replaying SetAction for each existing action.
var actionIDs []string
if err := p.ActionKeeper.IterateActions(ctx, func(a *actiontypes.Action) bool {
actionIDs = append(actionIDs, a.ActionID)
return false
}); err != nil {
return nil, fmt.Errorf("failed to iterate actions for index backfill: %w", err)
}
for _, id := range actionIDs {
a, found := p.ActionKeeper.GetActionByID(ctx, id)
if !found {
continue
}
if err := p.ActionKeeper.SetAction(ctx, a); err != nil {
return nil, fmt.Errorf("failed to backfill action indices for action_id=%s: %w", id, err)
}
}

// Backfill supernode account index by replaying SetSuperNode for each supernode.
supernodes, err := p.SupernodeKeeper.GetAllSuperNodes(ctx)
if err != nil {
return nil, fmt.Errorf("failed to list supernodes for account index backfill: %w", err)
}
for _, sn := range supernodes {
if err := p.SupernodeKeeper.SetSuperNode(ctx, sn); err != nil {
return nil, fmt.Errorf("failed to backfill supernode account index for validator=%s: %w", sn.ValidatorAddress, err)
}
}

p.Logger.Info(fmt.Sprintf("Successfully completed upgrade %s", UpgradeName))
return newVM, nil
}
}
2 changes: 1 addition & 1 deletion docs/static/openapi.yml

Large diffs are not rendered by default.

37 changes: 34 additions & 3 deletions tests/systemtests/metrics_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,40 @@ func decodeMetricsMsg(t *testing.T, txJSON string) sntypes.MsgReportSupernodeMet
return msg
}

msgRaw := gjson.Get(txJSON, "tx.body.messages.0").Raw
require.NotEmpty(t, msgRaw, "message not found in tx: %s", txJSON)
require.NoError(t, json.Unmarshal([]byte(msgRaw), &msg))
msgRaw := gjson.Get(txJSON, "tx.body.messages.0")
require.True(t, msgRaw.Exists(), "message not found in tx: %s", txJSON)

msg.ValidatorAddress = msgRaw.Get("validator_address").String()
msg.SupernodeAccount = msgRaw.Get("supernode_account").String()

metrics := msgRaw.Get("metrics")
msg.Metrics.VersionMajor = uint32(metrics.Get("version_major").Uint())
msg.Metrics.VersionMinor = uint32(metrics.Get("version_minor").Uint())
msg.Metrics.VersionPatch = uint32(metrics.Get("version_patch").Uint())
msg.Metrics.UptimeSeconds = metrics.Get("uptime_seconds").Float()

rawPorts := metrics.Get("open_ports").Array()
msg.Metrics.OpenPorts = make([]sntypes.PortStatus, 0, len(rawPorts))
for _, p := range rawPorts {
stateField := p.Get("state")
var stateVal sntypes.PortState
switch stateField.Type {
case gjson.String:
if v, ok := sntypes.PortState_value[stateField.String()]; ok {
stateVal = sntypes.PortState(v)
} else {
require.FailNow(t, "unknown port state", "state=%s tx=%s", stateField.String(), txJSON)
}
default:
stateVal = sntypes.PortState(stateField.Int())
}

msg.Metrics.OpenPorts = append(msg.Metrics.OpenPorts, sntypes.PortStatus{
Port: uint32(p.Get("port").Uint()),
State: stateVal,
})
}

return msg
}

Expand Down
95 changes: 76 additions & 19 deletions x/action/v1/keeper/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import (
"encoding/binary"
"fmt"
"slices"
"strconv"
"strings"

"cosmossdk.io/errors"
"cosmossdk.io/math"
storetypes "cosmossdk.io/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"

Expand All @@ -18,10 +20,13 @@ import (

// Key prefixes for store
const (
ActionKeyPrefix = "Action/value/"
ActionCountKey = "Action/count/"
ActionByStatePrefix = "Action/state/"
ActionByCreatorPrefix = "Action/creator/"
ActionKeyPrefix = "Action/value/"
ActionCountKey = "Action/count/"
ActionByStatePrefix = "Action/state/"
ActionByCreatorPrefix = "Action/creator/"
ActionByTypePrefix = "Action/type/"
ActionByBlockHeightPrefix = "Action/block/"
ActionBySuperNodePrefix = "Action/supernode/"
)

// RegisterAction creates and configures a new action with default parameters
Expand Down Expand Up @@ -334,7 +339,7 @@ func (k *Keeper) SetAction(ctx sdk.Context, action *actiontypes.Action) error {
return err
}

// Handle state indexing
// Handle state index
if found && existingAction.State != action.State {
oldStateKey := []byte(ActionByStatePrefix + existingAction.State.String() + "/" + action.ActionID)
if err := store.Delete(oldStateKey); err != nil {
Expand All @@ -345,17 +350,77 @@ func (k *Keeper) SetAction(ctx sdk.Context, action *actiontypes.Action) error {
"old_state", existingAction.State.String(),
"new_state", action.State.String())
}

stateKey := []byte(ActionByStatePrefix + action.State.String() + "/" + action.ActionID)
if err := store.Set(stateKey, []byte{1}); err != nil { // Just a marker
return err
}

// Index by creator
// Index by creator (assumed immutable in practice)
if found && existingAction.Creator != action.Creator {
oldCreatorKey := []byte(ActionByCreatorPrefix + existingAction.Creator + "/" + action.ActionID)
if err := store.Delete(oldCreatorKey); err != nil {
return err
}
}
creatorKey := []byte(ActionByCreatorPrefix + action.Creator + "/" + action.ActionID)
if err := store.Set(creatorKey, []byte{1}); err != nil { // Just a marker
return err
}

// Index by type
if found && existingAction.ActionType != action.ActionType {
oldTypeKey := []byte(ActionByTypePrefix + existingAction.ActionType.String() + "/" + action.ActionID)
if err := store.Delete(oldTypeKey); err != nil {
return err
}
}
typeKey := []byte(ActionByTypePrefix + action.ActionType.String() + "/" + action.ActionID)
if err := store.Set(typeKey, []byte{1}); err != nil { // Just a marker
return err
}

// Index by block height
if found && existingAction.BlockHeight != action.BlockHeight {
oldBlockKey := []byte(ActionByBlockHeightPrefix + strconv.FormatInt(existingAction.BlockHeight, 10) + "/" + action.ActionID)
if err := store.Delete(oldBlockKey); err != nil {
return err
}
}
blockKey := []byte(ActionByBlockHeightPrefix + strconv.FormatInt(action.BlockHeight, 10) + "/" + action.ActionID)
if err := store.Set(blockKey, []byte{1}); err != nil { // Just a marker
return err
}

// Index by supernodes
existingSN := make(map[string]struct{})
if found {
for _, sn := range existingAction.SuperNodes {
existingSN[sn] = struct{}{}
}
}
currentSN := make(map[string]struct{})
for _, sn := range action.SuperNodes {
currentSN[sn] = struct{}{}
}

// Remove stale supernode index entries
if found {
for sn := range existingSN {
if _, stillPresent := currentSN[sn]; !stillPresent {
oldSNKey := []byte(ActionBySuperNodePrefix + sn + "/" + action.ActionID)
if err := store.Delete(oldSNKey); err != nil {
return err
}
}
}
}
// Add/update current supernode index entries
for sn := range currentSN {
snKey := []byte(ActionBySuperNodePrefix + sn + "/" + action.ActionID)
if err := store.Set(snKey, []byte{1}); err != nil { // Just a marker
return err
}
}
return nil
}

Expand Down Expand Up @@ -391,7 +456,8 @@ func (k *Keeper) IterateActions(ctx sdk.Context, handler func(*actiontypes.Actio
store := k.storeService.OpenKVStore(ctx)

// Use prefix iterator to get all actions with the ActionKeyPrefix
iter, err := store.Iterator([]byte(ActionKeyPrefix), nil)
actionPrefix := []byte(ActionKeyPrefix)
iter, err := store.Iterator(actionPrefix, storetypes.PrefixEndBytes(actionPrefix))
if err != nil {
return errors.Wrap(err, "failed to create iterator for actions")
}
Expand Down Expand Up @@ -424,27 +490,18 @@ func (k *Keeper) IterateActionsByState(ctx sdk.Context, state actiontypes.Action
// Create the state-specific prefix for iteration
// The key format is ActionByStatePrefix + state + "/" + actionID
prefixStr := ActionByStatePrefix + state.String() + "/"
prefixLen := len(prefixStr)
statePrefix := []byte(prefixStr)

// Use prefix iterator to get all actions with this state
iter, err := store.Iterator(statePrefix, nil)
iter, err := store.Iterator(statePrefix, storetypes.PrefixEndBytes(statePrefix))
if err != nil {
return errors.Wrap(err, "failed to create iterator for actions by state")
}
defer iter.Close()

for ; iter.Valid(); iter.Next() {
key := iter.Key()
keyStr := string(key)

// Validate the key has the correct prefix to prevent panics
if len(keyStr) <= prefixLen || !strings.HasPrefix(keyStr, prefixStr) {
continue
}

// Extract the action ID from the key
actionID := keyStr[prefixLen:]
actionID := string(iter.Key()[len(statePrefix):])

// Get the full action using the actionID
action, found := k.GetActionByID(ctx, actionID)
Expand Down
Loading
Loading