Files

393 lines
10 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package atomictest
import (
"math/rand"
"testing"
"github.com/stretchr/testify/require"
"github.com/luxfi/constants"
"github.com/luxfi/database"
"github.com/luxfi/ids"
"github.com/luxfi/vm/chains/atomic"
)
// SharedMemoryTests is a list of all shared memory tests
var SharedMemoryTests = []func(t *testing.T, chainID0, chainID1 ids.ID, sm0, sm1 atomic.SharedMemory, db database.Database){
TestSharedMemoryPutAndGet,
TestSharedMemoryLargePutGetAndRemove,
TestSharedMemoryIndexed,
TestSharedMemoryLargeIndexed,
TestSharedMemoryCantDuplicatePut,
TestSharedMemoryCantDuplicateRemove,
TestSharedMemoryCommitOnPut,
TestSharedMemoryCommitOnRemove,
TestSharedMemoryLargeBatchSize,
TestPutAndRemoveBatch,
}
func TestSharedMemoryPutAndGet(t *testing.T, chainID0, chainID1 ids.ID, sm0, sm1 atomic.SharedMemory, _ database.Database) {
require := require.New(t)
require.NoError(sm0.Apply(map[ids.ID]*atomic.Requests{chainID1: {PutRequests: []*atomic.Element{{
Key: []byte{0},
Value: []byte{1},
}}}}))
values, err := sm1.Get(chainID0, [][]byte{{0}})
require.NoError(err)
require.Equal([][]byte{{1}}, values, "wrong values returned")
}
// TestSharedMemoryLargePutGetAndRemove tests to make sure that the interface
// can support large values.
func TestSharedMemoryLargePutGetAndRemove(t *testing.T, chainID0, chainID1 ids.ID, sm0, sm1 atomic.SharedMemory, _ database.Database) {
require := require.New(t)
rand.Seed(0)
totalSize := 16 * constants.MiB // 16 MiB
elementSize := 4 * constants.KiB // 4 KiB
pairSize := 2 * elementSize // 8 KiB
b := make([]byte, totalSize)
_, err := rand.Read(b) // #nosec G404
require.NoError(err)
elems := []*atomic.Element{}
keys := [][]byte{}
for len(b) > pairSize {
key := b[:elementSize]
b = b[elementSize:]
value := b[:elementSize]
b = b[elementSize:]
elems = append(elems, &atomic.Element{
Key: key,
Value: value,
})
keys = append(keys, key)
}
require.NoError(sm0.Apply(map[ids.ID]*atomic.Requests{
chainID1: {
PutRequests: elems,
},
}))
values, err := sm1.Get(
chainID0,
keys,
)
require.NoError(err)
for i, value := range values {
require.Equal(elems[i].Value, value)
}
require.NoError(sm1.Apply(map[ids.ID]*atomic.Requests{
chainID0: {
RemoveRequests: keys,
},
}))
}
func TestSharedMemoryIndexed(t *testing.T, chainID0, chainID1 ids.ID, sm0, sm1 atomic.SharedMemory, _ database.Database) {
require := require.New(t)
require.NoError(sm0.Apply(map[ids.ID]*atomic.Requests{chainID1: {PutRequests: []*atomic.Element{{
Key: []byte{0},
Value: []byte{1},
Traits: [][]byte{
{2},
{3},
},
}}}}))
require.NoError(sm0.Apply(map[ids.ID]*atomic.Requests{chainID1: {PutRequests: []*atomic.Element{{
Key: []byte{4},
Value: []byte{5},
Traits: [][]byte{
{2},
{3},
},
}}}}))
values, _, _, err := sm0.Indexed(chainID1, [][]byte{{2}}, nil, nil, 1)
require.NoError(err)
require.Empty(values, "wrong indexed values returned")
values, _, _, err = sm1.Indexed(chainID0, [][]byte{{2}}, nil, nil, 0)
require.NoError(err)
require.Empty(values, "wrong indexed values returned")
values, _, _, err = sm1.Indexed(chainID0, [][]byte{{2}}, nil, nil, 1)
require.NoError(err)
require.Equal([][]byte{{5}}, values, "wrong indexed values returned")
values, _, _, err = sm1.Indexed(chainID0, [][]byte{{2}}, nil, nil, 2)
require.NoError(err)
require.Equal([][]byte{{5}, {1}}, values, "wrong indexed values returned")
values, _, _, err = sm1.Indexed(chainID0, [][]byte{{2}}, nil, nil, 3)
require.NoError(err)
require.Equal([][]byte{{5}, {1}}, values, "wrong indexed values returned")
values, _, _, err = sm1.Indexed(chainID0, [][]byte{{3}}, nil, nil, 3)
require.NoError(err)
require.Equal([][]byte{{5}, {1}}, values, "wrong indexed values returned")
values, _, _, err = sm1.Indexed(chainID0, [][]byte{{2}, {3}}, nil, nil, 3)
require.NoError(err)
require.Equal([][]byte{{5}, {1}}, values, "wrong indexed values returned")
}
func TestSharedMemoryLargeIndexed(t *testing.T, chainID0, chainID1 ids.ID, sm0, sm1 atomic.SharedMemory, _ database.Database) {
require := require.New(t)
totalSize := 8 * constants.MiB // 8 MiB
elementSize := 1 * constants.KiB // 1 KiB
pairSize := 3 * elementSize // 3 KiB
b := make([]byte, totalSize)
_, err := rand.Read(b) // #nosec G404
require.NoError(err)
elems := []*atomic.Element{}
allTraits := [][]byte{}
for len(b) > pairSize {
key := b[:elementSize]
b = b[elementSize:]
value := b[:elementSize]
b = b[elementSize:]
traits := [][]byte{
b[:elementSize],
}
allTraits = append(allTraits, traits...)
b = b[elementSize:]
elems = append(elems, &atomic.Element{
Key: key,
Value: value,
Traits: traits,
})
}
require.NoError(sm0.Apply(map[ids.ID]*atomic.Requests{chainID1: {PutRequests: elems}}))
values, _, _, err := sm1.Indexed(chainID0, allTraits, nil, nil, len(elems)+1)
require.NoError(err)
require.Len(values, len(elems), "wrong number of values returned")
}
func TestSharedMemoryCantDuplicatePut(t *testing.T, _, chainID1 ids.ID, sm0, _ atomic.SharedMemory, _ database.Database) {
require := require.New(t)
err := sm0.Apply(map[ids.ID]*atomic.Requests{chainID1: {PutRequests: []*atomic.Element{
{
Key: []byte{0},
Value: []byte{1},
},
{
Key: []byte{0},
Value: []byte{2},
},
}}})
require.Error(err) //nolint:forbidigo // currently returns grpc errors too
require.NoError(sm0.Apply(map[ids.ID]*atomic.Requests{chainID1: {PutRequests: []*atomic.Element{{
Key: []byte{0},
Value: []byte{1},
}}}}))
err = sm0.Apply(map[ids.ID]*atomic.Requests{chainID1: {PutRequests: []*atomic.Element{{
Key: []byte{0},
Value: []byte{1},
}}}})
require.Error(err) //nolint:forbidigo // currently returns grpc errors too
}
func TestSharedMemoryCantDuplicateRemove(t *testing.T, _, chainID1 ids.ID, sm0, _ atomic.SharedMemory, _ database.Database) {
require := require.New(t)
require.NoError(sm0.Apply(map[ids.ID]*atomic.Requests{chainID1: {RemoveRequests: [][]byte{{0}}}}))
err := sm0.Apply(map[ids.ID]*atomic.Requests{chainID1: {RemoveRequests: [][]byte{{0}}}})
// TODO: require error to be errDuplicatedOperation
require.Error(err) //nolint:forbidigo // currently returns grpc errors too
}
func TestSharedMemoryCommitOnPut(t *testing.T, _, chainID1 ids.ID, sm0, _ atomic.SharedMemory, db database.Database) {
require := require.New(t)
require.NoError(db.Put([]byte{1}, []byte{2}))
batch := db.NewBatch()
require.NoError(batch.Put([]byte{0}, []byte{1}))
require.NoError(batch.Delete([]byte{1}))
require.NoError(sm0.Apply(
map[ids.ID]*atomic.Requests{chainID1: {PutRequests: []*atomic.Element{{
Key: []byte{0},
Value: []byte{1},
}}}},
batch,
))
val, err := db.Get([]byte{0})
require.NoError(err)
require.Equal([]byte{1}, val)
has, err := db.Has([]byte{1})
require.NoError(err)
require.False(has)
}
func TestSharedMemoryCommitOnRemove(t *testing.T, _, chainID1 ids.ID, sm0, _ atomic.SharedMemory, db database.Database) {
require := require.New(t)
require.NoError(db.Put([]byte{1}, []byte{2}))
batch := db.NewBatch()
require.NoError(batch.Put([]byte{0}, []byte{1}))
require.NoError(batch.Delete([]byte{1}))
require.NoError(sm0.Apply(
map[ids.ID]*atomic.Requests{chainID1: {RemoveRequests: [][]byte{{0}}}},
batch,
))
val, err := db.Get([]byte{0})
require.NoError(err)
require.Equal([]byte{1}, val)
has, err := db.Has([]byte{1})
require.NoError(err)
require.False(has)
}
// TestPutAndRemoveBatch tests to make sure multiple put and remove requests work properly
func TestPutAndRemoveBatch(t *testing.T, chainID0, _ ids.ID, _, sm1 atomic.SharedMemory, db database.Database) {
require := require.New(t)
batch := db.NewBatch()
require.NoError(batch.Put([]byte{0}, []byte{1}))
batchChainsAndInputs := make(map[ids.ID]*atomic.Requests)
byteArr := [][]byte{{0}, {1}, {5}}
batchChainsAndInputs[chainID0] = &atomic.Requests{
PutRequests: []*atomic.Element{{
Key: []byte{2},
Value: []byte{9},
}},
RemoveRequests: byteArr,
}
require.NoError(sm1.Apply(batchChainsAndInputs, batch))
val, err := db.Get([]byte{0})
require.NoError(err)
require.Equal([]byte{1}, val)
}
// TestSharedMemoryLargeBatchSize tests to make sure that the interface can
// support large batches.
func TestSharedMemoryLargeBatchSize(t *testing.T, _, chainID1 ids.ID, sm0, _ atomic.SharedMemory, db database.Database) {
require := require.New(t)
rand.Seed(0)
totalSize := 8 * constants.MiB // 8 MiB
elementSize := 4 * constants.KiB // 4 KiB
pairSize := 2 * elementSize // 8 KiB
bytes := make([]byte, totalSize)
_, err := rand.Read(bytes) // #nosec G404
require.NoError(err)
batch := db.NewBatch()
require.NotNil(batch)
initialBytes := bytes
for len(bytes) > pairSize {
key := bytes[:elementSize]
bytes = bytes[elementSize:]
value := bytes[:elementSize]
bytes = bytes[elementSize:]
require.NoError(batch.Put(key, value))
}
require.NoError(db.Put([]byte{1}, []byte{2}))
require.NoError(batch.Put([]byte{0}, []byte{1}))
require.NoError(batch.Delete([]byte{1}))
require.NoError(sm0.Apply(
map[ids.ID]*atomic.Requests{chainID1: {RemoveRequests: [][]byte{{0}}}},
batch,
))
val, err := db.Get([]byte{0})
require.NoError(err)
require.Equal([]byte{1}, val)
has, err := db.Has([]byte{1})
require.NoError(err)
require.False(has)
batch.Reset()
bytes = initialBytes
for len(bytes) > pairSize {
key := bytes[:elementSize]
bytes = bytes[pairSize:]
require.NoError(batch.Delete(key))
}
require.NoError(sm0.Apply(
map[ids.ID]*atomic.Requests{chainID1: {RemoveRequests: [][]byte{{1}}}},
batch,
))
batch.Reset()
bytes = initialBytes
for len(bytes) > pairSize {
key := bytes[:elementSize]
bytes = bytes[pairSize:]
require.NoError(batch.Delete(key))
}
batchChainsAndInputs := make(map[ids.ID]*atomic.Requests)
byteArr := [][]byte{{30}, {40}, {50}}
batchChainsAndInputs[chainID1] = &atomic.Requests{
PutRequests: []*atomic.Element{{
Key: []byte{2},
Value: []byte{9},
}},
RemoveRequests: byteArr,
}
require.NoError(sm0.Apply(
batchChainsAndInputs,
batch,
))
}