diff --git a/swarm/shed/example_store_test.go b/swarm/shed/example_store_test.go index 733a972c65..d1e6aa463b 100644 --- a/swarm/shed/example_store_test.go +++ b/swarm/shed/example_store_test.go @@ -24,10 +24,14 @@ import ( "io/ioutil" "log" "os" + "strings" "time" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/swarm/shed" "github.com/ethereum/go-ethereum/swarm/storage" + "github.com/ethereum/go-ethereum/swarm/storage/mock" + "github.com/ethereum/go-ethereum/swarm/storage/mock/mem" "github.com/syndtr/goleveldb/leveldb" ) @@ -43,7 +47,7 @@ type Store struct { schemaName shed.StringField sizeCounter shed.Uint64Field accessCounter shed.Uint64Field - retrievalIndex shed.Index + retrievalIndex shed.IndexInterface // example of swapable Index/MockIndex accessIndex shed.Index gcIndex shed.Index } @@ -51,7 +55,7 @@ type Store struct { // New returns new Store. All fields and indexes are initialized // and possible conflicts with schema from existing database is checked // automatically. -func New(path string) (s *Store, err error) { +func New(path string, mockStore *mock.NodeStore) (s *Store, err error) { db, err := shed.NewDB(path) if err != nil { return nil, err @@ -70,7 +74,7 @@ func New(path string) (s *Store, err error) { return nil, err } // Index storing actual chunk address, data and store timestamp. - s.retrievalIndex, err = db.NewIndex("Address->StoreTimestamp|Data", shed.IndexFuncs{ + retrievalIndex, err := db.NewIndex("Address->StoreTimestamp|Data", shed.IndexFuncs{ EncodeKey: func(fields shed.IndexItem) (key []byte, err error) { return fields.Address, nil }, @@ -90,6 +94,14 @@ func New(path string) (s *Store, err error) { return e, nil }, }) + if err != nil { + return nil, err + } + s.retrievalIndex = retrievalIndex + if mockStore != nil { + // If mock store is provided, use it for retrieval index. + s.retrievalIndex = retrievalIndex.NewMockIndex(mockStore) + } // Index storing access timestamp for a particular address. // It is needed in order to update gc index keys for iteration order. s.accessIndex, err = db.NewIndex("Address->AccessTimestamp", shed.IndexFuncs{ @@ -110,6 +122,9 @@ func New(path string) (s *Store, err error) { return e, nil }, }) + if err != nil { + return nil, err + } // Index with keys ordered by access timestamp for garbage collection prioritization. s.gcIndex, err = db.NewIndex("AccessTimestamp|StoredTimestamp|Address->nil", shed.IndexFuncs{ EncodeKey: func(fields shed.IndexItem) (key []byte, err error) { @@ -303,7 +318,20 @@ func Example_store() { } defer os.RemoveAll(dir) - s, err := New(dir) + var mockStore *mock.NodeStore + // Configuring mock store using environment variable is + // just an example, it can be accomplished in more elegant ways. + if strings.EqualFold(os.Getenv("SWARM_USE_MOCKSTORE"), "true") { + // Global store is constructed here for example purposes. + // It should be available globally so that all nodes can access it. + globalStore := mem.NewGlobalStore() + // An arbitrary address is used for this example. + // In real situations this address should be the same as the Swarm + // node address. + mockStore = globalStore.NewNodeStore(common.HexToAddress("12345678")) + } + + s, err := New(dir, mockStore) if err != nil { log.Fatal(err) } diff --git a/swarm/shed/index.go b/swarm/shed/index.go index d1cf7a757f..04ed0f19c9 100644 --- a/swarm/shed/index.go +++ b/swarm/shed/index.go @@ -17,6 +17,7 @@ package shed import ( + "github.com/ethereum/go-ethereum/swarm/storage/mock" "github.com/syndtr/goleveldb/leveldb" ) @@ -64,6 +65,7 @@ func (i IndexItem) Join(i2 IndexItem) (new IndexItem) { // - getting a particular IndexItem // - saving a particular IndexItem // - iterating over a sorted LevelDB keys +// It implements IndexIteratorInterface interface. type Index struct { db *DB prefix []byte @@ -92,18 +94,10 @@ func (db *DB) NewIndex(name string, funcs IndexFuncs) (f Index, err error) { } prefix := []byte{id} return Index{ - db: db, - prefix: prefix, - encodeKeyFunc: func(e IndexItem) (key []byte, err error) { - key, err = funcs.EncodeKey(e) - if err != nil { - return nil, err - } - return append(append(make([]byte, 0, len(key)+1), prefix...), key...), nil - }, - decodeKeyFunc: func(key []byte) (e IndexItem, err error) { - return funcs.DecodeKey(key[1:]) - }, + db: db, + prefix: prefix, + encodeKeyFunc: newIndexEncodeKeyFunc(funcs.EncodeKey, id), + decodeKeyFunc: newDecodeKeyFunc(funcs.DecodeKey), encodeValueFunc: funcs.EncodeValue, decodeValueFunc: funcs.DecodeValue, }, nil @@ -248,3 +242,68 @@ func (f Index) IterateFrom(start IndexItem, fn IndexIterFunc) (err error) { } return it.Error() } + +// NewMockIndex is a helper function to easily construct MockIndex +// when from the same definition of Index. +func (f Index) NewMockIndex(store *mock.NodeStore) (m MockIndex) { + return MockIndex{ + store: store, + prefix: f.prefix, + encodeKeyFunc: f.encodeKeyFunc, + decodeKeyFunc: f.decodeKeyFunc, + encodeValueFunc: f.encodeValueFunc, + decodeValueFunc: f.decodeValueFunc, + } +} + +// IndexInterface defines methods that are required for a simple index +// that does not use iterations. +// It can be used when Index should be swapped with MockIndex or any other +// IndexInterface implementation. +// In most cases, interface is not needed and it is recommended to use the +// Index implementation whenever possible. +type IndexInterface interface { + Get(keyFields IndexItem) (out IndexItem, err error) + Put(i IndexItem) (err error) + PutInBatch(batch *leveldb.Batch, i IndexItem) (err error) + Delete(keyFields IndexItem) (err error) + DeleteInBatch(batch *leveldb.Batch, keyFields IndexItem) (err error) +} + +// IndexIteratorInterface defines metods for a full index implementation with +// iterators. Index type implements this type. +// In most cases, interface is not needed and it is recommended to use the +// Index implementation whenever possible. +type IndexIteratorInterface interface { + IndexInterface + IterateAll(fn IndexIterFunc) (err error) + IterateFrom(start IndexItem, fn IndexIterFunc) (err error) +} + +// newIndexEncodeKeyFunc adjusts Index and MockIndex LevelDB key +// by appending the provided index id byte. +// This is needed to avoid collisions between keys of different +// indexes as all index ids are unique. +func newIndexEncodeKeyFunc( + encodeKeyFunc func(fields IndexItem) (key []byte, err error), + id byte, +) (f func(e IndexItem) (key []byte, err error)) { + prefix := []byte{id} + return func(e IndexItem) (key []byte, err error) { + key, err = encodeKeyFunc(e) + if err != nil { + return nil, err + } + return append(append(make([]byte, 0, len(key)+1), prefix...), key...), nil + } +} + +// newDecodeKeyFunc reverses the newIndexEncodeKeyFunc constructd key +// to transparently work with index keys without their index ids. +// This function is used in NewIndex and NewMockIndex constructors. +// It assumes that index keys are prefixed with only one byte. +func newDecodeKeyFunc(decodeKeyFunc func(key []byte) (e IndexItem, err error)) (f func(key []byte) (e IndexItem, err error)) { + return func(key []byte) (e IndexItem, err error) { + return decodeKeyFunc(key[1:]) + } +} diff --git a/swarm/shed/index_mock.go b/swarm/shed/index_mock.go new file mode 100644 index 0000000000..d9b7187a3d --- /dev/null +++ b/swarm/shed/index_mock.go @@ -0,0 +1,113 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package shed + +import ( + "github.com/ethereum/go-ethereum/swarm/storage/mock" + "github.com/syndtr/goleveldb/leveldb" +) + +// MockIndex provides a way to inject a mock.NodeStore to store +// data centrally instead on provided DB. DB is used just for schema +// validation and identifying byte prefix for the particular index +// that is mocked. +// Iterator functions are not implemented and MockIndex can not replace +// indexes that rely on them. +// It implements IndexField interface. +type MockIndex struct { + store *mock.NodeStore + prefix []byte + encodeKeyFunc func(fields IndexItem) (key []byte, err error) + decodeKeyFunc func(key []byte) (e IndexItem, err error) + encodeValueFunc func(fields IndexItem) (value []byte, err error) + decodeValueFunc func(value []byte) (e IndexItem, err error) +} + +// NewMockIndex returns a new MockIndex instance with defined name and +// encoding functions. The name must be unique and will be validated +// on database schema for a key prefix byte. +// The data will not be saved on the DB itself, but using a provided +// mock.NodeStore. +func (db *DB) NewMockIndex(store *mock.NodeStore, name string, funcs IndexFuncs) (f MockIndex, err error) { + id, err := db.schemaIndexPrefix(name) + if err != nil { + return f, err + } + return MockIndex{ + store: store, + prefix: []byte{id}, + encodeKeyFunc: newIndexEncodeKeyFunc(funcs.EncodeKey, id), + decodeKeyFunc: newDecodeKeyFunc(funcs.DecodeKey), + encodeValueFunc: funcs.EncodeValue, + decodeValueFunc: funcs.DecodeValue, + }, nil +} + +// Get accepts key fields represented as IndexItem to retrieve a +// value from the index and return maximum available information +// from the index represented as another IndexItem. +func (f MockIndex) Get(keyFields IndexItem) (out IndexItem, err error) { + key, err := f.encodeKeyFunc(keyFields) + if err != nil { + return out, err + } + value, err := f.store.Get(key) + if err != nil { + return out, err + } + out, err = f.decodeValueFunc(value) + if err != nil { + return out, err + } + return out.Join(keyFields), nil +} + +// Put accepts IndexItem to encode information from it +// and save it to the database. +func (f MockIndex) Put(i IndexItem) (err error) { + key, err := f.encodeKeyFunc(i) + if err != nil { + return err + } + value, err := f.encodeValueFunc(i) + if err != nil { + return err + } + return f.store.Put(key, value) +} + +// PutInBatch is the same as Put method. +// Batch is ignored and the data is saved to the mock store instantly. +func (f MockIndex) PutInBatch(_ *leveldb.Batch, i IndexItem) (err error) { + return f.Put(i) +} + +// Delete accepts IndexItem to remove a key/value pair +// form the database based on its fields. +func (f MockIndex) Delete(keyFields IndexItem) (err error) { + key, err := f.encodeKeyFunc(keyFields) + if err != nil { + return err + } + return f.store.Delete(key) +} + +// DeleteInBatch is the same as Delete just the operation. +// Batch is ignored and the data is deleted on the mock store instantly. +func (f MockIndex) DeleteInBatch(_ *leveldb.Batch, keyFields IndexItem) (err error) { + return f.Delete(keyFields) +} diff --git a/swarm/shed/index_mock_test.go b/swarm/shed/index_mock_test.go new file mode 100644 index 0000000000..adc9dd068f --- /dev/null +++ b/swarm/shed/index_mock_test.go @@ -0,0 +1,45 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package shed + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + + "github.com/ethereum/go-ethereum/swarm/storage/mock/mem" +) + +// TestMockIndex validates put, get and delete functions of +// the MockIndex implementation. +func TestMockIndex(t *testing.T) { + db, cleanupFunc := newTestDB(t) + defer cleanupFunc() + + globalStore := mem.NewGlobalStore() + + index, err := db.NewMockIndex( + globalStore.NewNodeStore(common.HexToAddress("12345678")), + "retrieval", + retrievalIndexFuncs, + ) + if err != nil { + t.Fatal(err) + } + + testIndex(t, db, index) +} diff --git a/swarm/shed/index_test.go b/swarm/shed/index_test.go index 4e3eba326a..a5c81091cc 100644 --- a/swarm/shed/index_test.go +++ b/swarm/shed/index_test.go @@ -24,6 +24,9 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/swarm/storage/mock" + "github.com/ethereum/go-ethereum/swarm/storage/mock/mem" "github.com/syndtr/goleveldb/leveldb" ) @@ -49,7 +52,7 @@ var retrievalIndexFuncs = IndexFuncs{ }, } -// TestIndex validates put, get and delete functions of the index. +// TestIndex validates put, get and delete functions of the Index implementation. func TestIndex(t *testing.T) { db, cleanupFunc := newTestDB(t) defer cleanupFunc() @@ -59,6 +62,29 @@ func TestIndex(t *testing.T) { t.Fatal(err) } + testIndex(t, db, index) +} + +// TestIndex_NewMockIndex validates put, get and delete functions of +// the MockIndex implementation, when constructed with Index.NewMockIndex function. +func TestIndex_NewMockIndex(t *testing.T) { + db, cleanupFunc := newTestDB(t) + defer cleanupFunc() + + index, err := db.NewIndex("retrieval", retrievalIndexFuncs) + if err != nil { + t.Fatal(err) + } + + globalStore := mem.NewGlobalStore() + + mockIndex := index.NewMockIndex(globalStore.NewNodeStore(common.HexToAddress("12345678"))) + + testIndex(t, db, mockIndex) +} + +// testIndex validates put, get and delete functions of a index interface. +func testIndex(t *testing.T, db *DB, index IndexInterface) { t.Run("put", func(t *testing.T) { want := IndexItem{ Address: []byte("put-hash"), @@ -66,7 +92,7 @@ func TestIndex(t *testing.T) { StoreTimestamp: time.Now().UTC().UnixNano(), } - err = index.Put(want) + err := index.Put(want) if err != nil { t.Fatal(err) } @@ -108,7 +134,7 @@ func TestIndex(t *testing.T) { batch := new(leveldb.Batch) index.PutInBatch(batch, want) - db.WriteBatch(batch) + err := db.WriteBatch(batch) if err != nil { t.Fatal(err) } @@ -150,7 +176,7 @@ func TestIndex(t *testing.T) { StoreTimestamp: time.Now().UTC().UnixNano(), } - err = index.Put(want) + err := index.Put(want) if err != nil { t.Fatal(err) } @@ -169,11 +195,15 @@ func TestIndex(t *testing.T) { t.Fatal(err) } + wantErr := leveldb.ErrNotFound + if _, ok := index.(MockIndex); ok { + wantErr = mock.ErrNotFound + } got, err = index.Get(IndexItem{ Address: want.Address, }) - if err != leveldb.ErrNotFound { - t.Fatalf("got error %v, want %v", err, leveldb.ErrNotFound) + if err != wantErr { + t.Fatalf("got error %v, want %v", err, wantErr) } }) @@ -184,7 +214,7 @@ func TestIndex(t *testing.T) { StoreTimestamp: time.Now().UTC().UnixNano(), } - err = index.Put(want) + err := index.Put(want) if err != nil { t.Fatal(err) } @@ -205,11 +235,15 @@ func TestIndex(t *testing.T) { t.Fatal(err) } + wantErr := leveldb.ErrNotFound + if _, ok := index.(MockIndex); ok { + wantErr = mock.ErrNotFound + } got, err = index.Get(IndexItem{ Address: want.Address, }) - if err != leveldb.ErrNotFound { - t.Fatalf("got error %v, want %v", err, leveldb.ErrNotFound) + if err != wantErr { + t.Fatalf("got error %v, want %v", err, wantErr) } }) } diff --git a/swarm/storage/mock/db/db.go b/swarm/storage/mock/db/db.go index 43bfa24f05..73ae199e8b 100644 --- a/swarm/storage/mock/db/db.go +++ b/swarm/storage/mock/db/db.go @@ -86,6 +86,13 @@ func (s *GlobalStore) Put(addr common.Address, key []byte, data []byte) error { return s.db.Write(batch, nil) } +// Delete removes the chunk reference to node with address addr. +func (s *GlobalStore) Delete(addr common.Address, key []byte) error { + batch := new(leveldb.Batch) + batch.Delete(nodeDBKey(addr, key)) + return s.db.Write(batch, nil) +} + // HasKey returns whether a node with addr contains the key. func (s *GlobalStore) HasKey(addr common.Address, key []byte) bool { has, err := s.db.Has(nodeDBKey(addr, key), nil) diff --git a/swarm/storage/mock/mem/mem.go b/swarm/storage/mock/mem/mem.go index 8878309d0e..3a0a2beb8d 100644 --- a/swarm/storage/mock/mem/mem.go +++ b/swarm/storage/mock/mem/mem.go @@ -83,6 +83,22 @@ func (s *GlobalStore) Put(addr common.Address, key []byte, data []byte) error { return nil } +// Delete removes the chunk data for node with address addr. +func (s *GlobalStore) Delete(addr common.Address, key []byte) error { + s.mu.Lock() + defer s.mu.Unlock() + + var count int + if _, ok := s.nodes[string(key)]; ok { + delete(s.nodes[string(key)], addr) + count = len(s.nodes[string(key)]) + } + if count == 0 { + delete(s.data, string(key)) + } + return nil +} + // HasKey returns whether a node with addr contains the key. func (s *GlobalStore) HasKey(addr common.Address, key []byte) bool { s.mu.Lock() diff --git a/swarm/storage/mock/mock.go b/swarm/storage/mock/mock.go index 81340f9274..1fb71b70a2 100644 --- a/swarm/storage/mock/mock.go +++ b/swarm/storage/mock/mock.go @@ -70,6 +70,12 @@ func (n *NodeStore) Put(key []byte, data []byte) error { return n.store.Put(n.addr, key, data) } +// Delete removes chunk data for a key for a node that has the address +// provided on NodeStore initialization. +func (n *NodeStore) Delete(key []byte) error { + return n.store.Delete(n.addr, key) +} + // GlobalStorer defines methods for mock db store // that stores chunk data for all swarm nodes. // It is used in tests to construct mock NodeStores @@ -77,6 +83,7 @@ func (n *NodeStore) Put(key []byte, data []byte) error { type GlobalStorer interface { Get(addr common.Address, key []byte) (data []byte, err error) Put(addr common.Address, key []byte, data []byte) error + Delete(addr common.Address, key []byte) error HasKey(addr common.Address, key []byte) bool // NewNodeStore creates an instance of NodeStore // to be used by a single swarm node with diff --git a/swarm/storage/mock/rpc/rpc.go b/swarm/storage/mock/rpc/rpc.go index 6e735f6988..8cd6c83a7a 100644 --- a/swarm/storage/mock/rpc/rpc.go +++ b/swarm/storage/mock/rpc/rpc.go @@ -73,6 +73,12 @@ func (s *GlobalStore) Put(addr common.Address, key []byte, data []byte) error { return err } +// Delete calls a Delete method to RPC server. +func (s *GlobalStore) Delete(addr common.Address, key []byte) error { + err := s.client.Call(nil, "mockStore_delete", addr, key) + return err +} + // HasKey calls a HasKey method to RPC server. func (s *GlobalStore) HasKey(addr common.Address, key []byte) bool { var has bool diff --git a/swarm/storage/mock/test/test.go b/swarm/storage/mock/test/test.go index 02da3af553..10180985f3 100644 --- a/swarm/storage/mock/test/test.go +++ b/swarm/storage/mock/test/test.go @@ -72,6 +72,31 @@ func MockStore(t *testing.T, globalStore mock.GlobalStorer, n int) { } } } + t.Run("delete", func(t *testing.T) { + chunkAddr := storage.Address([]byte("1234567890abcd")) + for _, addr := range addrs { + err := globalStore.Put(addr, chunkAddr, []byte("data")) + if err != nil { + t.Fatalf("put data to store %s key %s: %v", addr.Hex(), chunkAddr.Hex(), err) + } + } + firstNodeAddr := addrs[0] + if err := globalStore.Delete(firstNodeAddr, chunkAddr); err != nil { + t.Fatalf("delete from store %s key %s: %v", firstNodeAddr.Hex(), chunkAddr.Hex(), err) + } + for i, addr := range addrs { + _, err := globalStore.Get(addr, chunkAddr) + if i == 0 { + if err != mock.ErrNotFound { + t.Errorf("get data from store %s key %s: expected mock.ErrNotFound error, got %v", addr.Hex(), chunkAddr.Hex(), err) + } + } else { + if err != nil { + t.Errorf("get data from store %s key %s: %v", addr.Hex(), chunkAddr.Hex(), err) + } + } + } + }) }) t.Run("NodeStore", func(t *testing.T) { @@ -114,6 +139,34 @@ func MockStore(t *testing.T, globalStore mock.GlobalStorer, n int) { } } } + t.Run("delete", func(t *testing.T) { + chunkAddr := storage.Address([]byte("1234567890abcd")) + var chosenStore *mock.NodeStore + for addr, store := range nodes { + if chosenStore == nil { + chosenStore = store + } + err := store.Put(chunkAddr, []byte("data")) + if err != nil { + t.Fatalf("put data to store %s key %s: %v", addr.Hex(), chunkAddr.Hex(), err) + } + } + if err := chosenStore.Delete(chunkAddr); err != nil { + t.Fatalf("delete key %s: %v", chunkAddr.Hex(), err) + } + for addr, store := range nodes { + _, err := store.Get(chunkAddr) + if store == chosenStore { + if err != mock.ErrNotFound { + t.Errorf("get data from store %s key %s: expected mock.ErrNotFound error, got %v", addr.Hex(), chunkAddr.Hex(), err) + } + } else { + if err != nil { + t.Errorf("get data from store %s key %s: %v", addr.Hex(), chunkAddr.Hex(), err) + } + } + } + }) }) }