Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5509d38
basic prunning
pthmas Jan 16, 2026
95a3876
prune metadata from ev-node store
pthmas Jan 23, 2026
b8ca7fb
wiring prunning config to go-header
pthmas Jan 23, 2026
d09b803
prune evm exec store
pthmas Jan 26, 2026
c329f76
add parameters validation
pthmas Jan 26, 2026
9ae2f6a
make error handling consistent
pthmas Jan 26, 2026
907f28c
add method to tracedstore to respect interface
pthmas Jan 27, 2026
b022f10
add prune block to mockstore
pthmas Jan 27, 2026
99d586b
fix helper for flag for consistency
pthmas Jan 28, 2026
f798b18
add replace statement for local packages
pthmas Jan 28, 2026
70e4901
flags
pthmas Jan 28, 2026
9315c6c
add safetey mechanism for pruning only da included blocks
pthmas Feb 2, 2026
23af6b7
fix rebase
pthmas Feb 2, 2026
f250b64
remove useless check
pthmas Feb 3, 2026
135c925
use lastprunedheight in Tail() to optimize
pthmas Feb 3, 2026
c1168c9
move pruning from executor to dainclusionloop
pthmas Feb 3, 2026
155ed5c
don't prune go-header store
pthmas Feb 3, 2026
52c005e
update tail function
pthmas Feb 4, 2026
9e046c4
rename execmetapruner to execpruner
pthmas Feb 4, 2026
3c1641b
Update core/execution/execution.go
pthmas Feb 4, 2026
2566dae
Update execution/evm/execution.go
pthmas Feb 4, 2026
535cf1c
trigger pruning every ticker
pthmas Feb 4, 2026
4ee40d6
add store to store adapter
pthmas Feb 4, 2026
a4434d1
invalidate cache store data after pruning
pthmas Feb 4, 2026
86eb1a2
nit
pthmas Feb 4, 2026
ca26acd
move pruning to it's own function
pthmas Feb 10, 2026
b1a6a09
update config.md file
pthmas Feb 10, 2026
fec4fb4
remove ai comments
pthmas Feb 10, 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
1 change: 1 addition & 0 deletions apps/evm/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.25.6

replace (
github.com/evstack/ev-node => ../../
github.com/evstack/ev-node/core => ../../core
github.com/evstack/ev-node/execution/evm => ../../execution/evm
)

Expand Down
2 changes: 0 additions & 2 deletions apps/evm/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -411,8 +411,6 @@ github.com/ethereum/go-ethereum v1.16.8 h1:LLLfkZWijhR5m6yrAXbdlTeXoqontH+Ga2f9i
github.com/ethereum/go-ethereum v1.16.8/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk=
github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8=
github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
github.com/evstack/ev-node/core v1.0.0-rc.1 h1:Dic2PMUMAYUl5JW6DkDj6HXDEWYzorVJQuuUJOV0FjE=
github.com/evstack/ev-node/core v1.0.0-rc.1/go.mod h1:n2w/LhYQTPsi48m6lMj16YiIqsaQw6gxwjyJvR+B3sY=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
Expand Down
1 change: 1 addition & 0 deletions apps/grpc/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.25.6

replace (
github.com/evstack/ev-node => ../../
github.com/evstack/ev-node/core => ../../core
github.com/evstack/ev-node/execution/grpc => ../../execution/grpc
)

Expand Down
2 changes: 0 additions & 2 deletions apps/grpc/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -367,8 +367,6 @@ github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6Ni
github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
github.com/envoyproxy/protoc-gen-validate v1.0.1/go.mod h1:0vj8bNkYbSTNS2PIyH87KZaeN4x9zpL9Qt8fQC7d+vs=
github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE=
github.com/evstack/ev-node/core v1.0.0-rc.1 h1:Dic2PMUMAYUl5JW6DkDj6HXDEWYzorVJQuuUJOV0FjE=
github.com/evstack/ev-node/core v1.0.0-rc.1/go.mod h1:n2w/LhYQTPsi48m6lMj16YiIqsaQw6gxwjyJvR+B3sY=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
Expand Down
5 changes: 4 additions & 1 deletion apps/testapp/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ module github.com/evstack/ev-node/apps/testapp

