Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rollapp): delete stale state updates #1176

Merged
merged 8 commits into from
Sep 12, 2024
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
1 change: 1 addition & 0 deletions app/keepers/keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,7 @@ func (a *AppKeepers) SetupHooks() {
a.TxFeesKeeper.Hooks(),
a.DelayedAckKeeper.GetEpochHooks(),
a.DymNSKeeper.GetEpochHooks(),
a.RollappKeeper.GetEpochHooks(),
),
)

Expand Down
5 changes: 4 additions & 1 deletion proto/dymensionxyz/dymension/rollapp/params.proto
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import "cosmos/base/v1beta1/coin.proto";
// Params defines the parameters for the module.
message Params {
option (gogoproto.goproto_stringer) = false;

// dispute_period_in_blocks the number of blocks it takes
// to change a status of a state from received to finalized.
// during that period, any user could submit fraud proof
Expand All @@ -29,4 +29,7 @@ message Params {
(gogoproto.nullable) = false,
(gogoproto.moretags) = "yaml:\"app_registration_fee\""
];
// state_info_deletion_epoch_identifier is used to control the interval at which the state info records will be deleted.
string state_info_deletion_epoch_identifier = 8
[ (gogoproto.moretags) = "yaml:\"state_info_deletion_epoch_identifier\"" ];
}
7 changes: 7 additions & 0 deletions proto/dymensionxyz/dymension/rollapp/state_info.proto
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package dymensionxyz.dymension.rollapp;
option go_package = "github.com/dymensionxyz/dymension/v3/x/rollapp/types";

import "gogoproto/gogo.proto";
import "google/protobuf/timestamp.proto";

import "dymensionxyz/dymension/rollapp/block_descriptor.proto";
import "dymensionxyz/dymension/common/status.proto";
Expand Down Expand Up @@ -46,6 +47,12 @@ message StateInfo {
// BDs is a list of block description objects (one per block)
// the list must be ordered by height, starting from startHeight to startHeight+numBlocks-1
BlockDescriptors BDs = 9 [(gogoproto.nullable) = false];
// created_at is the timestamp at which the StateInfo was created
google.protobuf.Timestamp created_at = 10 [
(gogoproto.stdtime) = true,
(gogoproto.nullable) = false,
(gogoproto.moretags) = "yaml:\"created_at\""
];
}

// StateInfoSummary is a compact representation of StateInfo
Expand Down
38 changes: 38 additions & 0 deletions x/rollapp/keeper/hooks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package keeper

import (
sdk "github.com/cosmos/cosmos-sdk/types"
epochstypes "github.com/osmosis-labs/osmosis/v15/x/epochs/types"
)

var _ epochstypes.EpochHooks = epochHooks{}

type epochHooks struct {
Keeper
}

func (k Keeper) GetEpochHooks() epochstypes.EpochHooks {
return epochHooks{
Keeper: k,
}
}

// AfterEpochEnd is the epoch end hook.
// We want to clean up all the state info records that are older than the sequencer unbonding time.
func (e epochHooks) AfterEpochEnd(ctx sdk.Context, epochIdentifier string, _ int64) error {
if epochIdentifier != e.StateInfoDeletionEpochIdentifier(ctx) {
return nil
}

currentTimestamp := ctx.BlockTime()
// for the time being, we can assume that the sequencer unbonding time will not change, therefore
// we can assume that the number of resulting deletable state updates will remain constant
seqUnbondingTime := e.sequencerKeeper.UnbondingTime(ctx)
endTimestamp := currentTimestamp.Add(-seqUnbondingTime)

e.DeleteStateInfoUntilTimestamp(ctx, endTimestamp)
return nil
}

// BeforeEpochStart is the epoch start hook.
func (e epochHooks) BeforeEpochStart(sdk.Context, string, int64) error { return nil }
3 changes: 2 additions & 1 deletion x/rollapp/keeper/latest_finalized_state_index.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package keeper
import (
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/dymensionxyz/dymension/v3/x/rollapp/types"
)

