Skip to content

Commit

Permalink
clean up cc @walldiss
Browse files Browse the repository at this point in the history
  • Loading branch information
renaynay committed Jan 17, 2025
1 parent 5c2b9a9 commit 794789c
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 115 deletions.
62 changes: 24 additions & 38 deletions nodebuilder/pruner/migration_utils.go
Original file line number Diff line number Diff line change
@@ -1,55 +1,30 @@
package pruner

import (
"bytes"
"context"
"fmt"

"github.com/ipfs/go-datastore"
"github.com/ipfs/go-datastore/namespace"

fullavail "github.com/celestiaorg/celestia-node/share/availability/full"
)

// TODO @renaynay: remove this file after a few releases -- this utility serves as a temporary solution
// to detect if the node has been run with pruning enabled before on previous version(s), and disallow
// running as an archival node.

var (
storePrefix = datastore.NewKey("full_avail")
previousModeKey = datastore.NewKey("previous_run")
pruned = []byte("pruned")
archival = []byte("archival")
)

// convertFromArchivalToPruned ensures that a node has not been run with pruning enabled before
// cannot revert to archival mode. It returns true only if the node is converting to
// pruned mode for the first time.
func convertFromArchivalToPruned(ctx context.Context, cfg *Config, ds datastore.Datastore) (bool, error) {
prevMode, err := ds.Get(ctx, previousModeKey)
if err != nil {
return false, err
}

if bytes.Equal(prevMode, pruned) && !cfg.EnableService {
return false, fullavail.ErrDisallowRevertToArchival
}

if bytes.Equal(prevMode, archival) && cfg.EnableService {
// allow conversion from archival to pruned
err = ds.Put(ctx, previousModeKey, pruned)
if err != nil {
return false, fmt.Errorf("share/availability/full: failed to updated pruning mode in "+
"datastore: %w", err)
}
return true, nil
}

// no changes in pruning mode
return false, nil
}