go 1.25.6

replace github.com/evstack/ev-node => ../../
replace (
github.com/evstack/ev-node => ../../.
github.com/evstack/ev-node/core => ../../core
)

require (
github.com/evstack/ev-node v1.0.0-rc.3
Expand Down
2 changes: 0 additions & 2 deletions apps/testapp/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -367,8 +367,6 @@ github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6Ni
github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
github.com/envoyproxy/protoc-gen-validate v1.0.1/go.mod h1:0vj8bNkYbSTNS2PIyH87KZaeN4x9zpL9Qt8fQC7d+vs=
github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE=
github.com/evstack/ev-node/core v1.0.0-rc.1 h1:Dic2PMUMAYUl5JW6DkDj6HXDEWYzorVJQuuUJOV0FjE=
github.com/evstack/ev-node/core v1.0.0-rc.1/go.mod h1:n2w/LhYQTPsi48m6lMj16YiIqsaQw6gxwjyJvR+B3sY=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
Expand Down
44 changes: 44 additions & 0 deletions block/internal/submitting/submitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,50 @@ func (s *Submitter) processDAInclusionLoop() {
// This can only be performed after the height has been persisted to store
s.cache.DeleteHeight(nextHeight)
}

// Run height-based pruning if enabled.
s.pruneBlocks()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems blocking? since we are pruning data that will not be modified we should be able to handle it async. Can we get some benchmarks on what the potential slow down to a node operating at 100ms is going to experience with different settings

Copy link
Member

@julienrbrt julienrbrt Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to extract it, then i suppose we should create a pruning package under internal and have it be a new component. It was to put it in submitting so it would be called by both syncer and executor.
Having it its own component will make it async.

}
}
}