Expand Down Expand Up @@ -46,7 +47,7 @@ func (k Keeper) RemoveLatestFinalizedStateIndex(
))
}

// GetAllLatestFinalizedStateIndex returns all latestFinalizedStateIndex
// GetAllLatestFinalizedStateIndex returns latestFinalizedStateIndex for all rollapps
func (k Keeper) GetAllLatestFinalizedStateIndex(ctx sdk.Context) (list []types.StateInfoIndex) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.LatestFinalizedStateIndexKeyPrefix))
iterator := sdk.KVStorePrefixIterator(store, []byte{})
Expand Down
3 changes: 2 additions & 1 deletion x/rollapp/keeper/latest_state_info_index.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package keeper
import (
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/dymensionxyz/dymension/v3/x/rollapp/types"
)

Expand Down Expand Up @@ -46,7 +47,7 @@ func (k Keeper) RemoveLatestStateInfoIndex(
))
}

// GetAllLatestStateInfoIndex returns all latestStateInfoIndex
// GetAllLatestStateInfoIndex returns latestStateInfoIndex for all rollapps
func (k Keeper) GetAllLatestStateInfoIndex(ctx sdk.Context) (list []types.StateInfoIndex) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.LatestStateInfoIndexKeyPrefix))
iterator := sdk.KVStorePrefixIterator(store, []byte{})
Expand Down
5 changes: 5 additions & 0 deletions x/rollapp/keeper/liveness_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"slices"
"testing"
"time"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/dymensionxyz/sdk-utils/utils/urand"
Expand Down Expand Up @@ -147,6 +148,10 @@ type livenessMockSequencerKeeper struct {
jails map[string]int
}

func (l livenessMockSequencerKeeper) UnbondingTime(sdk.Context) (res time.Duration) {
return time.Minute
}

func newLivenessMockSequencerKeeper() livenessMockSequencerKeeper {
return livenessMockSequencerKeeper{
make(map[string]int),
Expand Down
13 changes: 12 additions & 1 deletion x/rollapp/keeper/msg_server_update_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,18 @@ func (k msgServer) UpdateState(goCtx context.Context, msg *types.MsgUpdateState)
})

creationHeight := uint64(ctx.BlockHeight())
stateInfo := types.NewStateInfo(msg.RollappId, newIndex, msg.Creator, msg.StartHeight, msg.NumBlocks, msg.DAPath, creationHeight, msg.BDs)
blockTime := ctx.BlockTime()
stateInfo := types.NewStateInfo(
msg.RollappId,
newIndex,
msg.Creator,
msg.StartHeight,
msg.NumBlocks,
msg.DAPath,
creationHeight,
msg.BDs,
blockTime,
)
// Write new state information to the store indexed by <RollappId,LatestStateInfoIndex>
k.SetStateInfo(ctx, *stateInfo)

Expand Down
6 changes: 6 additions & 0 deletions x/rollapp/keeper/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func (k Keeper) GetParams(ctx sdk.Context) types.Params {
k.LivenessSlashInterval(ctx),
k.LivenessJailBlocks(ctx),
k.AppRegistrationFee(ctx),
k.StateInfoDeletionEpochIdentifier(ctx),
)
}

Expand Down Expand Up @@ -48,3 +49,8 @@ func (k Keeper) AppRegistrationFee(ctx sdk.Context) (res sdk.Coin) {
k.paramstore.Get(ctx, types.KeyAppRegistrationFee, &res)
return
}

func (k Keeper) StateInfoDeletionEpochIdentifier(ctx sdk.Context) (res string) {
k.paramstore.Get(ctx, types.KeyStateInfoDeletionEpochIdentifier, &res)
return
}
58 changes: 58 additions & 0 deletions x/rollapp/keeper/state_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package keeper

import (
"fmt"
"time"

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

"github.com/dymensionxyz/dymension/v3/x/rollapp/types"
)

