mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
1435 lines
43 KiB
Go
1435 lines
43 KiB
Go
//go:build grpc
|
|
|
|
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package sync
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"math/rand"
|
|
"slices"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
"github.com/luxfi/database"
|
|
"github.com/luxfi/database/memdb"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/log"
|
|
"github.com/luxfi/metric"
|
|
"github.com/luxfi/node/trace"
|
|
"github.com/luxfi/node/x/merkledb"
|
|
"github.com/luxfi/p2p"
|
|
"github.com/luxfi/p2p/p2ptest"
|
|
"github.com/luxfi/container/maybe"
|
|
"github.com/luxfi/warp"
|
|
|
|
pb "github.com/luxfi/node/proto/pb/sync"
|
|
)
|
|
|
|
var _ p2p.Handler = (*waitingHandler)(nil)
|
|
var _ p2p.Handler = (*flakyHandler)(nil)
|
|
|
|
// Test helper functions
|
|
|
|
func newDefaultDBConfig() merkledb.Config {
|
|
return merkledb.Config{
|
|
IntermediateWriteBatchSize: 100,
|
|
HistoryLength: defaultRequestKeyLimit,
|
|
ValueNodeCacheSize: defaultRequestKeyLimit,
|
|
IntermediateWriteBufferSize: defaultRequestKeyLimit,
|
|
IntermediateNodeCacheSize: defaultRequestKeyLimit,
|
|
Reg: metric.NewNoOp().Registry(),
|
|
Tracer: trace.Noop,
|
|
BranchFactor: merkledb.BranchFactor16,
|
|
}
|
|
}
|
|
|
|
func newFlakyRangeProofHandler(
|
|
t *testing.T,
|
|
db merkledb.MerkleDB,
|
|
modifyResponse func(response *merkledb.RangeProof),
|
|
) p2p.Handler {
|
|
handler := NewGetRangeProofHandler(db)
|
|
|
|
c := counter{m: 2}
|
|
return &p2p.TestHandler{
|
|
RequestF: func(ctx context.Context, nodeID ids.NodeID, deadline time.Time, requestBytes []byte) ([]byte, *warp.Error) {
|
|
responseBytes, appErr := handler.Request(ctx, nodeID, deadline, requestBytes)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
response := &pb.RangeProof{}
|
|
require.NoError(t, proto.Unmarshal(responseBytes, response))
|
|
|
|
proof := &merkledb.RangeProof{}
|
|
require.NoError(t, proof.UnmarshalProto(response))
|
|
|
|
// Half of requests are modified
|
|
if c.Inc() == 0 {
|
|
modifyResponse(proof)
|
|
}
|
|
|
|
responseBytes, err := proto.Marshal(proof.ToProto())
|
|
if err != nil {
|
|
return nil, &warp.Error{Code: 123, Message: err.Error()}
|
|
}
|
|
|
|
return responseBytes, nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func newFlakyChangeProofHandler(
|
|
t *testing.T,
|
|
db merkledb.MerkleDB,
|
|
modifyResponse func(response *merkledb.ChangeProof),
|
|
) p2p.Handler {
|
|
handler := NewGetChangeProofHandler(db)
|
|
|
|
c := counter{m: 2}
|
|
return &p2p.TestHandler{
|
|
RequestF: func(ctx context.Context, nodeID ids.NodeID, deadline time.Time, requestBytes []byte) ([]byte, *warp.Error) {
|
|
var err error
|
|
responseBytes, appErr := handler.Request(ctx, nodeID, deadline, requestBytes)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
response := &pb.SyncGetChangeProofResponse{}
|
|
require.NoError(t, proto.Unmarshal(responseBytes, response))
|
|
|
|
changeProof := response.Response.(*pb.SyncGetChangeProofResponse_ChangeProof)
|
|
proof := &merkledb.ChangeProof{}
|
|
require.NoError(t, proof.UnmarshalProto(changeProof.ChangeProof))
|
|
|
|
// Half of requests are modified
|
|
if c.Inc() == 0 {
|
|
modifyResponse(proof)
|
|
}
|
|
|
|
responseBytes, err = proto.Marshal(&pb.SyncGetChangeProofResponse{
|
|
Response: &pb.SyncGetChangeProofResponse_ChangeProof{
|
|
ChangeProof: proof.ToProto(),
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, &warp.Error{Code: 123, Message: err.Error()}
|
|
}
|
|
|
|
return responseBytes, nil
|
|
},
|
|
}
|
|
}
|
|
|
|
type flakyHandler struct {
|
|
p2p.Handler
|
|
c *counter
|
|
}
|
|
|
|
func (f *flakyHandler) Request(ctx context.Context, nodeID ids.NodeID, deadline time.Time, requestBytes []byte) ([]byte, *warp.Error) {
|
|
if f.c.Inc() == 0 {
|
|
return nil, &warp.Error{Code: 123, Message: "flake error"}
|
|
}
|
|
|
|
return f.Handler.Request(ctx, nodeID, deadline, requestBytes)
|
|
}
|
|
|
|
type counter struct {
|
|
i int
|
|
m int
|
|
lock sync.Mutex
|
|
}
|
|
|
|
func (c *counter) Inc() int {
|
|
c.lock.Lock()
|
|
defer c.lock.Unlock()
|
|
|
|
tmp := c.i
|
|
result := tmp % c.m
|
|
|
|
c.i++
|
|
return result
|
|
}
|
|
|
|
type waitingHandler struct {
|
|
p2p.NoOpHandler
|
|
handler p2p.Handler
|
|
updatedRootChan chan struct{}
|
|
}
|
|
|
|
func (w *waitingHandler) Request(ctx context.Context, nodeID ids.NodeID, deadline time.Time, requestBytes []byte) ([]byte, *warp.Error) {
|
|
<-w.updatedRootChan
|
|
return w.handler.Request(ctx, nodeID, deadline, requestBytes)
|
|
}
|
|
|
|
func Test_Creation(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
db, err := merkledb.New(
|
|
context.Background(),
|
|
memdb.New(),
|
|
newDefaultDBConfig(),
|
|
)
|
|
require.NoError(err)
|
|
|
|
ctx := context.Background()
|
|
syncer, err := NewManager(ManagerConfig[[]byte]{
|
|
DB: db,
|
|
RangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetRangeProofHandler(db)),
|
|
ChangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetChangeProofHandler(db)),
|
|
SimultaneousWorkLimit: 5,
|
|
Log: log.NewNoOpLogger(),
|
|
BranchFactor: merkledb.BranchFactor16,
|
|
}, metric.NewRegistry())
|
|
require.NoError(err)
|
|
require.NotNil(syncer)
|
|
}
|
|
|
|
func Test_Completion(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
emptyDB, err := merkledb.New(
|
|
context.Background(),
|
|
memdb.New(),
|
|
newDefaultDBConfig(),
|
|
)
|
|
require.NoError(err)
|
|
|
|
emptyRoot, err := emptyDB.GetMerkleRoot(context.Background())
|
|
require.NoError(err)
|
|
|
|
db, err := merkledb.New(
|
|
context.Background(),
|
|
memdb.New(),
|
|
newDefaultDBConfig(),
|
|
)
|
|
require.NoError(err)
|
|
|
|
ctx := context.Background()
|
|
syncer, err := NewManager(ManagerConfig[[]byte]{
|
|
DB: db,
|
|
RangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetRangeProofHandler(emptyDB)),
|
|
ChangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetChangeProofHandler(emptyDB)),
|
|
TargetRoot: emptyRoot,
|
|
SimultaneousWorkLimit: 5,
|
|
Log: log.NewNoOpLogger(),
|
|
BranchFactor: merkledb.BranchFactor16,
|
|
}, metric.NewRegistry())
|
|
require.NoError(err)
|
|
require.NotNil(syncer)
|
|
|
|
require.NoError(syncer.Start(context.Background()))
|
|
require.NoError(syncer.Wait(context.Background()))
|
|
|
|
syncer.workLock.Lock()
|
|
require.Zero(syncer.unprocessedWork.Len())
|
|
require.Equal(1, syncer.processedWork.Len())
|
|
syncer.workLock.Unlock()
|
|
}
|
|
|
|
func Test_Midpoint(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
mid := midPoint(maybe.Some([]byte{1, 255}), maybe.Some([]byte{2, 1}))
|
|
require.Equal(maybe.Some([]byte{2, 0}), mid)
|
|
|
|
mid = midPoint(maybe.Nothing[[]byte](), maybe.Some([]byte{255, 255, 0}))
|
|
require.Equal(maybe.Some([]byte{127, 255, 128}), mid)
|
|
|
|
mid = midPoint(maybe.Some([]byte{255, 255, 255}), maybe.Some([]byte{255, 255}))
|
|
require.Equal(maybe.Some([]byte{255, 255, 127, 128}), mid)
|
|
|
|
mid = midPoint(maybe.Nothing[[]byte](), maybe.Some([]byte{255}))
|
|
require.Equal(maybe.Some([]byte{127, 127}), mid)
|
|
|
|
mid = midPoint(maybe.Some([]byte{1, 255}), maybe.Some([]byte{255, 1}))
|
|
require.Equal(maybe.Some([]byte{128, 128}), mid)
|
|
|
|
mid = midPoint(maybe.Some([]byte{140, 255}), maybe.Some([]byte{141, 0}))
|
|
require.Equal(maybe.Some([]byte{140, 255, 127}), mid)
|
|
|
|
mid = midPoint(maybe.Some([]byte{126, 255}), maybe.Some([]byte{127}))
|
|
require.Equal(maybe.Some([]byte{126, 255, 127}), mid)
|
|
|
|
mid = midPoint(maybe.Nothing[[]byte](), maybe.Nothing[[]byte]())
|
|
require.Equal(maybe.Some([]byte{127}), mid)
|
|
|
|
low := midPoint(maybe.Nothing[[]byte](), mid)
|
|
require.Equal(maybe.Some([]byte{63, 127}), low)
|
|
|
|
high := midPoint(mid, maybe.Nothing[[]byte]())
|
|
require.Equal(maybe.Some([]byte{191}), high)
|
|
|
|
mid = midPoint(maybe.Some([]byte{255, 255}), maybe.Nothing[[]byte]())
|
|
require.Equal(maybe.Some([]byte{255, 255, 127, 127}), mid)
|
|
|
|
mid = midPoint(maybe.Some([]byte{255}), maybe.Nothing[[]byte]())
|
|
require.Equal(maybe.Some([]byte{255, 127, 127}), mid)
|
|
|
|
for i := 0; i < 5000; i++ {
|
|
r := rand.New(rand.NewSource(int64(i))) // #nosec G404
|
|
|
|
start := make([]byte, r.Intn(99)+1)
|
|
_, err := r.Read(start)
|
|
require.NoError(err)
|
|
|
|
end := make([]byte, r.Intn(99)+1)
|
|
_, err = r.Read(end)
|
|
require.NoError(err)
|
|
|
|
for bytes.Equal(start, end) {
|
|
_, err = r.Read(end)
|
|
require.NoError(err)
|
|
}
|
|
|
|
if bytes.Compare(start, end) == 1 {
|
|
start, end = end, start
|
|
}
|
|
|
|
mid = midPoint(maybe.Some(start), maybe.Some(end))
|
|
require.Equal(-1, bytes.Compare(start, mid.Value()))
|
|
require.Equal(-1, bytes.Compare(mid.Value(), end))
|
|
}
|
|
}
|
|
|
|
func Test_Sync_FindNextKey_InSync(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
now := time.Now().UnixNano()
|
|
t.Logf("seed: %d", now)
|
|
r := rand.New(rand.NewSource(now)) // #nosec G404
|
|
dbToSync, err := generateTrie(t, r, 1000)
|
|
require.NoError(err)
|
|
syncRoot, err := dbToSync.GetMerkleRoot(context.Background())
|
|
require.NoError(err)
|
|
|
|
db, err := merkledb.New(
|
|
context.Background(),
|
|
memdb.New(),
|
|
newDefaultDBConfig(),
|
|
)
|
|
require.NoError(err)
|
|
|
|
ctx := context.Background()
|
|
syncer, err := NewManager(ManagerConfig[[]byte]{
|
|
DB: db,
|
|
RangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetRangeProofHandler(dbToSync)),
|
|
ChangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetChangeProofHandler(dbToSync)),
|
|
TargetRoot: syncRoot,
|
|
SimultaneousWorkLimit: 5,
|
|
Log: log.NewNoOpLogger(),
|
|
BranchFactor: merkledb.BranchFactor16,
|
|
}, metric.NewRegistry())
|
|
require.NoError(err)
|
|
require.NotNil(syncer)
|
|
|
|
require.NoError(syncer.Start(context.Background()))
|
|
require.NoError(syncer.Wait(context.Background()))
|
|
|
|
proof, err := dbToSync.GetRangeProof(context.Background(), maybe.Nothing[[]byte](), maybe.Nothing[[]byte](), 500)
|
|
require.NoError(err)
|
|
|
|
// the two dbs should be in sync, so next key should be nil
|
|
lastKey := proof.KeyChanges[len(proof.KeyChanges)-1].Key
|
|
nextKey, err := syncer.findNextKey(context.Background(), lastKey, maybe.Nothing[[]byte](), proof.EndProof)
|
|
require.NoError(err)
|
|
require.True(nextKey.IsNothing())
|
|
|
|
// add an extra value to sync db past the last key returned
|
|
newKey := midPoint(maybe.Some(lastKey), maybe.Nothing[[]byte]())
|
|
newKeyVal := newKey.Value()
|
|
require.NoError(db.Put(newKeyVal, []byte{1}))
|
|
|
|
// create a range endpoint that is before the newly added key, but after the last key
|
|
endPointBeforeNewKey := make([]byte, 0, 2)
|
|
for i := 0; i < len(newKeyVal); i++ {
|
|
endPointBeforeNewKey = append(endPointBeforeNewKey, newKeyVal[i])
|
|
|
|
// we need the new key to be after the last key
|
|
// don't subtract anything from the current byte if newkey and lastkey are equal
|
|
if lastKey[i] == newKeyVal[i] {
|
|
continue
|
|
}
|
|
|
|
// if the first nibble is > 0, subtract "1" from it
|
|
if endPointBeforeNewKey[i] >= 16 {
|
|
endPointBeforeNewKey[i] -= 16
|
|
break
|
|
}
|
|
// if the second nibble > 0, subtract 1 from it
|
|
if endPointBeforeNewKey[i] > 0 {
|
|
endPointBeforeNewKey[i] -= 1
|
|
break
|
|
}
|
|
// both nibbles were 0, so move onto the next byte
|
|
}
|
|
|
|
nextKey, err = syncer.findNextKey(context.Background(), lastKey, maybe.Some(endPointBeforeNewKey), proof.EndProof)
|
|
require.NoError(err)
|
|
|
|
// next key would be after the end of the range, so it returns Nothing instead
|
|
require.True(nextKey.IsNothing())
|
|
}
|
|
|
|
func Test_Sync_FindNextKey_Deleted(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
db, err := merkledb.New(
|
|
context.Background(),
|
|
memdb.New(),
|
|
newDefaultDBConfig(),
|
|
)
|
|
require.NoError(err)
|
|
require.NoError(db.Put([]byte{0x10}, []byte{1}))
|
|
require.NoError(db.Put([]byte{0x11, 0x11}, []byte{2}))
|
|
|
|
syncRoot, err := db.GetMerkleRoot(context.Background())
|
|
require.NoError(err)
|
|
|
|
ctx := context.Background()
|
|
syncer, err := NewManager(ManagerConfig[[]byte]{
|
|
DB: db,
|
|
RangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetRangeProofHandler(db)),
|
|
ChangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetChangeProofHandler(db)),
|
|
TargetRoot: syncRoot,
|
|
SimultaneousWorkLimit: 5,
|
|
Log: log.NewNoOpLogger(),
|
|
BranchFactor: merkledb.BranchFactor16,
|
|
}, metric.NewRegistry())
|
|
require.NoError(err)
|
|
|
|
// 0x12 was "deleted" and there should be no extra node in the proof since there was nothing with a common prefix
|
|
noExtraNodeProof, err := db.GetProof(context.Background(), []byte{0x12})
|
|
require.NoError(err)
|
|
|
|
// 0x11 was "deleted" and 0x11.0x11 should be in the exclusion proof
|
|
extraNodeProof, err := db.GetProof(context.Background(), []byte{0x11})
|
|
require.NoError(err)
|
|
|
|
// there is now another value in the range that needs to be sync'ed
|
|
require.NoError(db.Put([]byte{0x13}, []byte{3}))
|
|
|
|
nextKey, err := syncer.findNextKey(context.Background(), []byte{0x12}, maybe.Some([]byte{0x20}), noExtraNodeProof.Path)
|
|
require.NoError(err)
|
|
require.Equal(maybe.Some([]byte{0x13}), nextKey)
|
|
|
|
nextKey, err = syncer.findNextKey(context.Background(), []byte{0x11}, maybe.Some([]byte{0x20}), extraNodeProof.Path)
|
|
require.NoError(err)
|
|
require.Equal(maybe.Some([]byte{0x13}), nextKey)
|
|
}
|
|
|
|
func Test_Sync_FindNextKey_BranchInLocal(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
db, err := merkledb.New(
|
|
context.Background(),
|
|
memdb.New(),
|
|
newDefaultDBConfig(),
|
|
)
|
|
require.NoError(err)
|
|
require.NoError(db.Put([]byte{0x11}, []byte{1}))
|
|
require.NoError(db.Put([]byte{0x11, 0x11}, []byte{2}))
|
|
|
|
targetRoot, err := db.GetMerkleRoot(context.Background())
|
|
require.NoError(err)
|
|
|
|
proof, err := db.GetProof(context.Background(), []byte{0x11, 0x11})
|
|
require.NoError(err)
|
|
|
|
ctx := context.Background()
|
|
syncer, err := NewManager(ManagerConfig[[]byte]{
|
|
DB: db,
|
|
RangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetRangeProofHandler(db)),
|
|
ChangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetChangeProofHandler(db)),
|
|
TargetRoot: targetRoot,
|
|
SimultaneousWorkLimit: 5,
|
|
Log: log.NewNoOpLogger(),
|
|
BranchFactor: merkledb.BranchFactor16,
|
|
}, metric.NewRegistry())
|
|
require.NoError(err)
|
|
require.NoError(db.Put([]byte{0x11, 0x15}, []byte{4}))
|
|
|
|
nextKey, err := syncer.findNextKey(context.Background(), []byte{0x11, 0x11}, maybe.Some([]byte{0x20}), proof.Path)
|
|
require.NoError(err)
|
|
require.Equal(maybe.Some([]byte{0x11, 0x15}), nextKey)
|
|
}
|
|
|
|
func Test_Sync_FindNextKey_BranchInReceived(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
db, err := merkledb.New(
|
|
context.Background(),
|
|
memdb.New(),
|
|
newDefaultDBConfig(),
|
|
)
|
|
require.NoError(err)
|
|
require.NoError(db.Put([]byte{0x11}, []byte{1}))
|
|
require.NoError(db.Put([]byte{0x12}, []byte{2}))
|
|
require.NoError(db.Put([]byte{0x12, 0xA0}, []byte{4}))
|
|
|
|
targetRoot, err := db.GetMerkleRoot(context.Background())
|
|
require.NoError(err)
|
|
|
|
proof, err := db.GetProof(context.Background(), []byte{0x12})
|
|
require.NoError(err)
|
|
|
|
ctx := context.Background()
|
|
syncer, err := NewManager(ManagerConfig[[]byte]{
|
|
DB: db,
|
|
RangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetRangeProofHandler(db)),
|
|
ChangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetChangeProofHandler(db)),
|
|
TargetRoot: targetRoot,
|
|
SimultaneousWorkLimit: 5,
|
|
Log: log.NewNoOpLogger(),
|
|
BranchFactor: merkledb.BranchFactor16,
|
|
}, metric.NewRegistry())
|
|
require.NoError(err)
|
|
require.NoError(db.Delete([]byte{0x12, 0xA0}))
|
|
|
|
nextKey, err := syncer.findNextKey(context.Background(), []byte{0x12}, maybe.Some([]byte{0x20}), proof.Path)
|
|
require.NoError(err)
|
|
require.Equal(maybe.Some([]byte{0x12, 0xA0}), nextKey)
|
|
}
|
|
|
|
func Test_Sync_FindNextKey_ExtraValues(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
now := time.Now().UnixNano()
|
|
t.Logf("seed: %d", now)
|
|
r := rand.New(rand.NewSource(now)) // #nosec G404
|
|
dbToSync, err := generateTrie(t, r, 1000)
|
|
require.NoError(err)
|
|
syncRoot, err := dbToSync.GetMerkleRoot(context.Background())
|
|
require.NoError(err)
|
|
|
|
db, err := merkledb.New(
|
|
context.Background(),
|
|
memdb.New(),
|
|
newDefaultDBConfig(),
|
|
)
|
|
require.NoError(err)
|
|
|
|
ctx := context.Background()
|
|
syncer, err := NewManager(ManagerConfig[[]byte]{
|
|
DB: db,
|
|
RangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetRangeProofHandler(dbToSync)),
|
|
ChangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetChangeProofHandler(dbToSync)),
|
|
TargetRoot: syncRoot,
|
|
SimultaneousWorkLimit: 5,
|
|
Log: log.NewNoOpLogger(),
|
|
BranchFactor: merkledb.BranchFactor16,
|
|
}, metric.NewRegistry())
|
|
require.NoError(err)
|
|
require.NotNil(syncer)
|
|
|
|
require.NoError(syncer.Start(context.Background()))
|
|
require.NoError(syncer.Wait(context.Background()))
|
|
|
|
proof, err := dbToSync.GetRangeProof(context.Background(), maybe.Nothing[[]byte](), maybe.Nothing[[]byte](), 500)
|
|
require.NoError(err)
|
|
|
|
// add an extra value to local db
|
|
lastKey := proof.KeyChanges[len(proof.KeyChanges)-1].Key
|
|
midpoint := midPoint(maybe.Some(lastKey), maybe.Nothing[[]byte]())
|
|
midPointVal := midpoint.Value()
|
|
|
|
require.NoError(db.Put(midPointVal, []byte{1}))
|
|
|
|
// next key at prefix of newly added point
|
|
nextKey, err := syncer.findNextKey(context.Background(), lastKey, maybe.Nothing[[]byte](), proof.EndProof)
|
|
require.NoError(err)
|
|
require.True(nextKey.HasValue())
|
|
|
|
require.True(isPrefix(midPointVal, nextKey.Value()))
|
|
|
|
require.NoError(db.Delete(midPointVal))
|
|
|
|
require.NoError(dbToSync.Put(midPointVal, []byte{1}))
|
|
|
|
proof, err = dbToSync.GetRangeProof(context.Background(), maybe.Nothing[[]byte](), maybe.Some(lastKey), 500)
|
|
require.NoError(err)
|
|
|
|
// next key at prefix of newly added point
|
|
nextKey, err = syncer.findNextKey(context.Background(), lastKey, maybe.Nothing[[]byte](), proof.EndProof)
|
|
require.NoError(err)
|
|
require.True(nextKey.HasValue())
|
|
|
|
// deal with odd length key
|
|
require.True(isPrefix(midPointVal, nextKey.Value()))
|
|
}
|
|
|
|
func TestFindNextKeyEmptyEndProof(t *testing.T) {
|
|
require := require.New(t)
|
|
now := time.Now().UnixNano()
|
|
t.Logf("seed: %d", now)
|
|
r := rand.New(rand.NewSource(now)) // #nosec G404
|
|
|
|
db, err := merkledb.New(
|
|
context.Background(),
|
|
memdb.New(),
|
|
newDefaultDBConfig(),
|
|
)
|
|
require.NoError(err)
|
|
|
|
ctx := context.Background()
|
|
syncer, err := NewManager(ManagerConfig[[]byte]{
|
|
DB: db,
|
|
RangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetRangeProofHandler(db)),
|
|
ChangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetChangeProofHandler(db)),
|
|
TargetRoot: ids.Empty,
|
|
SimultaneousWorkLimit: 5,
|
|
Log: log.NewNoOpLogger(),
|
|
BranchFactor: merkledb.BranchFactor16,
|
|
}, metric.NewRegistry())
|
|
require.NoError(err)
|
|
require.NotNil(syncer)
|
|
|
|
for i := 0; i < 100; i++ {
|
|
lastReceivedKeyLen := r.Intn(16)
|
|
lastReceivedKey := make([]byte, lastReceivedKeyLen)
|
|
_, _ = r.Read(lastReceivedKey) // #nosec G404
|
|
|
|
rangeEndLen := r.Intn(16)
|
|
rangeEndBytes := make([]byte, rangeEndLen)
|
|
_, _ = r.Read(rangeEndBytes) // #nosec G404
|
|
|
|
rangeEnd := maybe.Nothing[[]byte]()
|
|
if rangeEndLen > 0 {
|
|
rangeEnd = maybe.Some(rangeEndBytes)
|
|
}
|
|
|
|
nextKey, err := syncer.findNextKey(
|
|
context.Background(),
|
|
lastReceivedKey,
|
|
rangeEnd,
|
|
nil, /* endProof */
|
|
)
|
|
require.NoError(err)
|
|
require.Equal(maybe.Some(append(lastReceivedKey, 0)), nextKey)
|
|
}
|
|
}
|
|
|
|
func isPrefix(data []byte, prefix []byte) bool {
|
|
if prefix[len(prefix)-1]%16 == 0 {
|
|
index := 0
|
|
for ; index < len(prefix)-1; index++ {
|
|
if data[index] != prefix[index] {
|
|
return false
|
|
}
|
|
}
|
|
return data[index]>>4 == prefix[index]>>4
|
|
}
|
|
return bytes.HasPrefix(data, prefix)
|
|
}
|
|
|
|
func Test_Sync_FindNextKey_DifferentChild(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
now := time.Now().UnixNano()
|
|
t.Logf("seed: %d", now)
|
|
r := rand.New(rand.NewSource(now)) // #nosec G404
|
|
dbToSync, err := generateTrie(t, r, 500)
|
|
require.NoError(err)
|
|
syncRoot, err := dbToSync.GetMerkleRoot(context.Background())
|
|
require.NoError(err)
|
|
|
|
db, err := merkledb.New(
|
|
context.Background(),
|
|
memdb.New(),
|
|
newDefaultDBConfig(),
|
|
)
|
|
require.NoError(err)
|
|
|
|
ctx := context.Background()
|
|
syncer, err := NewManager(ManagerConfig[[]byte]{
|
|
DB: db,
|
|
RangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetRangeProofHandler(dbToSync)),
|
|
ChangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetChangeProofHandler(dbToSync)),
|
|
TargetRoot: syncRoot,
|
|
SimultaneousWorkLimit: 5,
|
|
Log: log.NewNoOpLogger(),
|
|
BranchFactor: merkledb.BranchFactor16,
|
|
}, metric.NewRegistry())
|
|
require.NoError(err)
|
|
require.NotNil(syncer)
|
|
require.NoError(syncer.Start(context.Background()))
|
|
require.NoError(syncer.Wait(context.Background()))
|
|
|
|
proof, err := dbToSync.GetRangeProof(context.Background(), maybe.Nothing[[]byte](), maybe.Nothing[[]byte](), 100)
|
|
require.NoError(err)
|
|
lastKey := proof.KeyChanges[len(proof.KeyChanges)-1].Key
|
|
|
|
// local db has a different child than remote db
|
|
lastKey = append(lastKey, 16)
|
|
require.NoError(db.Put(lastKey, []byte{1}))
|
|
|
|
require.NoError(dbToSync.Put(lastKey, []byte{2}))
|
|
|
|
proof, err = dbToSync.GetRangeProof(context.Background(), maybe.Nothing[[]byte](), maybe.Some(proof.KeyChanges[len(proof.KeyChanges)-1].Key), 100)
|
|
require.NoError(err)
|
|
|
|
nextKey, err := syncer.findNextKey(context.Background(), proof.KeyChanges[len(proof.KeyChanges)-1].Key, maybe.Nothing[[]byte](), proof.EndProof)
|
|
require.NoError(err)
|
|
require.True(nextKey.HasValue())
|
|
require.Equal(lastKey, nextKey.Value())
|
|
}
|
|
|
|
// Test findNextKey by computing the expected result in a naive, inefficient
|
|
// way and comparing it to the actual result
|
|
func TestFindNextKeyRandom(t *testing.T) {
|
|
now := time.Now().UnixNano()
|
|
t.Logf("seed: %d", now)
|
|
rand := rand.New(rand.NewSource(now)) // #nosec G404
|
|
require := require.New(t)
|
|
|
|
// Create a "remote" database and "local" database
|
|
remoteDB, err := merkledb.New(
|
|
context.Background(),
|
|
memdb.New(),
|
|
newDefaultDBConfig(),
|
|
)
|
|
require.NoError(err)
|
|
|
|
config := newDefaultDBConfig()
|
|
localDB, err := merkledb.New(
|
|
context.Background(),
|
|
memdb.New(),
|
|
config,
|
|
)
|
|
require.NoError(err)
|
|
|
|
var (
|
|
numProofsToTest = 250
|
|
numKeyValues = 250
|
|
maxKeyLen = 256
|
|
maxValLen = 256
|
|
maxRangeStartLen = 8
|
|
maxRangeEndLen = 8
|
|
maxProofLen = 128
|
|
)
|
|
|
|
// Put random keys into the databases
|
|
for _, db := range []database.Database{remoteDB, localDB} {
|
|
for i := 0; i < numKeyValues; i++ {
|
|
key := make([]byte, rand.Intn(maxKeyLen))
|
|
_, _ = rand.Read(key)
|
|
val := make([]byte, rand.Intn(maxValLen))
|
|
_, _ = rand.Read(val)
|
|
require.NoError(db.Put(key, val))
|
|
}
|
|
}
|
|
|
|
// Repeatedly generate end proofs from the remote database and compare
|
|
// the result of findNextKey to the expected result.
|
|
for proofIndex := 0; proofIndex < numProofsToTest; proofIndex++ {
|
|
// Generate a proof for a random key
|
|
var (
|
|
rangeStart []byte
|
|
rangeEnd []byte
|
|
)
|
|
// Generate a valid range start and end
|
|
for rangeStart == nil || bytes.Compare(rangeStart, rangeEnd) == 1 {
|
|
rangeStart = make([]byte, rand.Intn(maxRangeStartLen)+1)
|
|
_, _ = rand.Read(rangeStart)
|
|
rangeEnd = make([]byte, rand.Intn(maxRangeEndLen)+1)
|
|
_, _ = rand.Read(rangeEnd)
|
|
}
|
|
|
|
startKey := maybe.Nothing[[]byte]()
|
|
if len(rangeStart) > 0 {
|
|
startKey = maybe.Some(rangeStart)
|
|
}
|
|
endKey := maybe.Nothing[[]byte]()
|
|
if len(rangeEnd) > 0 {
|
|
endKey = maybe.Some(rangeEnd)
|
|
}
|
|
|
|
remoteProof, err := remoteDB.GetRangeProof(
|
|
context.Background(),
|
|
startKey,
|
|
endKey,
|
|
rand.Intn(maxProofLen)+1,
|
|
)
|
|
require.NoError(err)
|
|
|
|
if len(remoteProof.KeyChanges) == 0 {
|
|
continue
|
|
}
|
|
lastReceivedKey := remoteProof.KeyChanges[len(remoteProof.KeyChanges)-1].Key
|
|
|
|
// Commit the proof to the local database as we do
|
|
// in the actual syncer.
|
|
require.NoError(localDB.CommitRangeProof(
|
|
context.Background(),
|
|
startKey,
|
|
endKey,
|
|
remoteProof,
|
|
))
|
|
|
|
localProof, err := localDB.GetProof(
|
|
context.Background(),
|
|
lastReceivedKey,
|
|
)
|
|
require.NoError(err)
|
|
|
|
type keyAndID struct {
|
|
key merkledb.Key
|
|
id ids.ID
|
|
}
|
|
|
|
// Set of key prefix/ID pairs proven by the remote database's end proof.
|
|
remoteKeyIDs := []keyAndID{}
|
|
for _, node := range remoteProof.EndProof {
|
|
for childIdx, childID := range node.Children {
|
|
remoteKeyIDs = append(remoteKeyIDs, keyAndID{
|
|
key: node.Key.Extend(merkledb.ToToken(childIdx, merkledb.BranchFactorToTokenSize[config.BranchFactor])),
|
|
id: childID,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Set of key prefix/ID pairs proven by the local database's proof.
|
|
localKeyIDs := []keyAndID{}
|
|
for _, node := range localProof.Path {
|
|
for childIdx, childID := range node.Children {
|
|
localKeyIDs = append(localKeyIDs, keyAndID{
|
|
key: node.Key.Extend(merkledb.ToToken(childIdx, merkledb.BranchFactorToTokenSize[config.BranchFactor])),
|
|
id: childID,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Sort in ascending order by key prefix.
|
|
serializedPathCompare := func(i, j keyAndID) int {
|
|
return i.key.Compare(j.key)
|
|
}
|
|
slices.SortFunc(remoteKeyIDs, serializedPathCompare)
|
|
slices.SortFunc(localKeyIDs, serializedPathCompare)
|
|
|
|
// Filter out keys that are before the last received key
|
|
findBounds := func(keyIDs []keyAndID) (int, int) {
|
|
var (
|
|
firstIdxInRange = len(keyIDs)
|
|
firstIdxInRangeFound = false
|
|
firstIdxOutOfRange = len(keyIDs)
|
|
)
|
|
for i, keyID := range keyIDs {
|
|
if !firstIdxInRangeFound && bytes.Compare(keyID.key.Bytes(), lastReceivedKey) > 0 {
|
|
firstIdxInRange = i
|
|
firstIdxInRangeFound = true
|
|
continue
|
|
}
|
|
if bytes.Compare(keyID.key.Bytes(), rangeEnd) > 0 {
|
|
firstIdxOutOfRange = i
|
|
break
|
|
}
|
|
}
|
|
return firstIdxInRange, firstIdxOutOfRange
|
|
}
|
|
|
|
remoteFirstIdxAfterLastReceived, remoteFirstIdxAfterEnd := findBounds(remoteKeyIDs)
|
|
remoteKeyIDs = remoteKeyIDs[remoteFirstIdxAfterLastReceived:remoteFirstIdxAfterEnd]
|
|
|
|
localFirstIdxAfterLastReceived, localFirstIdxAfterEnd := findBounds(localKeyIDs)
|
|
localKeyIDs = localKeyIDs[localFirstIdxAfterLastReceived:localFirstIdxAfterEnd]
|
|
|
|
// Find smallest difference between the set of key/ID pairs proven by
|
|
// the remote/local proofs for key/ID pairs after the last received key.
|
|
var (
|
|
smallestDiffKey merkledb.Key
|
|
foundDiff bool
|
|
)
|
|
for i := 0; i < len(remoteKeyIDs) && i < len(localKeyIDs); i++ {
|
|
// See if the keys are different.
|
|
smaller, bigger := remoteKeyIDs[i], localKeyIDs[i]
|
|
if serializedPathCompare(localKeyIDs[i], remoteKeyIDs[i]) == -1 {
|
|
smaller, bigger = localKeyIDs[i], remoteKeyIDs[i]
|
|
}
|
|
|
|
if smaller.key != bigger.key || smaller.id != bigger.id {
|
|
smallestDiffKey = smaller.key
|
|
foundDiff = true
|
|
break
|
|
}
|
|
}
|
|
if !foundDiff {
|
|
// All the keys were equal. The smallest diff is the next key
|
|
// in the longer of the lists (if they're not same length.)
|
|
if len(remoteKeyIDs) < len(localKeyIDs) {
|
|
smallestDiffKey = localKeyIDs[len(remoteKeyIDs)].key
|
|
} else if len(remoteKeyIDs) > len(localKeyIDs) {
|
|
smallestDiffKey = remoteKeyIDs[len(localKeyIDs)].key
|
|
}
|
|
}
|
|
|
|
// Get the actual value from the syncer
|
|
ctx := context.Background()
|
|
syncer, err := NewManager(ManagerConfig[[]byte]{
|
|
DB: localDB,
|
|
RangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetRangeProofHandler(remoteDB)),
|
|
ChangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetChangeProofHandler(remoteDB)),
|
|
TargetRoot: ids.GenerateTestID(),
|
|
SimultaneousWorkLimit: 5,
|
|
Log: log.NewNoOpLogger(),
|
|
BranchFactor: merkledb.BranchFactor16,
|
|
}, metric.NewRegistry())
|
|
require.NoError(err)
|
|
|
|
gotFirstDiff, err := syncer.findNextKey(
|
|
context.Background(),
|
|
lastReceivedKey,
|
|
endKey,
|
|
remoteProof.EndProof,
|
|
)
|
|
require.NoError(err)
|
|
|
|
if bytes.Compare(smallestDiffKey.Bytes(), rangeEnd) >= 0 {
|
|
// The smallest key which differs is after the range end so the
|
|
// next key to get should be nil because we're done fetching the range.
|
|
require.True(gotFirstDiff.IsNothing())
|
|
} else {
|
|
require.Equal(smallestDiffKey.Bytes(), gotFirstDiff.Value())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tests that we are able to sync to the correct root while the server is
|
|
// updating
|
|
func Test_Sync_Result_Correct_Root(t *testing.T) {
|
|
now := time.Now().UnixNano()
|
|
t.Logf("seed: %d", now)
|
|
r := rand.New(rand.NewSource(now)) // #nosec G404
|
|
|
|
tests := []struct {
|
|
name string
|
|
db merkledb.MerkleDB
|
|
rangeProofClient func(db merkledb.MerkleDB) *p2p.Client
|
|
changeProofClient func(db merkledb.MerkleDB) *p2p.Client
|
|
}{
|
|
{
|
|
name: "range proof bad response - too many leaves in response",
|
|
rangeProofClient: func(db merkledb.MerkleDB) *p2p.Client {
|
|
handler := newFlakyRangeProofHandler(t, db, func(response *merkledb.RangeProof) {
|
|
response.KeyChanges = append(response.KeyChanges, merkledb.KeyChange{})
|
|
})
|
|
|
|
return p2ptest.NewSelfClient(t, context.Background(), ids.EmptyNodeID, handler)
|
|
},
|
|
},
|
|
{
|
|
name: "range proof bad response - removed first key in response",
|
|
rangeProofClient: func(db merkledb.MerkleDB) *p2p.Client {
|
|
handler := newFlakyRangeProofHandler(t, db, func(response *merkledb.RangeProof) {
|
|
response.KeyChanges = response.KeyChanges[min(1, len(response.KeyChanges)):]
|
|
})
|
|
|
|
return p2ptest.NewSelfClient(t, context.Background(), ids.EmptyNodeID, handler)
|
|
},
|
|
},
|
|
{
|
|
name: "range proof bad response - removed first key in response and replaced proof",
|
|
rangeProofClient: func(db merkledb.MerkleDB) *p2p.Client {
|
|
handler := newFlakyRangeProofHandler(t, db, func(response *merkledb.RangeProof) {
|
|
response.KeyChanges = response.KeyChanges[min(1, len(response.KeyChanges)):]
|
|
response.KeyChanges = []merkledb.KeyChange{
|
|
{
|
|
Key: []byte("foo"),
|
|
Value: maybe.Some([]byte("bar")),
|
|
},
|
|
}
|
|
response.StartProof = []merkledb.ProofNode{
|
|
{
|
|
Key: merkledb.Key{},
|
|
},
|
|
}
|
|
response.EndProof = []merkledb.ProofNode{
|
|
{
|
|
Key: merkledb.Key{},
|
|
},
|
|
}
|
|
})
|
|
|
|
return p2ptest.NewSelfClient(t, context.Background(), ids.EmptyNodeID, handler)
|
|
},
|
|
},
|
|
{
|
|
name: "range proof bad response - removed key from middle of response",
|
|
rangeProofClient: func(db merkledb.MerkleDB) *p2p.Client {
|
|
handler := newFlakyRangeProofHandler(t, db, func(response *merkledb.RangeProof) {
|
|
i := rand.Intn(max(1, len(response.KeyChanges)-1)) // #nosec G404
|
|
_ = slices.Delete(response.KeyChanges, i, min(len(response.KeyChanges), i+1))
|
|
})
|
|
|
|
return p2ptest.NewSelfClient(t, context.Background(), ids.EmptyNodeID, handler)
|
|
},
|
|
},
|
|
{
|
|
name: "range proof bad response - start and end proof nodes removed",
|
|
rangeProofClient: func(db merkledb.MerkleDB) *p2p.Client {
|
|
handler := newFlakyRangeProofHandler(t, db, func(response *merkledb.RangeProof) {
|
|
response.StartProof = nil
|
|
response.EndProof = nil
|
|
})
|
|
|
|
return p2ptest.NewSelfClient(t, context.Background(), ids.EmptyNodeID, handler)
|
|
},
|
|
},
|
|
{
|
|
name: "range proof bad response - end proof removed",
|
|
rangeProofClient: func(db merkledb.MerkleDB) *p2p.Client {
|
|
handler := newFlakyRangeProofHandler(t, db, func(response *merkledb.RangeProof) {
|
|
response.EndProof = nil
|
|
})
|
|
|
|
return p2ptest.NewSelfClient(t, context.Background(), ids.EmptyNodeID, handler)
|
|
},
|
|
},
|
|
{
|
|
name: "range proof bad response - empty proof",
|
|
rangeProofClient: func(db merkledb.MerkleDB) *p2p.Client {
|
|
handler := newFlakyRangeProofHandler(t, db, func(response *merkledb.RangeProof) {
|
|
response.StartProof = nil
|
|
response.EndProof = nil
|
|
response.KeyChanges = nil
|
|
})
|
|
|
|
return p2ptest.NewSelfClient(t, context.Background(), ids.EmptyNodeID, handler)
|
|
},
|
|
},
|
|
{
|
|
name: "range proof server flake",
|
|
rangeProofClient: func(db merkledb.MerkleDB) *p2p.Client {
|
|
return p2ptest.NewSelfClient(t, context.Background(), ids.EmptyNodeID, &flakyHandler{
|
|
Handler: NewGetRangeProofHandler(db),
|
|
c: &counter{m: 2},
|
|
})
|
|
},
|
|
},
|
|
{
|
|
name: "change proof bad response - too many keys in response",
|
|
changeProofClient: func(db merkledb.MerkleDB) *p2p.Client {
|
|
handler := newFlakyChangeProofHandler(t, db, func(response *merkledb.ChangeProof) {
|
|
response.KeyChanges = append(response.KeyChanges, make([]merkledb.KeyChange, defaultRequestKeyLimit)...)
|
|
})
|
|
|
|
return p2ptest.NewSelfClient(t, context.Background(), ids.EmptyNodeID, handler)
|
|
},
|
|
},
|
|
{
|
|
name: "change proof bad response - removed first key in response",
|
|
changeProofClient: func(db merkledb.MerkleDB) *p2p.Client {
|
|
handler := newFlakyChangeProofHandler(t, db, func(response *merkledb.ChangeProof) {
|
|
response.KeyChanges = response.KeyChanges[min(1, len(response.KeyChanges)):]
|
|
})
|
|
|
|
return p2ptest.NewSelfClient(t, context.Background(), ids.EmptyNodeID, handler)
|
|
},
|
|
},
|
|
{
|
|
name: "change proof bad response - removed key from middle of response",
|
|
changeProofClient: func(db merkledb.MerkleDB) *p2p.Client {
|
|
handler := newFlakyChangeProofHandler(t, db, func(response *merkledb.ChangeProof) {
|
|
i := rand.Intn(max(1, len(response.KeyChanges)-1)) // #nosec G404
|
|
_ = slices.Delete(response.KeyChanges, i, min(len(response.KeyChanges), i+1))
|
|
})
|
|
|
|
return p2ptest.NewSelfClient(t, context.Background(), ids.EmptyNodeID, handler)
|
|
},
|
|
},
|
|
{
|
|
name: "change proof bad response - all proof keys removed from response",
|
|
changeProofClient: func(db merkledb.MerkleDB) *p2p.Client {
|
|
handler := newFlakyChangeProofHandler(t, db, func(response *merkledb.ChangeProof) {
|
|
response.StartProof = nil
|
|
response.EndProof = nil
|
|
})
|
|
|
|
return p2ptest.NewSelfClient(t, context.Background(), ids.EmptyNodeID, handler)
|
|
},
|
|
},
|
|
{
|
|
name: "change proof flaky server",
|
|
changeProofClient: func(db merkledb.MerkleDB) *p2p.Client {
|
|
return p2ptest.NewSelfClient(t, context.Background(), ids.EmptyNodeID, &flakyHandler{
|
|
Handler: NewGetChangeProofHandler(db),
|
|
c: &counter{m: 2},
|
|
})
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
ctx := context.Background()
|
|
dbToSync, err := generateTrie(t, r, 3*maxKeyValuesLimit)
|
|
require.NoError(err)
|
|
|
|
syncRoot, err := dbToSync.GetMerkleRoot(ctx)
|
|
require.NoError(err)
|
|
|
|
db, err := merkledb.New(
|
|
ctx,
|
|
memdb.New(),
|
|
newDefaultDBConfig(),
|
|
)
|
|
require.NoError(err)
|
|
|
|
var (
|
|
rangeProofClient *p2p.Client
|
|
changeProofClient *p2p.Client
|
|
)
|
|
|
|
rangeProofHandler := NewGetRangeProofHandler(dbToSync)
|
|
rangeProofClient = p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, rangeProofHandler)
|
|
if tt.rangeProofClient != nil {
|
|
rangeProofClient = tt.rangeProofClient(dbToSync)
|
|
}
|
|
|
|
changeProofHandler := NewGetChangeProofHandler(dbToSync)
|
|
changeProofClient = p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, changeProofHandler)
|
|
if tt.changeProofClient != nil {
|
|
changeProofClient = tt.changeProofClient(dbToSync)
|
|
}
|
|
|
|
syncer, err := NewManager(ManagerConfig[[]byte]{
|
|
DB: db,
|
|
RangeProofClient: rangeProofClient,
|
|
ChangeProofClient: changeProofClient,
|
|
TargetRoot: syncRoot,
|
|
SimultaneousWorkLimit: 5,
|
|
Log: log.NewNoOpLogger(),
|
|
BranchFactor: merkledb.BranchFactor16,
|
|
}, metric.NewRegistry())
|
|
|
|
require.NoError(err)
|
|
require.NotNil(syncer)
|
|
|
|
// Start syncing from the server
|
|
require.NoError(syncer.Start(ctx))
|
|
|
|
// Simulate writes on the server
|
|
//
|
|
// TODO add more writes when api is not flaky. There is an inherent
|
|
// race condition in between writes where UpdateSyncTarget might
|
|
// error because it has already reached the sync target before it
|
|
// is called.
|
|
for i := 0; i < 50; i++ {
|
|
addkey := make([]byte, r.Intn(50))
|
|
_, err = r.Read(addkey)
|
|
require.NoError(err)
|
|
val := make([]byte, r.Intn(50))
|
|
_, err = r.Read(val)
|
|
require.NoError(err)
|
|
|
|
// Update the server's root + our sync target
|
|
require.NoError(dbToSync.Put(addkey, val))
|
|
targetRoot, err := dbToSync.GetMerkleRoot(ctx)
|
|
require.NoError(err)
|
|
|
|
// Simulate client periodically recording root updates
|
|
require.NoError(syncer.UpdateSyncTarget(targetRoot))
|
|
}
|
|
|
|
// Block until all syncing is done
|
|
require.NoError(syncer.Wait(ctx))
|
|
|
|
// We should have the same resulting root as the server
|
|
wantRoot, err := dbToSync.GetMerkleRoot(context.Background())
|
|
require.NoError(err)
|
|
|
|
gotRoot, err := db.GetMerkleRoot(context.Background())
|
|
require.NoError(err)
|
|
require.Equal(wantRoot, gotRoot)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_Sync_Result_Correct_Root_With_Sync_Restart(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
now := time.Now().UnixNano()
|
|
t.Logf("seed: %d", now)
|
|
r := rand.New(rand.NewSource(now)) // #nosec G404
|
|
dbToSync, err := generateTrie(t, r, 3*maxKeyValuesLimit)
|
|
require.NoError(err)
|
|
syncRoot, err := dbToSync.GetMerkleRoot(context.Background())
|
|
require.NoError(err)
|
|
|
|
db, err := merkledb.New(
|
|
context.Background(),
|
|
memdb.New(),
|
|
newDefaultDBConfig(),
|
|
)
|
|
require.NoError(err)
|
|
|
|
ctx := context.Background()
|
|
syncer, err := NewManager(ManagerConfig[[]byte]{
|
|
DB: db,
|
|
RangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetRangeProofHandler(dbToSync)),
|
|
ChangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetChangeProofHandler(dbToSync)),
|
|
TargetRoot: syncRoot,
|
|
SimultaneousWorkLimit: 5,
|
|
Log: log.NewNoOpLogger(),
|
|
BranchFactor: merkledb.BranchFactor16,
|
|
}, metric.NewRegistry())
|
|
require.NoError(err)
|
|
require.NotNil(syncer)
|
|
require.NoError(syncer.Start(context.Background()))
|
|
|
|
// Wait until we've processed some work
|
|
// before updating the sync target.
|
|
require.Eventually(
|
|
func() bool {
|
|
syncer.workLock.Lock()
|
|
defer syncer.workLock.Unlock()
|
|
|
|
return syncer.processedWork.Len() > 0
|
|
},
|
|
5*time.Second,
|
|
5*time.Millisecond,
|
|
)
|
|
syncer.Close()
|
|
|
|
newSyncer, err := NewManager(ManagerConfig[[]byte]{
|
|
DB: db,
|
|
RangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetRangeProofHandler(dbToSync)),
|
|
ChangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetChangeProofHandler(dbToSync)),
|
|
TargetRoot: syncRoot,
|
|
SimultaneousWorkLimit: 5,
|
|
Log: log.NewNoOpLogger(),
|
|
BranchFactor: merkledb.BranchFactor16,
|
|
}, metric.NewRegistry())
|
|
require.NoError(err)
|
|
require.NotNil(newSyncer)
|
|
|
|
require.NoError(newSyncer.Start(context.Background()))
|
|
require.NoError(newSyncer.Error())
|
|
require.NoError(newSyncer.Wait(context.Background()))
|
|
|
|
newRoot, err := db.GetMerkleRoot(context.Background())
|
|
require.NoError(err)
|
|
require.Equal(syncRoot, newRoot)
|
|
}
|
|
|
|
func Test_Sync_Result_Correct_Root_Update_Root_During(t *testing.T) {
|
|
t.Skip("Test exhibits timing-dependent behavior; requires refactoring for determinism")
|
|
|
|
require := require.New(t)
|
|
|
|
now := time.Now().UnixNano()
|
|
t.Logf("seed: %d", now)
|
|
r := rand.New(rand.NewSource(now)) // #nosec G404
|
|
|
|
dbToSync, err := generateTrie(t, r, 3*maxKeyValuesLimit)
|
|
require.NoError(err)
|
|
|
|
firstSyncRoot, err := dbToSync.GetMerkleRoot(context.Background())
|
|
require.NoError(err)
|
|
|
|
for x := 0; x < 100; x++ {
|
|
key := make([]byte, r.Intn(50))
|
|
_, err = r.Read(key)
|
|
require.NoError(err)
|
|
|
|
val := make([]byte, r.Intn(50))
|
|
_, err = r.Read(val)
|
|
require.NoError(err)
|
|
|
|
require.NoError(dbToSync.Put(key, val))
|
|
|
|
deleteKeyStart := make([]byte, r.Intn(50))
|
|
_, err = r.Read(deleteKeyStart)
|
|
require.NoError(err)
|
|
|
|
it := dbToSync.NewIteratorWithStart(deleteKeyStart)
|
|
if it.Next() {
|
|
require.NoError(dbToSync.Delete(it.Key()))
|
|
}
|
|
require.NoError(it.Error())
|
|
it.Release()
|
|
}
|
|
|
|
secondSyncRoot, err := dbToSync.GetMerkleRoot(context.Background())
|
|
require.NoError(err)
|
|
|
|
db, err := merkledb.New(
|
|
context.Background(),
|
|
memdb.New(),
|
|
newDefaultDBConfig(),
|
|
)
|
|
require.NoError(err)
|
|
|
|
// Only let one response go through until we update the root.
|
|
updatedRootChan := make(chan struct{}, 1)
|
|
updatedRootChan <- struct{}{}
|
|
|
|
ctx := context.Background()
|
|
rangeProofClient := p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, &waitingHandler{
|
|
handler: NewGetRangeProofHandler(dbToSync),
|
|
updatedRootChan: updatedRootChan,
|
|
})
|
|
|
|
changeProofClient := p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, &waitingHandler{
|
|
handler: NewGetChangeProofHandler(dbToSync),
|
|
updatedRootChan: updatedRootChan,
|
|
})
|
|
|
|
syncer, err := NewManager(ManagerConfig[[]byte]{
|
|
DB: db,
|
|
RangeProofClient: rangeProofClient,
|
|
ChangeProofClient: changeProofClient,
|
|
TargetRoot: firstSyncRoot,
|
|
SimultaneousWorkLimit: 5,
|
|
Log: log.NewNoOpLogger(),
|
|
BranchFactor: merkledb.BranchFactor16,
|
|
}, metric.NewRegistry())
|
|
require.NoError(err)
|
|
require.NotNil(syncer)
|
|
|
|
require.NoError(syncer.Start(context.Background()))
|
|
|
|
// Wait until we've processed some work
|
|
// before updating the sync target.
|
|
require.Eventually(
|
|
func() bool {
|
|
syncer.workLock.Lock()
|
|
defer syncer.workLock.Unlock()
|
|
|
|
return syncer.processedWork.Len() > 0
|
|
},
|
|
5*time.Second,
|
|
10*time.Millisecond,
|
|
)
|
|
require.NoError(syncer.UpdateSyncTarget(secondSyncRoot))
|
|
close(updatedRootChan)
|
|
|
|
require.NoError(syncer.Wait(context.Background()))
|
|
require.NoError(syncer.Error())
|
|
|
|
newRoot, err := db.GetMerkleRoot(context.Background())
|
|
require.NoError(err)
|
|
require.Equal(secondSyncRoot, newRoot)
|
|
}
|
|
|
|
func Test_Sync_UpdateSyncTarget(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
db, err := merkledb.New(
|
|
context.Background(),
|
|
memdb.New(),
|
|
newDefaultDBConfig(),
|
|
)
|
|
require.NoError(err)
|
|
ctx := context.Background()
|
|
m, err := NewManager(ManagerConfig[[]byte]{
|
|
DB: db,
|
|
RangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetRangeProofHandler(db)),
|
|
ChangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetChangeProofHandler(db)),
|
|
TargetRoot: ids.Empty,
|
|
SimultaneousWorkLimit: 5,
|
|
Log: log.NewNoOpLogger(),
|
|
BranchFactor: merkledb.BranchFactor16,
|
|
}, metric.NewRegistry())
|
|
require.NoError(err)
|
|
|
|
// Populate [m.processWork] to ensure that UpdateSyncTarget
|
|
// moves the work to [m.unprocessedWork].
|
|
item := &workItem{
|
|
start: maybe.Some([]byte{1}),
|
|
end: maybe.Some([]byte{2}),
|
|
localRootID: ids.GenerateTestID(),
|
|
}
|
|
m.processedWork.Insert(item)
|
|
|
|
// Make sure that [m.unprocessedWorkCond] is signaled.
|
|
gotSignalChan := make(chan struct{})
|
|
// Don't UpdateSyncTarget until we're waiting for the signal.
|
|
startedWaiting := make(chan struct{})
|
|
go func() {
|
|
m.workLock.Lock()
|
|
defer m.workLock.Unlock()
|
|
|
|
close(startedWaiting)
|
|
m.unprocessedWorkCond.Wait()
|
|
close(gotSignalChan)
|
|
}()
|
|
|
|
<-startedWaiting
|
|
newSyncRoot := ids.GenerateTestID()
|
|
require.NoError(m.UpdateSyncTarget(newSyncRoot))
|
|
<-gotSignalChan
|
|
|
|
require.Equal(newSyncRoot, m.config.TargetRoot)
|
|
require.Zero(m.processedWork.Len())
|
|
require.Equal(1, m.unprocessedWork.Len())
|
|
}
|
|
|
|
func generateTrie(t *testing.T, r *rand.Rand, count int) (merkledb.MerkleDB, error) {
|
|
return generateTrieWithMinKeyLen(t, r, count, 0)
|
|
}
|
|
|
|
func generateTrieWithMinKeyLen(t *testing.T, r *rand.Rand, count int, minKeyLen int) (merkledb.MerkleDB, error) {
|
|
require := require.New(t)
|
|
|
|
db, err := merkledb.New(
|
|
context.Background(),
|
|
memdb.New(),
|
|
newDefaultDBConfig(),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var (
|
|
allKeys [][]byte
|
|
seenKeys = make(map[string]struct{})
|
|
batch = db.NewBatch()
|
|
)
|
|
genKey := func() []byte {
|
|
// new prefixed key
|
|
if len(allKeys) > 2 && r.Intn(25) < 10 {
|
|
prefix := allKeys[r.Intn(len(allKeys))]
|
|
key := make([]byte, r.Intn(50)+len(prefix))
|
|
copy(key, prefix)
|
|
_, err := r.Read(key[len(prefix):])
|
|
require.NoError(err)
|
|
return key
|
|
}
|
|
|
|
// new key
|
|
key := make([]byte, r.Intn(50)+minKeyLen)
|
|
_, err = r.Read(key)
|
|
require.NoError(err)
|
|
return key
|
|
}
|
|
|
|
for i := 0; i < count; {
|
|
value := make([]byte, r.Intn(51))
|
|
if len(value) == 0 {
|
|
value = nil
|
|
} else {
|
|
_, err = r.Read(value)
|
|
require.NoError(err)
|
|
}
|
|
key := genKey()
|
|
if _, seen := seenKeys[string(key)]; seen {
|
|
continue // avoid duplicate keys so we always get the count
|
|
}
|
|
allKeys = append(allKeys, key)
|
|
seenKeys[string(key)] = struct{}{}
|
|
if err = batch.Put(key, value); err != nil {
|
|
return db, err
|
|
}
|
|
i++
|
|
}
|
|
return db, batch.Write()
|
|
}
|