func (s *Submitter) pruneBlocks() {
if !s.config.Node.PruningEnabled || s.config.Node.PruningKeepRecent == 0 || s.config.Node.PruningInterval == 0 {
return
}

currentDAIncluded := s.GetDAIncludedHeight()

var lastPruned uint64
if bz, err := s.store.GetMetadata(s.ctx, store.LastPrunedBlockHeightKey); err == nil && len(bz) == 8 {
lastPruned = binary.LittleEndian.Uint64(bz)
}

storeHeight, err := s.store.Height(s.ctx)
if err != nil {
s.logger.Error().Err(err).Msg("failed to get store height for pruning")
return
}
if storeHeight <= lastPruned+s.config.Node.PruningInterval {
return
}

// Never prune blocks that are not DA included
upperBound := min(storeHeight, currentDAIncluded)
if upperBound <= s.config.Node.PruningKeepRecent {
// Not enough fully included blocks to prune
return
}

targetHeight := upperBound - s.config.Node.PruningKeepRecent

if err := s.store.PruneBlocks(s.ctx, targetHeight); err != nil {
s.logger.Error().Err(err).Uint64("target_height", targetHeight).Msg("failed to prune old block data")
}

if pruner, ok := s.exec.(coreexecutor.ExecPruner); ok {
if err := pruner.PruneExec(s.ctx, targetHeight); err != nil {
s.logger.Error().Err(err).Uint64("target_height", targetHeight).Msg("failed to prune execution metadata")
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions core/execution/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,13 @@ type Rollbackable interface {
// Rollback resets the execution layer head to the specified height.
Rollback(ctx context.Context, targetHeight uint64) error
}

// ExecPruner is an optional interface that execution clients can implement
// to support height-based pruning of their execution metadata.
type ExecPruner interface {
// PruneExec should delete execution metadata for all heights up to and
// including the given height. Implementations should be idempotent and track
// their own progress so that repeated calls with the same or decreasing
// heights are cheap no-ops.
PruneExec(ctx context.Context, height uint64) error
}
35 changes: 35 additions & 0 deletions docs/learn/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ This document provides a comprehensive reference for all configuration options a
- [Maximum Pending Blocks](#maximum-pending-blocks)
- [Lazy Mode (Lazy Aggregator)](#lazy-mode-lazy-aggregator)
- [Lazy Block Interval](#lazy-block-interval)
- [Pruning (Height-Based Pruning)](#pruning-height-based-pruning)
- [Data Availability Configuration (`da`)](#data-availability-configuration-da)
- [DA Service Address](#da-service-address)
- [DA Authentication Token](#da-authentication-token)
Expand Down Expand Up @@ -279,6 +280,40 @@ _Example:_ `--rollkit.node.lazy_block_interval 1m`
_Default:_ `"30s"`
_Constant:_ `FlagLazyBlockTime`

### Pruning (Height-Based Pruning)

**Description:**
Controls height-based pruning of stored block data (headers, data, signatures, and index) from the local store. When pruning is enabled, the node periodically deletes old blocks while keeping a recent window of history. When disabled, the node keeps all blocks (archive mode).

**YAML:**

```yaml
node:
pruning_enabled: true
pruning_keep_recent: 100000
pruning_interval: 1000
```

**Command-line Flags:**

- `--evnode.node.pruning_enabled` (boolean)
- _Description:_ Enable height-based pruning of stored block data. When disabled, all blocks are kept (archive mode).
- `--evnode.node.pruning_keep_recent <uint64>`
- _Description:_ Number of most recent blocks to retain when pruning is enabled. Must be > 0 when pruning is enabled; set `pruning_enabled=false` to keep all blocks.
- `--evnode.node.pruning_interval <uint64>`
- _Description:_ Run pruning every N blocks. Must be >= 1 when pruning is enabled.

_Defaults:_

```yaml
node:
pruning_enabled: false
pruning_keep_recent: 0
pruning_interval: 0
```

_Constants:_ `FlagNodePruningEnabled`, `FlagNodePruningKeepRecent`, `FlagNodePruningInterval`

## Data Availability Configuration (`da`)

Parameters for connecting and interacting with the Data Availability (DA) layer, which Evolve uses to publish block data.
Expand Down
11 changes: 11 additions & 0 deletions execution/evm/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ var _ execution.HeightProvider = (*EngineClient)(nil)
// Ensure EngineClient implements the execution.Rollbackable interface
var _ execution.Rollbackable = (*EngineClient)(nil)

// Ensure EngineClient implements optional pruning interface when used with
// ev-node's height-based pruning.
var _ execution.ExecPruner = (*EngineClient)(nil)

// validatePayloadStatus checks the payload status and returns appropriate errors.
// It implements the Engine API specification's status handling:
// - VALID: Operation succeeded, return nil
Expand Down Expand Up @@ -265,6 +269,13 @@ func NewEngineExecutionClient(
}, nil
}

// PruneExec implements execution.ExecPruner by delegating to the
// underlying EVMStore. It is safe to call this multiple times with the same
// or increasing heights; the store tracks its own last-pruned height.
func (c *EngineClient) PruneExec(ctx context.Context, height uint64) error {
return c.store.PruneExec(ctx, height)
}

// SetLogger allows callers to attach a structured logger.
func (c *EngineClient) SetLogger(l zerolog.Logger) {
c.logger = l
Expand Down
5 changes: 5 additions & 0 deletions execution/evm/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,8 @@ require (
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.4.1 // indirect
)

replace (
github.com/evstack/ev-node => ../../
github.com/evstack/ev-node/core => ../../core
)
4 changes: 0 additions & 4 deletions execution/evm/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,6 @@ github.com/ethereum/go-ethereum v1.16.8 h1:LLLfkZWijhR5m6yrAXbdlTeXoqontH+Ga2f9i
github.com/ethereum/go-ethereum v1.16.8/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk=
github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8=
github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
github.com/evstack/ev-node v1.0.0-rc.3 h1:hphJBI0b1TgGN9wajB1twouMVMjhyHXXrS9QaG1XwvQ=
github.com/evstack/ev-node v1.0.0-rc.3/go.mod h1:5Cf3SauhgIV+seQKBJavv3f8ZZw+YTnH5DRJcI4Ooj0=
github.com/evstack/ev-node/core v1.0.0-rc.1 h1:Dic2PMUMAYUl5JW6DkDj6HXDEWYzorVJQuuUJOV0FjE=
github.com/evstack/ev-node/core v1.0.0-rc.1/go.mod h1:n2w/LhYQTPsi48m6lMj16YiIqsaQw6gxwjyJvR+B3sY=
github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY=
github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg=
github.com/filecoin-project/go-clock v0.1.0 h1:SFbYIM75M8NnFm1yMHhN9Ahy3W5bEZV9gd6MPfXbKVU=
Expand Down
53 changes: 53 additions & 0 deletions execution/evm/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ import (
// Store prefix for execution/evm data - keeps it isolated from other ev-node data
const evmStorePrefix = "evm/"

// lastPrunedExecMetaKey is the datastore key used to track the highest
// execution height for which ExecMeta has been pruned. All ExecMeta entries
// for heights <= this value are considered pruned.
const lastPrunedExecMetaKey = evmStorePrefix + "last-pruned-execmeta-height"

// ExecMeta stages
const (
ExecStageStarted = "started"
Expand Down Expand Up @@ -140,6 +145,54 @@ func (s *EVMStore) SaveExecMeta(ctx context.Context, meta *ExecMeta) error {
return nil
}

// PruneExec removes ExecMeta entries up to and including the given height.
// It is safe to call this multiple times with the same or increasing heights;
// previously pruned ranges will be skipped based on the last-pruned marker.
func (s *EVMStore) PruneExec(ctx context.Context, height uint64) error {
// Load last pruned height, if any.
var lastPruned uint64
data, err := s.db.Get(ctx, ds.NewKey(lastPrunedExecMetaKey))
if err != nil {
if !errors.Is(err, ds.ErrNotFound) {
return fmt.Errorf("failed to get last pruned execmeta height: %w", err)
}
} else if len(data) == 8 {
lastPruned = binary.BigEndian.Uint64(data)
}

// Nothing new to prune.
if height <= lastPruned {
return nil
}

batch, err := s.db.Batch(ctx)
if err != nil {
return fmt.Errorf("failed to create batch for execmeta pruning: %w", err)
}

for h := lastPruned + 1; h <= height; h++ {
key := execMetaKey(h)
if err := batch.Delete(ctx, key); err != nil {
if !errors.Is(err, ds.ErrNotFound) {
return fmt.Errorf("failed to delete exec meta at height %d: %w", h, err)
}
}
}

// Persist updated last pruned height.
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, height)
if err := batch.Put(ctx, ds.NewKey(lastPrunedExecMetaKey), buf); err != nil {
return fmt.Errorf("failed to update last pruned execmeta height: %w", err)
}

if err := batch.Commit(ctx); err != nil {
return fmt.Errorf("failed to commit execmeta pruning batch: %w", err)
}

return nil
}

// Sync ensures all pending writes are flushed to disk.
func (s *EVMStore) Sync(ctx context.Context) error {
return s.db.Sync(ctx, ds.NewKey(evmStorePrefix))
Expand Down
99 changes: 99 additions & 0 deletions execution/evm/store_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package evm

import (
"context"
"encoding/binary"
"testing"

ds "github.com/ipfs/go-datastore"
dssync "github.com/ipfs/go-datastore/sync"
"github.com/stretchr/testify/require"
)

// newTestDatastore creates an in-memory datastore for testing.
func newTestDatastore(t *testing.T) ds.Batching {
t.Helper()
// Wrap the in-memory MapDatastore to satisfy the Batching interface.
return dssync.MutexWrap(ds.NewMapDatastore())
}

func TestPruneExec_PrunesUpToTargetHeight(t *testing.T) {
t.Parallel()

ctx := context.Background()
db := newTestDatastore(t)
store := NewEVMStore(db)

// Seed ExecMeta entries at heights 1..5
for h := uint64(1); h <= 5; h++ {
meta := &ExecMeta{Height: h}
require.NoError(t, store.SaveExecMeta(ctx, meta))
}

// Sanity: all heights should be present
for h := uint64(1); h <= 5; h++ {
meta, err := store.GetExecMeta(ctx, h)
require.NoError(t, err)
require.NotNil(t, meta)
require.Equal(t, h, meta.Height)
}

// Prune up to height 3
require.NoError(t, store.PruneExec(ctx, 3))

// Heights 1..3 should be gone
for h := uint64(1); h <= 3; h++ {
meta, err := store.GetExecMeta(ctx, h)
require.NoError(t, err)
require.Nil(t, meta)
}

// Heights 4..5 should remain
for h := uint64(4); h <= 5; h++ {
meta, err := store.GetExecMeta(ctx, h)
require.NoError(t, err)
require.NotNil(t, meta)
}

// Re-pruning with the same height should be a no-op
require.NoError(t, store.PruneExec(ctx, 3))
}

func TestPruneExec_TracksLastPrunedHeight(t *testing.T) {
t.Parallel()

ctx := context.Background()
db := newTestDatastore(t)
store := NewEVMStore(db)

// Seed ExecMeta entries at heights 1..5
for h := uint64(1); h <= 5; h++ {
meta := &ExecMeta{Height: h}
require.NoError(t, store.SaveExecMeta(ctx, meta))
}

// First prune up to 2
require.NoError(t, store.PruneExec(ctx, 2))

// Then prune up to 4; heights 3..4 should be deleted in this run
require.NoError(t, store.PruneExec(ctx, 4))

// Verify all heights 1..4 are gone, 5 remains
for h := uint64(1); h <= 4; h++ {
meta, err := store.GetExecMeta(ctx, h)
require.NoError(t, err)
require.Nil(t, meta)
}

meta, err := store.GetExecMeta(ctx, 5)
require.NoError(t, err)
require.NotNil(t, meta)
require.Equal(t, uint64(5), meta.Height)

// Ensure last-pruned marker is set to 4
raw, err := db.Get(ctx, ds.NewKey(lastPrunedExecMetaKey))
require.NoError(t, err)
require.Len(t, raw, 8)
last := binary.BigEndian.Uint64(raw)
require.Equal(t, uint64(4), last)
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,6 @@ replace (
google.golang.org/genproto/googleapis/api => google.golang.org/genproto/googleapis/api v0.0.0-20240213162025-012b6fc9bca9
google.golang.org/genproto/googleapis/rpc => google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9
)

// use local core module during development/CI
replace github.com/evstack/ev-node/core => ./core
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -367,8 +367,6 @@ github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6Ni
github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
github.com/envoyproxy/protoc-gen-validate v1.0.1/go.mod h1:0vj8bNkYbSTNS2PIyH87KZaeN4x9zpL9Qt8fQC7d+vs=
github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE=
github.com/evstack/ev-node/core v1.0.0-rc.1 h1:Dic2PMUMAYUl5JW6DkDj6HXDEWYzorVJQuuUJOV0FjE=
github.com/evstack/ev-node/core v1.0.0-rc.1/go.mod h1:n2w/LhYQTPsi48m6lMj16YiIqsaQw6gxwjyJvR+B3sY=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
Expand Down
Loading
Loading