From 9335fb154c6383f9416279ab6145f0015c260952 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Mon, 2 Feb 2026 14:52:34 +0100 Subject: [PATCH 1/5] prunning --- pkg/adapter/adapter.go | 13 ++++++++++ pkg/store/store.go | 56 +++++++++++++++++++++++++++++++++++++++++ pkg/store/store_test.go | 53 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+) diff --git a/pkg/adapter/adapter.go b/pkg/adapter/adapter.go index 8127774a..25431c6c 100644 --- a/pkg/adapter/adapter.go +++ b/pkg/adapter/adapter.go @@ -90,6 +90,19 @@ type Adapter struct { stackedEvents []StackedEvent } +// PruneExecMeta implements execution.ExecMetaPruner for the ABCI adapter by +// delegating to the underlying ev-abci exec store. It prunes per-height ABCI +// execution metadata (block IDs and block responses) up to the given height. +// The method is safe to call multiple times with the same or increasing +// heights. +func (a *Adapter) PruneExecMeta(ctx context.Context, height uint64) error { + if a.Store == nil { + return nil + } + + return a.Store.Prune(ctx, height) +} + // NewABCIExecutor creates a new Adapter instance that implements the go-execution.Executor interface. // The Adapter wraps the provided ABCI application and delegates execution-related operations to it. func NewABCIExecutor( diff --git a/pkg/store/store.go b/pkg/store/store.go index ebd05dcc..381ee33d 100644 --- a/pkg/store/store.go +++ b/pkg/store/store.go @@ -2,6 +2,7 @@ package store import ( "context" + "errors" "fmt" "strconv" @@ -24,6 +25,10 @@ const ( blockResponseKey = "br" // blockIDKey is the key used for storing block IDs blockIDKey = "bid" + // lastPrunedHeightKey tracks the highest height that has been pruned. + // This makes pruning idempotent and allows incremental pruning across + // multiple calls. + lastPrunedHeightKey = "lph" ) // Store wraps a datastore with ABCI-specific functionality @@ -147,3 +152,54 @@ func (s *Store) GetBlockResponse(ctx context.Context, height uint64) (*abci.Resp return resp, nil } + +// Prune deletes per-height ABCI execution metadata (block IDs and block +// responses) for all heights up to and including the provided target +// height. The current ABCI state (stored under stateKey) is never pruned, +// as it is maintained separately by the application. +// +// Pruning is idempotent: the store tracks the highest pruned height and +// will skip work for already-pruned ranges. +func (s *Store) Prune(ctx context.Context, height uint64) error { + // Load the last pruned height, if any. + data, err := s.prefixedStore.Get(ctx, ds.NewKey(lastPrunedHeightKey)) + if err != nil { + if !errors.Is(err, ds.ErrNotFound) { + return fmt.Errorf("failed to get last pruned height: %w", err) + } + } + + var lastPruned uint64 + if len(data) > 0 { + lastPruned, err = strconv.ParseUint(string(data), 10, 64) + if err != nil { + return fmt.Errorf("invalid last pruned height value %q: %w", string(data), err) + } + } + + // Nothing to do if we've already pruned up to at least this height. + if height <= lastPruned { + return nil + } + + // Delete per-height ABCI metadata (block IDs and block responses) for + // heights in (lastPruned, height]. Missing keys are ignored. + for h := lastPruned + 1; h <= height; h++ { + bidKey := ds.NewKey(blockIDKey).ChildString(strconv.FormatUint(h, 10)) + if err := s.prefixedStore.Delete(ctx, bidKey); err != nil && !errors.Is(err, ds.ErrNotFound) { + return fmt.Errorf("failed to delete block ID at height %d during pruning: %w", h, err) + } + + brKey := ds.NewKey(blockResponseKey).ChildString(strconv.FormatUint(h, 10)) + if err := s.prefixedStore.Delete(ctx, brKey); err != nil && !errors.Is(err, ds.ErrNotFound) { + return fmt.Errorf("failed to delete block response at height %d during pruning: %w", h, err) + } + } + + // Persist the updated last pruned height. + if err := s.prefixedStore.Put(ctx, ds.NewKey(lastPrunedHeightKey), []byte(strconv.FormatUint(height, 10))); err != nil { + return fmt.Errorf("failed to update last pruned height: %w", err) + } + + return nil +} diff --git a/pkg/store/store_test.go b/pkg/store/store_test.go index 0567383c..2b8a64f3 100644 --- a/pkg/store/store_test.go +++ b/pkg/store/store_test.go @@ -22,3 +22,56 @@ func TestStateIO(t *testing.T) { require.NoError(t, gotErr) assert.True(t, exists) } + +func TestPrune_RemovesPerHeightABCIKeysUpToTarget(t *testing.T) { + db := ds.NewMapDatastore() + absciStore := store.NewExecABCIStore(db) + + ctx := t.Context() + + // Seed per-height block ID and block response keys for heights 1..5. + for h := 1; h <= 5; h++ { + heightKey := ds.NewKey("/abci/bid").ChildString(string(rune(h + '0'))) + require.NoError(t, db.Put(ctx, heightKey, []byte("bid"))) + + respKey := ds.NewKey("/abci/br").ChildString(string(rune(h + '0'))) + require.NoError(t, db.Put(ctx, respKey, []byte("br"))) + } + + // Seed state to ensure it is not affected by pruning. + require.NoError(t, db.Put(ctx, ds.NewKey("/abci/s"), []byte("state"))) + + // Prune up to height 3. + require.NoError(t, absciStore.Prune(ctx, 3)) + + // Heights 1..3 should be deleted. + for h := 1; h <= 3; h++ { + bidKey := ds.NewKey("/abci/bid").ChildString(string(rune(h + '0'))) + exists, err := db.Has(ctx, bidKey) + require.NoError(t, err) + assert.False(t, exists) + + respKey := ds.NewKey("/abci/br").ChildString(string(rune(h + '0'))) + exists, err = db.Has(ctx, respKey) + require.NoError(t, err) + assert.False(t, exists) + } + + // Heights 4..5 should remain. + for h := 4; h <= 5; h++ { + bidKey := ds.NewKey("/abci/bid").ChildString(string(rune(h + '0'))) + exists, err := db.Has(ctx, bidKey) + require.NoError(t, err) + assert.True(t, exists) + + respKey := ds.NewKey("/abci/br").ChildString(string(rune(h + '0'))) + exists, err = db.Has(ctx, respKey) + require.NoError(t, err) + assert.True(t, exists) + } + + // State should not be pruned. + exists, err := db.Has(ctx, ds.NewKey("/abci/s")) + require.NoError(t, err) + assert.True(t, exists) +} From 9e7777d37af2415b1630ca29a8cbc0b281d9a814 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Mon, 2 Feb 2026 20:16:13 +0100 Subject: [PATCH 2/5] changed to batch delete --- pkg/store/store.go | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/pkg/store/store.go b/pkg/store/store.go index 381ee33d..3c713c59 100644 --- a/pkg/store/store.go +++ b/pkg/store/store.go @@ -182,23 +182,37 @@ func (s *Store) Prune(ctx context.Context, height uint64) error { return nil } + // Use a batch to atomically delete all per-height ABCI metadata and + // update the last pruned height in a single transaction. This avoids + // leaving the store in a partially pruned state if an error occurs + // midway through the operation. + batch, err := s.prefixedStore.Batch(ctx) + if err != nil { + return fmt.Errorf("failed to create batch for pruning: %w", err) + } + // Delete per-height ABCI metadata (block IDs and block responses) for // heights in (lastPruned, height]. Missing keys are ignored. for h := lastPruned + 1; h <= height; h++ { - bidKey := ds.NewKey(blockIDKey).ChildString(strconv.FormatUint(h, 10)) - if err := s.prefixedStore.Delete(ctx, bidKey); err != nil && !errors.Is(err, ds.ErrNotFound) { - return fmt.Errorf("failed to delete block ID at height %d during pruning: %w", h, err) + hStr := strconv.FormatUint(h, 10) + bidKey := ds.NewKey(blockIDKey).ChildString(hStr) + if err := batch.Delete(ctx, bidKey); err != nil && !errors.Is(err, ds.ErrNotFound) { + return fmt.Errorf("failed to add block ID deletion to batch at height %d: %w", h, err) } - brKey := ds.NewKey(blockResponseKey).ChildString(strconv.FormatUint(h, 10)) - if err := s.prefixedStore.Delete(ctx, brKey); err != nil && !errors.Is(err, ds.ErrNotFound) { - return fmt.Errorf("failed to delete block response at height %d during pruning: %w", h, err) + brKey := ds.NewKey(blockResponseKey).ChildString(hStr) + if err := batch.Delete(ctx, brKey); err != nil && !errors.Is(err, ds.ErrNotFound) { + return fmt.Errorf("failed to add block response deletion to batch at height %d: %w", h, err) } } - // Persist the updated last pruned height. - if err := s.prefixedStore.Put(ctx, ds.NewKey(lastPrunedHeightKey), []byte(strconv.FormatUint(height, 10))); err != nil { - return fmt.Errorf("failed to update last pruned height: %w", err) + // Persist the updated last pruned height in the same batch. + if err := batch.Put(ctx, ds.NewKey(lastPrunedHeightKey), []byte(strconv.FormatUint(height, 10))); err != nil { + return fmt.Errorf("failed to add last pruned height update to batch: %w", err) + } + + if err := batch.Commit(ctx); err != nil { + return fmt.Errorf("failed to commit pruning batch: %w", err) } return nil From 4d7ceb519a9a7dcaf3a56e21115739a2fa59abf8 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 12:56:43 +0100 Subject: [PATCH 3/5] Map cosmos-sdk pruning config to ev-node format and remove sonic dependency (#345) * Initial plan * Implement pruning config mapping and remove sonic dependency - Rename clearConflictingViperKeys to mapCosmosPruningToEvNode - Map cosmos-sdk pruning keys to ev-node format with proper value conversion - Add comprehensive tests for pruning mapping function - Remove sonic installation from all Dockerfiles Co-authored-by: julienrbrt <29894366+julienrbrt@users.noreply.github.com> * Fix comment clarity in mapCosmosPruningToEvNode Co-authored-by: julienrbrt <29894366+julienrbrt@users.noreply.github.com> * Simplify logic and add test for override behavior - Simplify switch statement logic in mapCosmosPruningToEvNode - Add test case for verifying cosmos settings override existing evnode settings Co-authored-by: julienrbrt <29894366+julienrbrt@users.noreply.github.com> * Remove redundant empty string case in switch statement Co-authored-by: julienrbrt <29894366+julienrbrt@users.noreply.github.com> * Address PR review feedback on pruning mapping - Change unknown pruning values to default to "disabled" instead of "all" - Convert cosmos-sdk pruning-interval from blocks to time duration - Add evnode.node.block_time to tests and use DefaultConfig for realistic testing - Update all test expectations to match the block-to-duration conversion Co-authored-by: julienrbrt <29894366+julienrbrt@users.noreply.github.com> * Address PR review: use SDK flag constants and map unknown to metadata - Change default case to map unknown pruning values to "metadata" instead of "disabled" - Use cosmos-sdk server flag constants (FlagPruning, FlagPruningKeepRecent, FlagPruningInterval) instead of hardcoded strings - Update imports across server package to use qualified sdkserver import to avoid name collision - Update all tests to use the SDK flag constants - Update test expectations for unknown value mapping to metadata Co-authored-by: julienrbrt <29894366+julienrbrt@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: julienrbrt <29894366+julienrbrt@users.noreply.github.com> --- Dockerfile | 1 - Dockerfile.cosmos-sdk | 3 +- server/post_tx_cmd.go | 4 +- server/rollback_cmd.go | 6 +- server/start.go | 96 +++++++++++----- server/start_test.go | 149 +++++++++++++++++++++++++ tests/integration/docker/Dockerfile.gm | 1 - 7 files changed, 226 insertions(+), 34 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3e8361d9..b0f7ca82 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,6 @@ RUN ignite app install github.com/ignite/apps/evolve@${IGNITE_EVOLVE_APP_VERSION RUN go mod edit -replace github.com/evstack/ev-node=github.com/evstack/ev-node@${EVNODE_VERSION} && \ go mod edit -replace github.com/evstack/ev-abci=/workspace/ev-abci && \ - go get github.com/bytedance/sonic@v1.15.0 && \ go mod tidy && \ go mod download diff --git a/Dockerfile.cosmos-sdk b/Dockerfile.cosmos-sdk index e5898799..165538d2 100644 --- a/Dockerfile.cosmos-sdk +++ b/Dockerfile.cosmos-sdk @@ -28,8 +28,7 @@ RUN ignite evolve add-migrate # webtransport-go v0.9.0 (pulled in transitively via ev-abci) was built against # quic-go v0.53 but MVS selects v0.57+, breaking compilation. Bump the trio to # mutually-compatible versions before building. -RUN go get github.com/quic-go/webtransport-go@v0.10.0 github.com/libp2p/go-libp2p@v0.47.0 && \ - go get github.com/bytedance/sonic@v1.15.0 && go mod tidy +RUN go get github.com/quic-go/webtransport-go@v0.10.0 github.com/libp2p/go-libp2p@v0.47.0 && go mod tidy # TODO: replace this with proper ignite flag to skip IBC registration when available # Patch out IBC registration (comment out the call and its error handling) diff --git a/server/post_tx_cmd.go b/server/post_tx_cmd.go index 35d672b7..b63d3165 100644 --- a/server/post_tx_cmd.go +++ b/server/post_tx_cmd.go @@ -6,7 +6,7 @@ import ( "os" "github.com/cosmos/cosmos-sdk/client" - "github.com/cosmos/cosmos-sdk/server" + sdkserver "github.com/cosmos/cosmos-sdk/server" "github.com/rs/zerolog" "github.com/spf13/cobra" @@ -63,7 +63,7 @@ Examples: // postTxRunE executes the post-tx command func postTxRunE(cobraCmd *cobra.Command, args []string) error { clientCtx := client.GetClientContextFromCmd(cobraCmd) - serverCtx := server.GetServerContextFromCmd(cobraCmd) + serverCtx := sdkserver.GetServerContextFromCmd(cobraCmd) txInput := args[0] if txInput == "" { diff --git a/server/rollback_cmd.go b/server/rollback_cmd.go index a8ae5798..6240133a 100644 --- a/server/rollback_cmd.go +++ b/server/rollback_cmd.go @@ -9,7 +9,7 @@ import ( goheaderstore "github.com/celestiaorg/go-header/store" dbm "github.com/cosmos/cosmos-db" "github.com/cosmos/cosmos-sdk/client/flags" - "github.com/cosmos/cosmos-sdk/server" + sdkserver "github.com/cosmos/cosmos-sdk/server" "github.com/cosmos/cosmos-sdk/server/types" "github.com/spf13/cobra" @@ -39,7 +39,7 @@ Rollback overwrites a state at height n with the state at height n - 1. The application also rolls back to height n - 1. If a --height flag is specified, the rollback will be performed to that height. No rollback can be performed if the height has been committed to the DA layer. `, RunE: func(cmd *cobra.Command, args []string) error { - ctx := server.GetServerContextFromCmd(cmd) + ctx := sdkserver.GetServerContextFromCmd(cmd) cfg := ctx.Config home := cfg.RootDir @@ -49,7 +49,7 @@ The application also rolls back to height n - 1. If a --height flag is specified } // app db - db, err := openDB(home, server.GetAppDBBackend(ctx.Viper)) + db, err := openDB(home, sdkserver.GetAppDBBackend(ctx.Viper)) if err != nil { return err } diff --git a/server/start.go b/server/start.go index 05281733..4472f0c7 100644 --- a/server/start.go +++ b/server/start.go @@ -7,6 +7,8 @@ import ( "io" "net" "os" + "strconv" + "time" "cosmossdk.io/log" cmtcfg "github.com/cometbft/cometbft/config" @@ -20,7 +22,7 @@ import ( cmttypes "github.com/cometbft/cometbft/types" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" - "github.com/cosmos/cosmos-sdk/server" + sdkserver "github.com/cosmos/cosmos-sdk/server" "github.com/cosmos/cosmos-sdk/server/api" serverconfig "github.com/cosmos/cosmos-sdk/server/config" servergrpc "github.com/cosmos/cosmos-sdk/server/grpc" @@ -67,16 +69,16 @@ const ( // StartCommandHandler is the type that must implement nova to match Cosmos SDK start logic. type StartCommandHandler = func( - svrCtx *server.Context, + svrCtx *sdkserver.Context, clientCtx client.Context, appCreator sdktypes.AppCreator, withCmt bool, - opts server.StartCmdOptions, + opts sdkserver.StartCmdOptions, ) error // StartHandler starts the Rollkit server with the provided application and options. func StartHandler() StartCommandHandler { - return func(svrCtx *server.Context, clientCtx client.Context, appCreator sdktypes.AppCreator, inProcess bool, opts server.StartCmdOptions) error { + return func(svrCtx *sdkserver.Context, clientCtx client.Context, appCreator sdktypes.AppCreator, inProcess bool, opts sdkserver.StartCmdOptions) error { svrCfg, err := getAndValidateConfig(svrCtx) if err != nil { return err @@ -99,14 +101,14 @@ func StartHandler() StartCommandHandler { } } -func startApp(svrCtx *server.Context, appCreator sdktypes.AppCreator, opts server.StartCmdOptions) (app sdktypes.Application, cleanupFn func(), err error) { +func startApp(svrCtx *sdkserver.Context, appCreator sdktypes.AppCreator, opts sdkserver.StartCmdOptions) (app sdktypes.Application, cleanupFn func(), err error) { traceWriter, traceCleanupFn, err := setupTraceWriter(svrCtx) if err != nil { return app, traceCleanupFn, err } home := svrCtx.Config.RootDir - db, err := opts.DBOpener(home, server.GetAppDBBackend(svrCtx.Viper)) + db, err := opts.DBOpener(home, sdkserver.GetAppDBBackend(svrCtx.Viper)) if err != nil { return app, traceCleanupFn, err } @@ -122,8 +124,8 @@ func startApp(svrCtx *server.Context, appCreator sdktypes.AppCreator, opts serve return app, cleanupFn, nil } -func startInProcess(svrCtx *server.Context, svrCfg serverconfig.Config, clientCtx client.Context, app sdktypes.Application, - metrics *telemetry.Metrics, opts server.StartCmdOptions, +func startInProcess(svrCtx *sdkserver.Context, svrCfg serverconfig.Config, clientCtx client.Context, app sdktypes.Application, + metrics *telemetry.Metrics, opts sdkserver.StartCmdOptions, ) error { cmtCfg := svrCtx.Config g, ctx, cancelFn := getCtx(svrCtx) @@ -198,7 +200,7 @@ func startGrpcServer( g *errgroup.Group, config serverconfig.GRPCConfig, clientCtx client.Context, - svrCtx *server.Context, + svrCtx *sdkserver.Context, app sdktypes.Application, ) (*grpc.Server, client.Context, error) { if !config.Enable { @@ -256,7 +258,7 @@ func startAPIServer( g *errgroup.Group, svrCfg serverconfig.Config, clientCtx client.Context, - svrCtx *server.Context, + svrCtx *sdkserver.Context, app sdktypes.Application, home string, grpcSrv *grpc.Server, @@ -292,7 +294,7 @@ func startTelemetry(cfg serverconfig.Config) (*telemetry.Metrics, error) { func setupNodeAndExecutor( ctx context.Context, - srvCtx *server.Context, + srvCtx *sdkserver.Context, cfg *cmtcfg.Config, app sdktypes.Application, ) (rolllkitNode node.Node, executor *adapter.Adapter, cleanupFn func(), err error) { @@ -316,8 +318,8 @@ func setupNodeAndExecutor( nodeKey := &key.NodeKey{PrivKey: signingKey, PubKey: signingKey.GetPublic()} - // Clear cosmos-sdk pruning keys that conflict with ev-node's config. - clearConflictingViperKeys(srvCtx.Viper) + // Map cosmos-sdk pruning keys to ev-node's config format. + mapCosmosPruningToEvNode(srvCtx.Viper) evcfg, err := config.LoadFromViper(srvCtx.Viper) if err != nil { @@ -435,7 +437,7 @@ func setupNodeAndExecutor( opts..., ) - cmtApp := server.NewCometABCIWrapper(app) + cmtApp := sdkserver.NewCometABCIWrapper(app) clientCreator := proxy.NewLocalClientCreator(cmtApp) proxyApp, err := initProxyApp(clientCreator, sdkLogger, proxy.NopMetrics()) @@ -634,7 +636,7 @@ func createAndStartIndexerService( return indexerService, txIndexer, blockIndexer, nil } -func getAndValidateConfig(svrCtx *server.Context) (serverconfig.Config, error) { +func getAndValidateConfig(svrCtx *sdkserver.Context) (serverconfig.Config, error) { config, err := serverconfig.GetConfig(svrCtx.Viper) if err != nil { return config, err @@ -646,11 +648,11 @@ func getAndValidateConfig(svrCtx *server.Context) (serverconfig.Config, error) { return config, nil } -func getCtx(svrCtx *server.Context) (*errgroup.Group, context.Context, context.CancelFunc) { +func getCtx(svrCtx *sdkserver.Context) (*errgroup.Group, context.Context, context.CancelFunc) { ctx, cancelFn := context.WithCancel(context.Background()) g, ctx := errgroup.WithContext(ctx) // listen for quit signals so the calling parent process can gracefully exit - server.ListenForQuitSignals(g /* block */, false, cancelFn, svrCtx.Logger) + sdkserver.ListenForQuitSignals(g /* block */, false, cancelFn, svrCtx.Logger) return g, ctx, cancelFn } @@ -665,7 +667,7 @@ func openTraceWriter(traceWriterFile string) (w io.WriteCloser, err error) { ) } -func setupTraceWriter(svrCtx *server.Context) (traceWriter io.WriteCloser, cleanup func(), err error) { +func setupTraceWriter(svrCtx *sdkserver.Context) (traceWriter io.WriteCloser, cleanup func(), err error) { // clean up the traceWriter when the server is shutting down cleanup = func() {} @@ -724,13 +726,57 @@ func openRawEvolveDB(rootDir string) (ds.Batching, error) { return database, nil } -// clearConflictingViperKeys overrides cosmos-sdk config keys that conflict with -func clearConflictingViperKeys(v *viper.Viper) { - conflictingKeys := []string{"pruning", "pruning-keep-recent", "pruning-interval"} - for _, k := range conflictingKeys { - val := v.Get(k) - if _, isString := val.(string); isString { - v.Set(k, map[string]interface{}{}) +// mapCosmosPruningToEvNode maps cosmos-sdk pruning configuration keys to ev-node format. +// Cosmos SDK uses: "pruning", "pruning-keep-recent", "pruning-interval" +// ev-node expects: "evnode.pruning.pruning_mode", "evnode.pruning.pruning_keep_recent", "evnode.pruning.pruning_interval" +func mapCosmosPruningToEvNode(v *viper.Viper) { + // Get cosmos-sdk pruning configuration using SDK flag constants + cosmosPruning := v.GetString(sdkserver.FlagPruning) + cosmosKeepRecent := v.GetString(sdkserver.FlagPruningKeepRecent) + cosmosInterval := v.GetString(sdkserver.FlagPruningInterval) + + // Map cosmos-sdk pruning mode to ev-node pruning mode + var evnodePruningMode string + switch cosmosPruning { + case "nothing": + evnodePruningMode = "disabled" + case "default", "everything", "custom": + evnodePruningMode = "all" + default: + // Unknown non-empty values default to "metadata", empty values remain empty + if cosmosPruning != "" { + evnodePruningMode = "metadata" + } + } + + // Only set ev-node config if cosmos config was present + if evnodePruningMode != "" { + v.Set("evnode.pruning.pruning_mode", evnodePruningMode) + } + if cosmosKeepRecent != "" { + v.Set("evnode.pruning.pruning_keep_recent", cosmosKeepRecent) + } + if cosmosInterval != "" { + // Convert cosmos-sdk interval (number of blocks) to ev-node interval (time duration) + // Cosmos-sdk interval is in blocks, ev-node interval is in time + // Get block time from ev-node config, default to 1 second if not set + blockTimeStr := v.GetString("evnode.node.block_time") + if blockTimeStr == "" { + blockTimeStr = "1s" + } + + blockTime, err := time.ParseDuration(blockTimeStr) + if err != nil { + // If parsing fails, use default of 1 second + blockTime = time.Second + } + + // Parse cosmos interval as number of blocks + intervalBlocks, err := strconv.ParseUint(cosmosInterval, 10, 64) + if err == nil && intervalBlocks > 0 { + // Calculate duration: blocks * block_time + intervalDuration := time.Duration(intervalBlocks) * blockTime + v.Set("evnode.pruning.pruning_interval", intervalDuration.String()) } } } diff --git a/server/start_test.go b/server/start_test.go index 69bcf214..db635286 100644 --- a/server/start_test.go +++ b/server/start_test.go @@ -5,6 +5,9 @@ import ( "strings" "testing" + sdkserver "github.com/cosmos/cosmos-sdk/server" + serverconfig "github.com/cosmos/cosmos-sdk/server/config" + "github.com/spf13/viper" "github.com/stretchr/testify/require" "github.com/evstack/ev-node/pkg/genesis" @@ -131,3 +134,149 @@ func TestGetJSONTag(t *testing.T) { }) } } + +func TestMapCosmosPruningToEvNode(t *testing.T) { + testCases := []struct { + name string + cosmosPruning string + cosmosKeepRecent string + cosmosInterval string + evnodeBlockTime string + expEvnodePruningMode string + expEvnodeKeepRecent string + expEvnodeInterval string + }{ + { + name: "nothing maps to disabled", + cosmosPruning: "nothing", + cosmosKeepRecent: "0", + cosmosInterval: "0", + evnodeBlockTime: "1s", + expEvnodePruningMode: "disabled", + expEvnodeKeepRecent: "0", + expEvnodeInterval: "", // 0 interval is skipped + }, + { + name: "default maps to all", + cosmosPruning: "default", + cosmosKeepRecent: "362880", + cosmosInterval: "10", + evnodeBlockTime: "6s", + expEvnodePruningMode: "all", + expEvnodeKeepRecent: "362880", + expEvnodeInterval: "1m0s", // 10 blocks * 6s = 60s + }, + { + name: "everything maps to all", + cosmosPruning: "everything", + cosmosKeepRecent: "2", + cosmosInterval: "10", + evnodeBlockTime: "1s", + expEvnodePruningMode: "all", + expEvnodeKeepRecent: "2", + expEvnodeInterval: "10s", // 10 blocks * 1s = 10s + }, + { + name: "custom maps to all", + cosmosPruning: "custom", + cosmosKeepRecent: "100", + cosmosInterval: "5", + evnodeBlockTime: "2s", + expEvnodePruningMode: "all", + expEvnodeKeepRecent: "100", + expEvnodeInterval: "10s", // 5 blocks * 2s = 10s + }, + { + name: "empty values are not mapped", + cosmosPruning: "", + cosmosKeepRecent: "", + cosmosInterval: "", + evnodeBlockTime: "1s", + expEvnodePruningMode: "", + expEvnodeKeepRecent: "", + expEvnodeInterval: "", + }, + { + name: "unknown value maps to metadata", + cosmosPruning: "unknown", + cosmosKeepRecent: "50", + cosmosInterval: "15", + evnodeBlockTime: "1s", + expEvnodePruningMode: "metadata", + expEvnodeKeepRecent: "50", + expEvnodeInterval: "15s", // 15 blocks * 1s = 15s + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + v := viper.New() + v.Set(sdkserver.FlagPruning, tc.cosmosPruning) + v.Set(sdkserver.FlagPruningKeepRecent, tc.cosmosKeepRecent) + v.Set(sdkserver.FlagPruningInterval, tc.cosmosInterval) + v.Set("evnode.node.block_time", tc.evnodeBlockTime) + + mapCosmosPruningToEvNode(v) + + if tc.expEvnodePruningMode != "" { + require.Equal(t, tc.expEvnodePruningMode, v.GetString("evnode.pruning.pruning_mode")) + } else { + require.Empty(t, v.GetString("evnode.pruning.pruning_mode")) + } + + if tc.expEvnodeKeepRecent != "" { + require.Equal(t, tc.expEvnodeKeepRecent, v.GetString("evnode.pruning.pruning_keep_recent")) + } else { + require.Empty(t, v.GetString("evnode.pruning.pruning_keep_recent")) + } + + if tc.expEvnodeInterval != "" { + require.Equal(t, tc.expEvnodeInterval, v.GetString("evnode.pruning.pruning_interval")) + } else { + require.Empty(t, v.GetString("evnode.pruning.pruning_interval")) + } + }) + } +} + +func TestMapCosmosPruningToEvNode_WithDefaultConfig(t *testing.T) { + // Test using cosmos-sdk's DefaultConfig to ensure realistic integration + cosmosConfig := serverconfig.DefaultConfig() + + v := viper.New() + v.Set(sdkserver.FlagPruning, cosmosConfig.Pruning) + v.Set(sdkserver.FlagPruningKeepRecent, cosmosConfig.PruningKeepRecent) + v.Set(sdkserver.FlagPruningInterval, cosmosConfig.PruningInterval) + v.Set("evnode.node.block_time", "6s") + + mapCosmosPruningToEvNode(v) + + // DefaultConfig has pruning="default", which should map to "all" + require.Equal(t, "all", v.GetString("evnode.pruning.pruning_mode")) + require.Equal(t, cosmosConfig.PruningKeepRecent, v.GetString("evnode.pruning.pruning_keep_recent")) + // DefaultConfig has interval="0", which should not set a value + require.Empty(t, v.GetString("evnode.pruning.pruning_interval")) +} + +func TestMapCosmosPruningToEvNode_WithExistingEvnodeSettings(t *testing.T) { + // Test that cosmos settings override existing evnode settings + v := viper.New() + + // Set existing evnode settings + v.Set("evnode.pruning.pruning_mode", "metadata") + v.Set("evnode.pruning.pruning_keep_recent", "999") + v.Set("evnode.pruning.pruning_interval", "60m") + + // Set cosmos settings and block time using SDK flag constants + v.Set(sdkserver.FlagPruning, "default") + v.Set(sdkserver.FlagPruningKeepRecent, "100") + v.Set(sdkserver.FlagPruningInterval, "10") + v.Set("evnode.node.block_time", "1s") + + mapCosmosPruningToEvNode(v) + + // Verify cosmos settings were mapped and override existing evnode settings + require.Equal(t, "all", v.GetString("evnode.pruning.pruning_mode")) + require.Equal(t, "100", v.GetString("evnode.pruning.pruning_keep_recent")) + require.Equal(t, "10s", v.GetString("evnode.pruning.pruning_interval")) // 10 blocks * 1s = 10s +} diff --git a/tests/integration/docker/Dockerfile.gm b/tests/integration/docker/Dockerfile.gm index 5d029fd4..b49100f4 100644 --- a/tests/integration/docker/Dockerfile.gm +++ b/tests/integration/docker/Dockerfile.gm @@ -34,7 +34,6 @@ RUN chmod +x /workspace/patch-app-wiring.sh && \ # Align module versions like in CI RUN go mod edit -replace github.com/evstack/ev-node=github.com/evstack/ev-node@${EVNODE_VERSION} \ && go mod edit -replace github.com/evstack/ev-abci=../ev-abci \ - && go get github.com/bytedance/sonic@v1.15.0 \ && go mod tidy # Build gmd binary From a1b328e5d2d31bff56440278d7be3890eac26374 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Tue, 17 Feb 2026 17:18:44 +0100 Subject: [PATCH 4/5] fixes --- pkg/adapter/adapter.go | 13 ++-- pkg/store/store.go | 4 +- server/start.go | 31 ++++----- server/start_test.go | 145 +++++++++++++++++------------------------ 4 files changed, 84 insertions(+), 109 deletions(-) diff --git a/pkg/adapter/adapter.go b/pkg/adapter/adapter.go index aed46f6a..2ff0f9ae 100644 --- a/pkg/adapter/adapter.go +++ b/pkg/adapter/adapter.go @@ -33,7 +33,10 @@ import ( execstore "github.com/evstack/ev-abci/pkg/store" ) -var _ execution.Executor = &Adapter{} +var ( + _ execution.Executor = &Adapter{} + _ execution.ExecPruner = &Adapter{} +) type P2PClientInfo interface { Info() (string, string, string, error) @@ -88,12 +91,8 @@ type Adapter struct { stackedEvents []StackedEvent } -// PruneExecMeta implements execution.ExecMetaPruner for the ABCI adapter by -// delegating to the underlying ev-abci exec store. It prunes per-height ABCI -// execution metadata (block IDs and block responses) up to the given height. -// The method is safe to call multiple times with the same or increasing -// heights. -func (a *Adapter) PruneExecMeta(ctx context.Context, height uint64) error { +// PruneExec implements execution.ExecMetaPruner. +func (a *Adapter) PruneExec(ctx context.Context, height uint64) error { if a.Store == nil { return nil } diff --git a/pkg/store/store.go b/pkg/store/store.go index 3c713c59..f9b6f5a3 100644 --- a/pkg/store/store.go +++ b/pkg/store/store.go @@ -26,9 +26,7 @@ const ( // blockIDKey is the key used for storing block IDs blockIDKey = "bid" // lastPrunedHeightKey tracks the highest height that has been pruned. - // This makes pruning idempotent and allows incremental pruning across - // multiple calls. - lastPrunedHeightKey = "lph" + lastPrunedHeightKey = "lphk" ) // Store wraps a datastore with ABCI-specific functionality diff --git a/server/start.go b/server/start.go index 4472f0c7..038dd3c2 100644 --- a/server/start.go +++ b/server/start.go @@ -738,45 +738,46 @@ func mapCosmosPruningToEvNode(v *viper.Viper) { // Map cosmos-sdk pruning mode to ev-node pruning mode var evnodePruningMode string switch cosmosPruning { - case "nothing": - evnodePruningMode = "disabled" - case "default", "everything", "custom": - evnodePruningMode = "all" + case "default", "nothing": + evnodePruningMode = config.PruningModeDisabled + case "everything": + evnodePruningMode = config.PruningModeAll + case "custom": + evnodePruningMode = config.PruningModeMetadata default: - // Unknown non-empty values default to "metadata", empty values remain empty if cosmosPruning != "" { - evnodePruningMode = "metadata" + evnodePruningMode = config.PruningModeDisabled } } // Only set ev-node config if cosmos config was present - if evnodePruningMode != "" { - v.Set("evnode.pruning.pruning_mode", evnodePruningMode) + if evnodePruningMode != "" && v.GetString(config.FlagPruningMode) == "" { + v.Set(config.FlagPruningMode, evnodePruningMode) } - if cosmosKeepRecent != "" { - v.Set("evnode.pruning.pruning_keep_recent", cosmosKeepRecent) + if cosmosKeepRecent != "" && v.GetString(config.FlagPruningKeepRecent) == "" { + v.Set(config.FlagPruningKeepRecent, cosmosKeepRecent) } - if cosmosInterval != "" { + if cosmosInterval != "" && v.GetString(config.FlagPruningInterval) == "" { // Convert cosmos-sdk interval (number of blocks) to ev-node interval (time duration) // Cosmos-sdk interval is in blocks, ev-node interval is in time // Get block time from ev-node config, default to 1 second if not set - blockTimeStr := v.GetString("evnode.node.block_time") + blockTimeStr := v.GetString(config.FlagBlockTime) if blockTimeStr == "" { blockTimeStr = "1s" } - + blockTime, err := time.ParseDuration(blockTimeStr) if err != nil { // If parsing fails, use default of 1 second blockTime = time.Second } - + // Parse cosmos interval as number of blocks intervalBlocks, err := strconv.ParseUint(cosmosInterval, 10, 64) if err == nil && intervalBlocks > 0 { // Calculate duration: blocks * block_time intervalDuration := time.Duration(intervalBlocks) * blockTime - v.Set("evnode.pruning.pruning_interval", intervalDuration.String()) + v.Set(config.FlagPruningInterval, intervalDuration.String()) } } } diff --git a/server/start_test.go b/server/start_test.go index db635286..01e83599 100644 --- a/server/start_test.go +++ b/server/start_test.go @@ -137,74 +137,74 @@ func TestGetJSONTag(t *testing.T) { func TestMapCosmosPruningToEvNode(t *testing.T) { testCases := []struct { - name string - cosmosPruning string - cosmosKeepRecent string - cosmosInterval string - evnodeBlockTime string - expEvnodePruningMode string - expEvnodeKeepRecent string - expEvnodeInterval string + name string + cosmosPruning string + cosmosKeepRecent string + cosmosInterval string + evnodeBlockTime string + expEvnodePruningMode string + expEvnodeKeepRecent string + expEvnodeInterval string }{ { - name: "nothing maps to disabled", - cosmosPruning: "nothing", - cosmosKeepRecent: "0", - cosmosInterval: "0", - evnodeBlockTime: "1s", - expEvnodePruningMode: "disabled", - expEvnodeKeepRecent: "0", - expEvnodeInterval: "", // 0 interval is skipped + name: "nothing maps to disabled", + cosmosPruning: "nothing", + cosmosKeepRecent: "0", + cosmosInterval: "0", + evnodeBlockTime: "1s", + expEvnodePruningMode: "disabled", + expEvnodeKeepRecent: "0", + expEvnodeInterval: "", // 0 interval is skipped }, { - name: "default maps to all", - cosmosPruning: "default", - cosmosKeepRecent: "362880", - cosmosInterval: "10", - evnodeBlockTime: "6s", - expEvnodePruningMode: "all", - expEvnodeKeepRecent: "362880", - expEvnodeInterval: "1m0s", // 10 blocks * 6s = 60s + name: "default maps to disabled", + cosmosPruning: "default", + cosmosKeepRecent: "362880", + cosmosInterval: "10", + evnodeBlockTime: "6s", + expEvnodePruningMode: "disabled", + expEvnodeKeepRecent: "362880", + expEvnodeInterval: "1m0s", // 10 blocks * 6s = 60s }, { - name: "everything maps to all", - cosmosPruning: "everything", - cosmosKeepRecent: "2", - cosmosInterval: "10", - evnodeBlockTime: "1s", - expEvnodePruningMode: "all", - expEvnodeKeepRecent: "2", - expEvnodeInterval: "10s", // 10 blocks * 1s = 10s + name: "everything maps to all", + cosmosPruning: "everything", + cosmosKeepRecent: "2", + cosmosInterval: "10", + evnodeBlockTime: "1s", + expEvnodePruningMode: "all", + expEvnodeKeepRecent: "2", + expEvnodeInterval: "10s", // 10 blocks * 1s = 10s }, { - name: "custom maps to all", - cosmosPruning: "custom", - cosmosKeepRecent: "100", - cosmosInterval: "5", - evnodeBlockTime: "2s", - expEvnodePruningMode: "all", - expEvnodeKeepRecent: "100", - expEvnodeInterval: "10s", // 5 blocks * 2s = 10s + name: "custom maps to metadata", + cosmosPruning: "custom", + cosmosKeepRecent: "100", + cosmosInterval: "5", + evnodeBlockTime: "2s", + expEvnodePruningMode: "metadata", + expEvnodeKeepRecent: "100", + expEvnodeInterval: "10s", // 5 blocks * 2s = 10s }, { - name: "empty values are not mapped", - cosmosPruning: "", - cosmosKeepRecent: "", - cosmosInterval: "", - evnodeBlockTime: "1s", - expEvnodePruningMode: "", - expEvnodeKeepRecent: "", - expEvnodeInterval: "", + name: "empty values are not mapped", + cosmosPruning: "", + cosmosKeepRecent: "", + cosmosInterval: "", + evnodeBlockTime: "1s", + expEvnodePruningMode: "", + expEvnodeKeepRecent: "", + expEvnodeInterval: "", }, { - name: "unknown value maps to metadata", - cosmosPruning: "unknown", - cosmosKeepRecent: "50", - cosmosInterval: "15", - evnodeBlockTime: "1s", - expEvnodePruningMode: "metadata", - expEvnodeKeepRecent: "50", - expEvnodeInterval: "15s", // 15 blocks * 1s = 15s + name: "unknown value maps to disabled", + cosmosPruning: "unknown", + cosmosKeepRecent: "50", + cosmosInterval: "15", + evnodeBlockTime: "1s", + expEvnodePruningMode: "disabled", + expEvnodeKeepRecent: "50", + expEvnodeInterval: "15s", // 15 blocks * 1s = 15s }, } @@ -242,41 +242,18 @@ func TestMapCosmosPruningToEvNode(t *testing.T) { func TestMapCosmosPruningToEvNode_WithDefaultConfig(t *testing.T) { // Test using cosmos-sdk's DefaultConfig to ensure realistic integration cosmosConfig := serverconfig.DefaultConfig() - + v := viper.New() v.Set(sdkserver.FlagPruning, cosmosConfig.Pruning) v.Set(sdkserver.FlagPruningKeepRecent, cosmosConfig.PruningKeepRecent) v.Set(sdkserver.FlagPruningInterval, cosmosConfig.PruningInterval) v.Set("evnode.node.block_time", "6s") - + mapCosmosPruningToEvNode(v) - - // DefaultConfig has pruning="default", which should map to "all" - require.Equal(t, "all", v.GetString("evnode.pruning.pruning_mode")) + + // DefaultConfig has pruning="default", which should map to "disabled" + require.Equal(t, "disabled", v.GetString("evnode.pruning.pruning_mode")) require.Equal(t, cosmosConfig.PruningKeepRecent, v.GetString("evnode.pruning.pruning_keep_recent")) // DefaultConfig has interval="0", which should not set a value require.Empty(t, v.GetString("evnode.pruning.pruning_interval")) } - -func TestMapCosmosPruningToEvNode_WithExistingEvnodeSettings(t *testing.T) { - // Test that cosmos settings override existing evnode settings - v := viper.New() - - // Set existing evnode settings - v.Set("evnode.pruning.pruning_mode", "metadata") - v.Set("evnode.pruning.pruning_keep_recent", "999") - v.Set("evnode.pruning.pruning_interval", "60m") - - // Set cosmos settings and block time using SDK flag constants - v.Set(sdkserver.FlagPruning, "default") - v.Set(sdkserver.FlagPruningKeepRecent, "100") - v.Set(sdkserver.FlagPruningInterval, "10") - v.Set("evnode.node.block_time", "1s") - - mapCosmosPruningToEvNode(v) - - // Verify cosmos settings were mapped and override existing evnode settings - require.Equal(t, "all", v.GetString("evnode.pruning.pruning_mode")) - require.Equal(t, "100", v.GetString("evnode.pruning.pruning_keep_recent")) - require.Equal(t, "10s", v.GetString("evnode.pruning.pruning_interval")) // 10 blocks * 1s = 10s -} From 27770af6b88c225e10ea720fbaee305342eaefb3 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Tue, 17 Feb 2026 17:41:08 +0100 Subject: [PATCH 5/5] updates --- server/start.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/start.go b/server/start.go index 038dd3c2..5365756a 100644 --- a/server/start.go +++ b/server/start.go @@ -735,6 +735,11 @@ func mapCosmosPruningToEvNode(v *viper.Viper) { cosmosKeepRecent := v.GetString(sdkserver.FlagPruningKeepRecent) cosmosInterval := v.GetString(sdkserver.FlagPruningInterval) + // unset keys + for _, k := range []string{sdkserver.FlagPruning, sdkserver.FlagPruningKeepRecent, sdkserver.FlagPruningInterval} { + v.Set(k, map[string]interface{}{}) + } + // Map cosmos-sdk pruning mode to ev-node pruning mode var evnodePruningMode string switch cosmosPruning {