diff --git a/vms/evm/database/database.go b/vms/evm/database/database.go new file mode 100644 index 000000000000..138a2f6f0d3d --- /dev/null +++ b/vms/evm/database/database.go @@ -0,0 +1,73 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package database + +import ( + "errors" + + "github.com/ava-labs/libevm/ethdb" + + avalanchegodb "github.com/ava-labs/avalanchego/database" +) + +var ( + errSnapshotNotSupported = errors.New("snapshot is not supported") + errStatNotSupported = errors.New("stat is not supported") + + _ ethdb.Batch = (*batch)(nil) + _ ethdb.KeyValueStore = (*database)(nil) +) + +type database struct { + db avalanchegodb.Database +} + +func New(db avalanchegodb.Database) ethdb.KeyValueStore { return database{db} } + +func (database) Stat(string) (string, error) { return "", errStatNotSupported } + +func (db database) NewBatch() ethdb.Batch { return batch{batch: db.db.NewBatch()} } + +func (db database) Has(key []byte) (bool, error) { return db.db.Has(key) } + +func (db database) Get(key []byte) ([]byte, error) { return db.db.Get(key) } + +func (db database) Put(key, value []byte) error { return db.db.Put(key, value) } + +func (db database) Delete(key []byte) error { return db.db.Delete(key) } + +func (db database) Compact(start, limit []byte) error { return db.db.Compact(start, limit) } + +func (db database) Close() error { return db.db.Close() } + +func (db database) NewBatchWithSize(int) ethdb.Batch { return db.NewBatch() } + +func (database) NewSnapshot() (ethdb.Snapshot, error) { + return nil, errSnapshotNotSupported +} + +func (db database) NewIterator(prefix []byte, start []byte) ethdb.Iterator { + newStart := make([]byte, len(prefix)+len(start)) + copy(newStart, prefix) + copy(newStart[len(prefix):], start) + start = newStart + + return db.db.NewIteratorWithStartAndPrefix(start, prefix) +} + +type batch struct { + batch avalanchegodb.Batch +} + +func (b batch) Put(key, value []byte) error { return b.batch.Put(key, value) } + +func (b batch) Delete(key []byte) error { return b.batch.Delete(key) } + +func (b batch) ValueSize() int { return b.batch.Size() } + +func (b batch) Write() error { return b.batch.Write() } + +func (b batch) Reset() { b.batch.Reset() } + +func (b batch) Replay(w ethdb.KeyValueWriter) error { return b.batch.Replay(w) } diff --git a/vms/evm/database/database_test.go b/vms/evm/database/database_test.go new file mode 100644 index 000000000000..619e9597a91e --- /dev/null +++ b/vms/evm/database/database_test.go @@ -0,0 +1,148 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package database + +import ( + "bytes" + "errors" + "slices" + "testing" + + "github.com/ava-labs/libevm/ethdb" + "github.com/ava-labs/libevm/ethdb/dbtest" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/database/memdb" +) + +// testDatabase wraps the production database with test-only snapshot functionality +type testDatabase struct { + ethdb.KeyValueStore +} + +// Creates a snapshot by iterating over the entire database and copying key-value pairs. +func (db testDatabase) NewSnapshot() (ethdb.Snapshot, error) { + snapshotData := make(map[string][]byte) + + iter := db.NewIterator(nil, nil) + defer iter.Release() + + for iter.Next() { + key := iter.Key() + value := iter.Value() + valueCopy := make([]byte, len(value)) + copy(valueCopy, value) + snapshotData[string(key)] = valueCopy + } + + if err := iter.Error(); err != nil { + return nil, err + } + + return &testSnapshot{data: snapshotData}, nil +} + +// testSnapshot implements [ethdb.Snapshot] by storing a copy of the database state. +type testSnapshot struct { + data map[string][]byte +} + +func (t *testSnapshot) Get(key []byte) ([]byte, error) { + value, ok := t.data[string(key)] + if !ok { + return nil, errors.New("not found") + } + return value, nil +} + +func (t *testSnapshot) Has(key []byte) (bool, error) { + _, ok := t.data[string(key)] + return ok, nil +} + +func (*testSnapshot) Release() {} + +func (t *testSnapshot) NewIterator(prefix []byte, start []byte) ethdb.Iterator { + // Create a slice of key-value pairs that match the prefix and start criteria + pairs := make([]kvPair, 0, len(t.data)) + + for keyStr, value := range t.data { + key := []byte(keyStr) + + if prefix != nil && len(key) < len(prefix) { + continue + } + if prefix != nil && !bytes.HasPrefix(key, prefix) { + continue + } + + if start != nil && bytes.Compare(key, start) < 0 { + continue + } + + pairs = append(pairs, kvPair{key: key, value: value}) + } + + // Sort by key for consistent iteration + slices.SortFunc(pairs, func(a, b kvPair) int { + return bytes.Compare(a.key, b.key) + }) + + return &testSnapshotIterator{pairs: pairs, index: -1} +} + +type kvPair struct { + key []byte + value []byte +} + +type testSnapshotIterator struct { + pairs []kvPair + index int +} + +func (it *testSnapshotIterator) Next() bool { + it.index++ + return it.index < len(it.pairs) +} + +func (it *testSnapshotIterator) Key() []byte { + if it.index < 0 || it.index >= len(it.pairs) { + return nil + } + return it.pairs[it.index].key +} + +func (it *testSnapshotIterator) Value() []byte { + if it.index < 0 || it.index >= len(it.pairs) { + return nil + } + return it.pairs[it.index].value +} + +func (*testSnapshotIterator) Release() {} + +func (*testSnapshotIterator) Error() error { + return nil +} + +func TestInterface(t *testing.T) { + dbtest.TestDatabaseSuite(t, func() ethdb.KeyValueStore { + return &testDatabase{KeyValueStore: New(memdb.New())} + }) +} + +func TestUnimplemented(t *testing.T) { + t.Run("NewSnapshot_ReturnsError", func(t *testing.T) { + db := New(memdb.New()) + _, err := db.NewSnapshot() + require.ErrorIs(t, err, errSnapshotNotSupported) + }) + + t.Run("Stat_ReturnsError", func(t *testing.T) { + db := New(memdb.New()) + _, err := db.Stat("test") + require.ErrorIs(t, err, errStatNotSupported) + }) +}