Expand All @@ -15,6 +17,12 @@ func (k Keeper) SetStateInfo(ctx sdk.Context, stateInfo types.StateInfo) {
store.Set(types.StateInfoKey(
stateInfo.StateInfoIndex,
), b)

// store a key prefixed with the creation timestamp
storeTS := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.TimestampedStateInfoKeyPrefix))
storeTS.Set(types.StateInfoTimestampKey(
stateInfo,
), []byte{})
}

// GetStateInfo returns a stateInfo from its index
Expand Down Expand Up @@ -85,3 +93,53 @@ func (k Keeper) GetAllStateInfo(ctx sdk.Context) (list []types.StateInfo) {

return
}

// DeleteStateInfoUntilTimestamp deletes all stateInfo until the given timestamp
func (k Keeper) DeleteStateInfoUntilTimestamp(ctx sdk.Context, endTimestampExcl time.Time) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.StateInfoKeyPrefix))
storeTS := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.TimestampedStateInfoKeyPrefix))

// Note that for the active sequencer, the latest state info index will not be within the range of
// the state updates to be deleted, as it will be more recent.
// For a sequencer that is inactive for 21 days or more, it will be within range, furthermore
// the latest state info index and the latest finalized state info index will be the same.
latestIndexes := k.GetAllLatestStateInfoIndex(ctx)
skipStateInfoIndexes := make(map[string]uint64, len(latestIndexes))
for _, index := range latestIndexes {
skipStateInfoIndexes[index.RollappId] = index.Index
}

k.IterateStateInfoWithTimestamp(storeTS, endTimestampExcl.UnixMicro(), func(keyTS []byte) bool {
key := types.StateInfoIndexKeyFromTimestampKey(keyTS)
// skip latest stateInfo and latest finalized stateInfo
stateInfoIndex := types.StateInfoIndexFromKey(key)
zale144 marked this conversation as resolved.
Show resolved Hide resolved
index := skipStateInfoIndexes[stateInfoIndex.RollappId]
if index == stateInfoIndex.Index {
return false
}

store.Delete(key)
storeTS.Delete(keyTS)
return false
})
}

// IterateStateInfoWithTimestamp iterates over stateInfo until timestamp
func (k Keeper) IterateStateInfoWithTimestamp(store prefix.Store, endTimestampUNIX int64, fn func(key []byte) (stop bool)) {
endKey := types.StateInfoTimestampKeyPrefix(endTimestampUNIX)
iterator := store.ReverseIterator(nil, endKey)

defer iterator.Close() // nolint: errcheck

for ; iterator.Valid(); iterator.Next() {
if fn(iterator.Key()) {
break
}
}
}

// HasStateInfoTimestampKey checks if the stateInfo has a timestamp key - used for testing
func (k Keeper) HasStateInfoTimestampKey(ctx sdk.Context, stateInfo types.StateInfo) bool {
storeTS := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.TimestampedStateInfoKeyPrefix))
return storeTS.Has(types.StateInfoTimestampKey(stateInfo))
}
91 changes: 80 additions & 11 deletions x/rollapp/keeper/state_info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ package keeper_test
import (
"strconv"
"testing"
"time"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

keepertest "github.com/dymensionxyz/dymension/v3/testutil/keeper"
"github.com/dymensionxyz/dymension/v3/testutil/nullify"
"github.com/dymensionxyz/dymension/v3/x/rollapp/keeper"
"github.com/dymensionxyz/dymension/v3/x/rollapp/types"
"github.com/stretchr/testify/require"
)

// Prevent strconv unused error
Expand Down Expand Up @@ -38,11 +41,11 @@ func createNStateInfo(keeper *keeper.Keeper, ctx sdk.Context, n int) ([]types.St
}