// detectFirstRun is a temporary function that serves to assist migration to the refactored pruner
// implementation (v0.21.0). It checks if the node has been run with pruning enabled before by checking
// if the pruner service ran before, and disallows running as an archival node in the case it has.
//
// TODO @renaynay: remove this function after a few releases.
func detectFirstRun(ctx context.Context, cfg *Config, ds datastore.Datastore, lastPrunedHeight uint64) error {
ds = namespace.Wrap(ds, storePrefix)

exists, err := ds.Has(ctx, previousModeKey)
if err != nil {
return fmt.Errorf("share/availability/full: failed to check previous pruned run in "+
Expand All @@ -59,13 +34,24 @@ func detectFirstRun(ctx context.Context, cfg *Config, ds datastore.Datastore, la
return nil
}

if !cfg.EnableService {
if lastPrunedHeight > 1 {
return fullavail.ErrDisallowRevertToArchival
}
// if service is not enabled, service is archival
isArchival := !cfg.EnableService

if isArchival && lastPrunedHeight > 1 {
return fullavail.ErrDisallowRevertToArchival
}

return recordFirstRun(ctx, ds, isArchival)
}

return ds.Put(ctx, previousModeKey, archival)
// recordFirstRun exists to assist migration to new pruner implementation (v0.21.0) by recording
// the first run of the pruner service in the full availability's datastore. It assumes the datastore
// is already namespace-wrapped.
func recordFirstRun(ctx context.Context, ds datastore.Datastore, isArchival bool) error {
mode := []byte("pruned")
if isArchival {
mode = []byte("archival")
}

return ds.Put(ctx, previousModeKey, pruned)
return ds.Put(ctx, previousModeKey, mode)
}
81 changes: 8 additions & 73 deletions nodebuilder/pruner/migration_utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,110 +13,45 @@ import (
fullavail "github.com/celestiaorg/celestia-node/share/availability/full"
)

// TestDisallowRevertArchival tests that a node that has been previously run
// with full pruning cannot convert back into an "archival" node
func TestDisallowRevertArchival(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)

// create a pruned node instance (non-archival) for the first time
cfg := &Config{EnableService: true}
ds := ds_sync.MutexWrap(datastore.NewMapDatastore())
nsWrapped := namespace.Wrap(ds, storePrefix)
err := nsWrapped.Put(ctx, previousModeKey, pruned)
require.NoError(t, err)

convert, err := convertFromArchivalToPruned(ctx, cfg, nsWrapped)
assert.NoError(t, err)
assert.False(t, convert)
// ensure availability impl recorded the pruned run
prevMode, err := nsWrapped.Get(ctx, previousModeKey)
require.NoError(t, err)
assert.Equal(t, pruned, prevMode)

// now change to archival mode
cfg.EnableService = false

// ensure failure
convert, err = convertFromArchivalToPruned(ctx, cfg, nsWrapped)
assert.Error(t, err)
assert.ErrorIs(t, err, fullavail.ErrDisallowRevertToArchival)
assert.False(t, convert)

// ensure the node can still run in pruned mode
cfg.EnableService = true
convert, err = convertFromArchivalToPruned(ctx, cfg, nsWrapped)
assert.NoError(t, err)
assert.False(t, convert)
}

// TestAllowConversionFromArchivalToPruned tests that a node that has been previously run
// in archival mode can convert to a pruned node
func TestAllowConversionFromArchivalToPruned(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)

ds := ds_sync.MutexWrap(datastore.NewMapDatastore())
nsWrapped := namespace.Wrap(ds, storePrefix)
err := nsWrapped.Put(ctx, previousModeKey, archival)
require.NoError(t, err)

cfg := &Config{EnableService: false}

convert, err := convertFromArchivalToPruned(ctx, cfg, nsWrapped)
assert.NoError(t, err)
assert.False(t, convert)

cfg.EnableService = true

convert, err = convertFromArchivalToPruned(ctx, cfg, nsWrapped)
assert.NoError(t, err)
assert.True(t, convert)

prevMode, err := nsWrapped.Get(ctx, previousModeKey)
require.NoError(t, err)
assert.Equal(t, pruned, prevMode)
}

func TestDetectFirstRun(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

t.Run("FirstRunArchival", func(t *testing.T) {
ds := ds_sync.MutexWrap(datastore.NewMapDatastore())
nsWrapped := namespace.Wrap(ds, storePrefix)

cfg := &Config{EnableService: false}

err := detectFirstRun(ctx, cfg, nsWrapped, 1)
err := detectFirstRun(ctx, cfg, ds, 1)
assert.NoError(t, err)

nsWrapped := namespace.Wrap(ds, storePrefix)
prevMode, err := nsWrapped.Get(ctx, previousModeKey)
require.NoError(t, err)
assert.Equal(t, archival, prevMode)
assert.Equal(t, []byte("archival"), prevMode)
})

t.Run("FirstRunPruned", func(t *testing.T) {
ds := ds_sync.MutexWrap(datastore.NewMapDatastore())
nsWrapped := namespace.Wrap(ds, storePrefix)

cfg := &Config{EnableService: true}

err := detectFirstRun(ctx, cfg, nsWrapped, 1)
err := detectFirstRun(ctx, cfg, ds, 1)
assert.NoError(t, err)

nsWrapped := namespace.Wrap(ds, storePrefix)
prevMode, err := nsWrapped.Get(ctx, previousModeKey)
require.NoError(t, err)
assert.Equal(t, pruned, prevMode)
assert.Equal(t, []byte("pruned"), prevMode)
})

t.Run("RevertToArchivalNotAllowed", func(t *testing.T) {
ds := ds_sync.MutexWrap(datastore.NewMapDatastore())
nsWrapped := namespace.Wrap(ds, storePrefix)

// create archival node instance over a node that has been pruned before
cfg := &Config{EnableService: false}

err := detectFirstRun(ctx, cfg, nsWrapped, 500)
err := detectFirstRun(ctx, cfg, ds, 500)
assert.Error(t, err)
assert.ErrorIs(t, err, fullavail.ErrDisallowRevertToArchival)
})
Expand Down
6 changes: 2 additions & 4 deletions nodebuilder/pruner/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"

"github.com/ipfs/go-datastore"
"github.com/ipfs/go-datastore/namespace"
logging "github.com/ipfs/go-log/v2"
"go.uber.org/fx"

Expand Down Expand Up @@ -108,8 +107,6 @@ func convertToPruned() fx.Option {
ds datastore.Batching,
p *pruner.Service,
) error {
ds = namespace.Wrap(ds, storePrefix)

lastPrunedHeight, err := p.LastPruned(ctx)
if err != nil {
return err
Expand All @@ -120,7 +117,8 @@ func convertToPruned() fx.Option {
return err
}

convert, err := convertFromArchivalToPruned(ctx, cfg, ds)
isArchival := !cfg.EnableService
convert, err := fullavail.ConvertFromArchivalToPruned(ctx, ds, isArchival)
if err != nil {
return err
}
Expand Down
46 changes: 46 additions & 0 deletions share/availability/full/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package full

import (
"bytes"
"context"
"fmt"

"github.com/ipfs/go-datastore"
"github.com/ipfs/go-datastore/namespace"
)

var (
storePrefix = datastore.NewKey("full_avail")
previousModeKey = datastore.NewKey("previous_run")
pruned = []byte("pruned")
archival = []byte("archival")
)

// ConvertFromArchivalToPruned ensures that a node has not been run with pruning enabled before
// cannot revert to archival mode. It returns true only if the node is converting to
// pruned mode for the first time.
func ConvertFromArchivalToPruned(ctx context.Context, ds datastore.Datastore, isArchival bool) (bool, error) {
ds = namespace.Wrap(ds, storePrefix)

prevMode, err := ds.Get(ctx, previousModeKey)
if err != nil {
return false, err
}

if bytes.Equal(prevMode, pruned) && isArchival {
return false, ErrDisallowRevertToArchival
}

if bytes.Equal(prevMode, archival) && !isArchival {
// allow conversion from archival to pruned
err = ds.Put(ctx, previousModeKey, pruned)
if err != nil {
return false, fmt.Errorf("share/availability/full: failed to updated pruning mode in "+
"datastore: %w", err)
}
return true, nil
}

// no changes in pruning mode
return false, nil
}
72 changes: 72 additions & 0 deletions share/availability/full/utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package full

import (
"context"
"testing"

"github.com/ipfs/go-datastore"
"github.com/ipfs/go-datastore/namespace"
ds_sync "github.com/ipfs/go-datastore/sync"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestDisallowRevertArchival tests that a node that has been previously run
// with full pruning cannot convert back into an "archival" node
func TestDisallowRevertArchival(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)

ds := ds_sync.MutexWrap(datastore.NewMapDatastore())

// create a pruned node instance (non-archival) for the first time
nsWrapped := namespace.Wrap(ds, storePrefix)
err := nsWrapped.Put(ctx, previousModeKey, pruned)
require.NoError(t, err)

convert, err := ConvertFromArchivalToPruned(ctx, ds, false)
assert.NoError(t, err)
assert.False(t, convert)
// ensure availability impl recorded the pruned run
prevMode, err := nsWrapped.Get(ctx, previousModeKey)
require.NoError(t, err)
assert.Equal(t, pruned, prevMode)

// now change to archival mode, ensure failure
convert, err = ConvertFromArchivalToPruned(ctx, ds, true)
assert.Error(t, err)
assert.ErrorIs(t, err, ErrDisallowRevertToArchival)
assert.False(t, convert)

// ensure the node can still run in pruned mode
convert, err = ConvertFromArchivalToPruned(ctx, ds, false)
assert.NoError(t, err)
assert.False(t, convert)
}

// TestAllowConversionFromArchivalToPruned tests that a node that has been previously run
// in archival mode can convert to a pruned node
func TestAllowConversionFromArchivalToPruned(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)

ds := ds_sync.MutexWrap(datastore.NewMapDatastore())

// make an archival node
nsWrapped := namespace.Wrap(ds, storePrefix)
err := nsWrapped.Put(ctx, previousModeKey, archival)
require.NoError(t, err)

convert, err := ConvertFromArchivalToPruned(ctx, ds, true)
assert.NoError(t, err)
assert.False(t, convert)

// turn into a pruned node
convert, err = ConvertFromArchivalToPruned(ctx, ds, false)
assert.NoError(t, err)
assert.True(t, convert)

prevMode, err := nsWrapped.Get(ctx, previousModeKey)
require.NoError(t, err)
assert.Equal(t, pruned, prevMode)
}

0 comments on commit 794789c

Please sign in to comment.