From 2f5137e9b72d2cf68ca92c6b797843b9c0dabef5 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Mon, 3 Oct 2022 15:05:44 +0300 Subject: [PATCH] core: allow RO mode for Bolt and Level --- docs/node-configuration.md | 10 ++++-- pkg/core/storage/boltdb_store.go | 38 ++++++++++++++------ pkg/core/storage/boltdb_store_test.go | 43 +++++++++++++++++++++++ pkg/core/storage/dbconfig/store_config.go | 2 ++ pkg/core/storage/leveldb_store.go | 8 +++-- pkg/core/storage/leveldb_store_test.go | 27 ++++++++++++++ 6 files changed, 113 insertions(+), 15 deletions(-) diff --git a/docs/node-configuration.md b/docs/node-configuration.md index 7536dc4d1..9f4fa8d3a 100644 --- a/docs/node-configuration.md +++ b/docs/node-configuration.md @@ -47,14 +47,20 @@ DBConfiguration: Type: leveldb LevelDBOptions: DataDirectoryPath: /chains/privnet + ReadOnly: false BoltDBOptions: FilePath: ./chains/privnet.bolt + ReadOnly: false ``` where: - `Type` is the database type (string value). Supported types: `levelDB` and `boltDB`. -- `LevelDBOptions` are settings for LevelDB. -- `BoltDBOptions` configures BoltDB. +- `LevelDBOptions` are settings for LevelDB. Includes the DB files path and ReadOnly mode toggle. + If ReadOnly mode is on, then an error will be returned on attempt to connect to unexisting or empty + database. Database doesn't allow changes in this mode, a warning will be logged on DB persist attempts. +- `BoltDBOptions` configures BoltDB. Includes the DB files path and ReadOnly mode toggle. If ReadOnly + mode is on, then an error will be returned on attempt to connect with unexisting or empty database. + Database doesn't allow changes in this mode, a warning will be logged on DB persist attempts. Only options for the specified database type will be used. diff --git a/pkg/core/storage/boltdb_store.go b/pkg/core/storage/boltdb_store.go index 59b51a49c..a874dbd8c 100644 --- a/pkg/core/storage/boltdb_store.go +++ b/pkg/core/storage/boltdb_store.go @@ -2,6 +2,7 @@ package storage import ( "bytes" + "errors" "fmt" "os" @@ -22,23 +23,38 @@ type BoltDBStore struct { // NewBoltDBStore returns a new ready to use BoltDB storage with created bucket. func NewBoltDBStore(cfg dbconfig.BoltDBOptions) (*BoltDBStore, error) { - var opts *bbolt.Options // should be exposed via BoltDBOptions if anything needed + cp := *bbolt.DefaultOptions // Do not change bbolt's global variable. + opts := &cp fileMode := os.FileMode(0600) // should be exposed via BoltDBOptions if anything needed fileName := cfg.FilePath - if err := io.MakeDirForFile(fileName, "BoltDB"); err != nil { - return nil, err + if cfg.ReadOnly { + opts.ReadOnly = true + } else { + if err := io.MakeDirForFile(fileName, "BoltDB"); err != nil { + return nil, err + } } db, err := bbolt.Open(fileName, fileMode, opts) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to open BoltDB instance: %w", err) + } + if opts.ReadOnly { + err = db.View(func(tx *bbolt.Tx) error { + b := tx.Bucket(Bucket) + if b == nil { + return errors.New("root bucket does not exist") + } + return nil + }) + } else { + err = db.Update(func(tx *bbolt.Tx) error { + _, err = tx.CreateBucketIfNotExists(Bucket) + if err != nil { + return fmt.Errorf("could not create root bucket: %w", err) + } + return nil + }) } - err = db.Update(func(tx *bbolt.Tx) error { - _, err = tx.CreateBucketIfNotExists(Bucket) - if err != nil { - return fmt.Errorf("could not create root bucket: %w", err) - } - return nil - }) if err != nil { return nil, fmt.Errorf("failed to initialize BoltDB instance: %w", err) } diff --git a/pkg/core/storage/boltdb_store_test.go b/pkg/core/storage/boltdb_store_test.go index e46c5b3d4..a877b4e38 100644 --- a/pkg/core/storage/boltdb_store_test.go +++ b/pkg/core/storage/boltdb_store_test.go @@ -1,11 +1,15 @@ package storage import ( + "os" "path/filepath" + "strings" "testing" "github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig" + "github.com/nspcc-dev/neo-go/pkg/io" "github.com/stretchr/testify/require" + "go.etcd.io/bbolt" ) func newBoltStoreForTesting(t testing.TB) Store { @@ -15,3 +19,42 @@ func newBoltStoreForTesting(t testing.TB) Store { require.NoError(t, err) return boltDBStore } + +func TestROBoltDB(t *testing.T) { + d := t.TempDir() + testFileName := filepath.Join(d, "test_ro_bolt_db") + cfg := dbconfig.BoltDBOptions{ + FilePath: testFileName, + ReadOnly: true, + } + + // If DB doesn't exist, then error should be returned. + _, err := NewBoltDBStore(cfg) + require.Error(t, err) + + // Create the DB and try to open it in RO mode. + cfg.ReadOnly = false + store, err := NewBoltDBStore(cfg) + require.NoError(t, err) + require.NoError(t, store.Close()) + cfg.ReadOnly = true + + store, err = NewBoltDBStore(cfg) + require.NoError(t, err) + // Changes must be prohibited. + putErr := store.PutChangeSet(map[string][]byte{"one": []byte("one")}, nil) + require.ErrorIs(t, putErr, bbolt.ErrDatabaseReadOnly) + require.NoError(t, store.Close()) + + // Create the DB without buckets and try to open it in RO mode, an error is expected. + fileMode := os.FileMode(0600) + cfg.FilePath = filepath.Join(d, "clean_ro_bolt_db") + require.NoError(t, io.MakeDirForFile(cfg.FilePath, "BoltDB")) + db, err := bbolt.Open(cfg.FilePath, fileMode, nil) + require.NoError(t, err) + require.NoError(t, db.Close()) + + _, err = NewBoltDBStore(cfg) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "root bucket does not exist")) +} diff --git a/pkg/core/storage/dbconfig/store_config.go b/pkg/core/storage/dbconfig/store_config.go index c2dde12b7..30945a640 100644 --- a/pkg/core/storage/dbconfig/store_config.go +++ b/pkg/core/storage/dbconfig/store_config.go @@ -13,9 +13,11 @@ type ( // LevelDBOptions configuration for LevelDB. LevelDBOptions struct { DataDirectoryPath string `yaml:"DataDirectoryPath"` + ReadOnly bool `yaml:"ReadOnly"` } // BoltDBOptions configuration for BoltDB. BoltDBOptions struct { FilePath string `yaml:"FilePath"` + ReadOnly bool `yaml:"ReadOnly"` } ) diff --git a/pkg/core/storage/leveldb_store.go b/pkg/core/storage/leveldb_store.go index f1f4930f2..be2537c6e 100644 --- a/pkg/core/storage/leveldb_store.go +++ b/pkg/core/storage/leveldb_store.go @@ -2,6 +2,7 @@ package storage import ( "errors" + "fmt" "github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig" "github.com/syndtr/goleveldb/leveldb" @@ -21,11 +22,14 @@ type LevelDBStore struct { // initialize the database found at the given path. func NewLevelDBStore(cfg dbconfig.LevelDBOptions) (*LevelDBStore, error) { var opts = new(opt.Options) // should be exposed via LevelDBOptions if anything needed - + if cfg.ReadOnly { + opts.ReadOnly = true + opts.ErrorIfMissing = true + } opts.Filter = filter.NewBloomFilter(10) db, err := leveldb.OpenFile(cfg.DataDirectoryPath, opts) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to open LevelDB instance: %w", err) } return &LevelDBStore{ diff --git a/pkg/core/storage/leveldb_store_test.go b/pkg/core/storage/leveldb_store_test.go index 178e81298..6e9b105e7 100644 --- a/pkg/core/storage/leveldb_store_test.go +++ b/pkg/core/storage/leveldb_store_test.go @@ -5,6 +5,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig" "github.com/stretchr/testify/require" + "github.com/syndtr/goleveldb/leveldb" ) func newLevelDBForTesting(t testing.TB) Store { @@ -16,3 +17,29 @@ func newLevelDBForTesting(t testing.TB) Store { require.Nil(t, err, "NewLevelDBStore error") return newLevelStore } + +func TestROLevelDB(t *testing.T) { + ldbDir := t.TempDir() + opts := dbconfig.LevelDBOptions{ + DataDirectoryPath: ldbDir, + ReadOnly: true, + } + + // If DB doesn't exist, then error should be returned. + _, err := NewLevelDBStore(opts) + require.Error(t, err) + + // Create the DB and try to open it in RO mode. + opts.ReadOnly = false + store, err := NewLevelDBStore(opts) + require.NoError(t, err) + require.NoError(t, store.Close()) + opts.ReadOnly = true + + store, err = NewLevelDBStore(opts) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, store.Close()) }) + // Changes must be prohibited. + putErr := store.PutChangeSet(map[string][]byte{"one": []byte("one")}, nil) + require.ErrorIs(t, putErr, leveldb.ErrReadOnly) +}