func TestStateInfoGet(t *testing.T) {
keeper, ctx := keepertest.RollappKeeper(t)
items, _ := createNStateInfo(keeper, ctx, 10)
k, ctx := keepertest.RollappKeeper(t)
items, _ := createNStateInfo(k, ctx, 10)
for _, item := range items {
item := item
rst, found := keeper.GetStateInfo(ctx,
rst, found := k.GetStateInfo(ctx,
item.StateInfoIndex.RollappId,
item.StateInfoIndex.Index,
)
Expand All @@ -55,14 +58,14 @@ func TestStateInfoGet(t *testing.T) {
}

func TestStateInfoRemove(t *testing.T) {
keeper, ctx := keepertest.RollappKeeper(t)
items, _ := createNStateInfo(keeper, ctx, 10)
k, ctx := keepertest.RollappKeeper(t)
items, _ := createNStateInfo(k, ctx, 10)
for _, item := range items {
keeper.RemoveStateInfo(ctx,
k.RemoveStateInfo(ctx,
item.StateInfoIndex.RollappId,
item.StateInfoIndex.Index,
)
_, found := keeper.GetStateInfo(ctx,
_, found := k.GetStateInfo(ctx,
item.StateInfoIndex.RollappId,
item.StateInfoIndex.Index,
)
Expand All @@ -71,10 +74,76 @@ func TestStateInfoRemove(t *testing.T) {
}

func TestStateInfoGetAll(t *testing.T) {
keeper, ctx := keepertest.RollappKeeper(t)
items, _ := createNStateInfo(keeper, ctx, 10)
k, ctx := keepertest.RollappKeeper(t)
items, _ := createNStateInfo(k, ctx, 10)
require.ElementsMatch(t,
nullify.Fill(items),
nullify.Fill(keeper.GetAllStateInfo(ctx)),
nullify.Fill(k.GetAllStateInfo(ctx)),
)
}

func TestKeeper_DeleteStateInfoUntilTimestamp(t *testing.T) {
k, ctx := keepertest.RollappKeeper(t)

ts1 := time.Date(2020, time.May, 1, 10, 22, 0, 0, time.UTC)
ts2 := ts1.Add(9 * time.Second)
ts3 := ts2.Add(11 * time.Second)
ts4 := ts3.Add(13 * time.Second)

items := []types.StateInfo{
{CreatedAt: ts1},
{CreatedAt: ts2},
{CreatedAt: ts3},
{CreatedAt: ts4},
}
for i := range items {
items[i].StateInfoIndex.RollappId = strconv.Itoa(i + 1)
items[i].StateInfoIndex.Index = 1 + uint64(i)

k.SetStateInfo(ctx, items[i])
}

lastItem := items[len(items)-1]
latestStateInfoIndex := types.StateInfoIndex{
RollappId: lastItem.StateInfoIndex.RollappId,
Index: lastItem.StateInfoIndex.Index,
}
k.SetLatestStateInfoIndex(ctx, latestStateInfoIndex)

// delete all before ts3: only ts3 and ts4 should be found
k.DeleteStateInfoUntilTimestamp(ctx, ts2.Add(time.Second))

for _, item := range items {
_, found := k.GetStateInfo(ctx,
item.StateInfoIndex.RollappId,
item.StateInfoIndex.Index,
)

foundTSKey := k.HasStateInfoTimestampKey(ctx, item)

if item.CreatedAt.After(ts2) {
assert.True(t, found)
assert.True(t, foundTSKey)
continue
}
assert.Falsef(t, found, "item %v", item)
assert.False(t, foundTSKey)
}

// delete all: only ts4 should be found, as it's the latest and has an index
k.DeleteStateInfoUntilTimestamp(ctx, ts4.Add(time.Second))

info3 := items[2]
_, found := k.GetStateInfo(ctx,
info3.StateInfoIndex.RollappId,
info3.StateInfoIndex.Index,
)
assert.False(t, found)

info4 := items[3]
_, found = k.GetStateInfo(ctx,
info4.StateInfoIndex.RollappId,
info4.StateInfoIndex.Index,
)
assert.True(t, found)
}
1 change: 1 addition & 0 deletions x/rollapp/types/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const (
AttributeKeyNumBlocks = "num_blocks"
AttributeKeyDAPath = "da_path"
AttributeKeyStatus = "status"
AttributeKeyCreatedAt = "created_at"

// EventTypeFraud is emitted when a fraud evidence is submitted
EventTypeFraud = "fraud_proposal"
Expand Down
Loading
Loading