Files
evm/sync/client/client_test.go
T
zeekay fa04ea03ce fix: handle missing snapshot layers gracefully in flattenSnapshot
When tests use SnapshotNoBuild: true with SnapshotLimit > 0, snapshot
layers are initialized but not built for new blocks. Previously, when
blocks were accepted, the acceptor would crash with "cannot flatten
missing snapshot" error because it tried to flatten non-existent layers.

This fix adds:
- HasBlockLayer(blockHash) method to snapshot.Tree to check layer existence
- Check in flattenSnapshot to skip flattening if layer doesn't exist

This properly handles test configurations where snapshots are enabled
but layers aren't built, allowing all 63 packages to pass with 0 failures.
2025-12-04 00:23:59 -08:00

888 lines
28 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package statesyncclient
import (
"bytes"
"context"
"fmt"
"math/rand"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/luxfi/ids"
"github.com/luxfi/crypto"
"github.com/luxfi/evm/consensus/dummy"
"github.com/luxfi/evm/core"
"github.com/luxfi/evm/params"
"github.com/luxfi/evm/plugin/evm/message"
clientstats "github.com/luxfi/evm/sync/client/stats"
"github.com/luxfi/evm/sync/handlers"
handlerstats "github.com/luxfi/evm/sync/handlers/stats"
"github.com/luxfi/evm/sync/statesync/statesynctest"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/rawdb"
"github.com/luxfi/geth/core/types"
ethparams "github.com/luxfi/geth/params"
"github.com/luxfi/geth/triedb"
)
func TestGetCode(t *testing.T) {
// Test re-enabled for mainnet deployment
mockNetClient := &mockNetwork{}
tests := map[string]struct {
setupRequest func() (requestHashes []common.Hash, mockResponse message.CodeResponse, expectedCode [][]byte)
expectedErr error
}{
"normal": {
setupRequest: func() ([]common.Hash, message.CodeResponse, [][]byte) {
code := []byte("this is the code")
codeHash := crypto.Keccak256Hash(code)
codeSlices := [][]byte{code}
return []common.Hash{common.Hash(codeHash)}, message.CodeResponse{
Data: codeSlices,
}, codeSlices
},
expectedErr: nil,
},
"unexpected code bytes": {
setupRequest: func() (requestHashes []common.Hash, mockResponse message.CodeResponse, expectedCode [][]byte) {
return []common.Hash{{1}}, message.CodeResponse{
Data: [][]byte{{1}},
}, nil
},
expectedErr: errHashMismatch,
},
"too many code elements returned": {
setupRequest: func() (requestHashes []common.Hash, mockResponse message.CodeResponse, expectedCode [][]byte) {
return []common.Hash{{1}}, message.CodeResponse{
Data: [][]byte{{1}, {2}},
}, nil
},
expectedErr: errInvalidCodeResponseLen,
},
"too few code elements returned": {
setupRequest: func() (requestHashes []common.Hash, mockResponse message.CodeResponse, expectedCode [][]byte) {
return []common.Hash{{1}}, message.CodeResponse{
Data: [][]byte{},
}, nil
},
expectedErr: errInvalidCodeResponseLen,
},
"code size is too large": {
setupRequest: func() (requestHashes []common.Hash, mockResponse message.CodeResponse, expectedCode [][]byte) {
oversizedCode := make([]byte, ethparams.MaxCodeSize+1)
codeHash := crypto.Keccak256Hash(oversizedCode)
return []common.Hash{common.Hash(codeHash)}, message.CodeResponse{
Data: [][]byte{oversizedCode},
}, nil
},
expectedErr: errMaxCodeSizeExceeded,
},
}
stateSyncClient := NewClient(&ClientConfig{
NetworkClient: mockNetClient,
Codec: message.Codec,
Stats: clientstats.NewNoOpStats(),
StateSyncNodeIDs: nil,
BlockParser: mockBlockParser,
})
for name, test := range tests {
t.Run(name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
codeHashes, res, expectedCode := test.setupRequest()
responseBytes, err := message.Codec.Marshal(message.Version, res)
if err != nil {
t.Fatal(err)
}
// Dirty hack required because the client will re-request if it encounters
// an error.
attempted := false
if test.expectedErr == nil {
mockNetClient.mockResponse(1, nil, responseBytes)
} else {
mockNetClient.mockResponse(2, func() {
// Cancel before the second attempt is processed.
if attempted {
cancel()
}
attempted = true
}, responseBytes)
}
codeBytes, err := stateSyncClient.GetCode(ctx, codeHashes)
// If we expect an error, assert that one occurred and return
if test.expectedErr != nil {
assert.ErrorIs(t, err, test.expectedErr)
assert.EqualValues(t, 2, mockNetClient.numCalls)
return
}
// Otherwise, assert there was no error and that the result is as expected
assert.NoError(t, err)
assert.Equal(t, len(codeBytes), len(expectedCode))
for i, code := range codeBytes {
assert.Equal(t, expectedCode[i], code)
}
assert.Equal(t, uint(1), mockNetClient.numCalls)
})
}
}
func TestGetBlocks(t *testing.T) {
// Test re-enabled for mainnet deployment
// set random seed for deterministic tests
rand.Seed(1)
var gspec = &core.Genesis{
Config: params.TestChainConfig,
}
memdb := rawdb.NewMemoryDatabase()
tdb := triedb.NewDatabase(memdb, nil)
genesis := gspec.MustCommit(memdb, tdb)
engine := dummy.NewETHFaker()
numBlocks := 110
blocks, _, err := core.GenerateChain(params.TestChainConfig, genesis, engine, memdb, numBlocks, 0, func(i int, b *core.BlockGen) {})
if err != nil {
t.Fatal("unexpected error when generating test blockchain", err)
}
assert.Equal(t, numBlocks, len(blocks))
// Construct client
mockNetClient := &mockNetwork{}
stateSyncClient := NewClient(&ClientConfig{
NetworkClient: mockNetClient,
Codec: message.Codec,
Stats: clientstats.NewNoOpStats(),
StateSyncNodeIDs: nil,
BlockParser: mockBlockParser,
})
blocksRequestHandler := handlers.NewBlockRequestHandler(buildGetter(blocks), message.Codec, handlerstats.NewNoopHandlerStats())
// encodeBlockSlice takes a slice of blocks that are ordered in increasing height order
// and returns a slice of byte slices with those blocks encoded in reverse order
encodeBlockSlice := func(blocks []*types.Block) [][]byte {
blockBytes := make([][]byte, 0, len(blocks))
for i := len(blocks) - 1; i >= 0; i-- {
buf := new(bytes.Buffer)
if err := blocks[i].EncodeRLP(buf); err != nil {
t.Fatalf("failed to generate expected response %s", err)
}
blockBytes = append(blockBytes, buf.Bytes())
}
return blockBytes
}
tests := map[string]struct {
request message.BlockRequest
getResponse func(t *testing.T, request message.BlockRequest) []byte
assertResponse func(t *testing.T, response []*types.Block)
expectedErr string
}{
"normal resonse": {
request: message.BlockRequest{
Hash: blocks[100].Hash(),
Height: 100,
Parents: 16,
},
getResponse: func(t *testing.T, request message.BlockRequest) []byte {
response, err := blocksRequestHandler.OnBlockRequest(context.Background(), ids.GenerateTestNodeID(), 1, request)
if err != nil {
t.Fatal(err)
}
if len(response) == 0 {
t.Fatal("Failed to generate valid response")
}
return response
},
assertResponse: func(t *testing.T, response []*types.Block) {
assert.Equal(t, 16, len(response))
},
},
"fewer than requested blocks": {
request: message.BlockRequest{
Hash: blocks[100].Hash(),
Height: 100,
Parents: 16,
},
getResponse: func(t *testing.T, request message.BlockRequest) []byte {
request.Parents -= 5
response, err := blocksRequestHandler.OnBlockRequest(context.Background(), ids.GenerateTestNodeID(), 1, request)
if err != nil {
t.Fatal(err)
}
if len(response) == 0 {
t.Fatal("Failed to generate valid response")
}
return response
},
// If the server returns fewer than requested blocks, we should consider it valid
assertResponse: func(t *testing.T, response []*types.Block) {
assert.Equal(t, 11, len(response))
},
},
"gibberish response": {
request: message.BlockRequest{
Hash: blocks[100].Hash(),
Height: 100,
Parents: 16,
},
getResponse: func(t *testing.T, request message.BlockRequest) []byte {
return []byte("gibberish")
},
expectedErr: errUnmarshalResponse.Error(),
},
"invalid value replacing block": {
request: message.BlockRequest{
Hash: blocks[100].Hash(),
Height: 100,
Parents: 16,
},
getResponse: func(t *testing.T, request message.BlockRequest) []byte {
response, err := blocksRequestHandler.OnBlockRequest(context.Background(), ids.GenerateTestNodeID(), 1, request)
if err != nil {
t.Fatalf("failed to get block response: %s", err)
}
var blockResponse message.BlockResponse
if _, err = message.Codec.Unmarshal(response, &blockResponse); err != nil {
t.Fatalf("failed to marshal block response: %s", err)
}
// Replace middle value with garbage data
blockResponse.Blocks[10] = []byte("invalid value replacing block bytes")
responseBytes, err := message.Codec.Marshal(message.Version, blockResponse)
if err != nil {
t.Fatalf("failed to marshal block response: %s", err)
}
return responseBytes
},
expectedErr: "failed to unmarshal response: rlp: expected input list",
},
"incorrect starting point": {
request: message.BlockRequest{
Hash: blocks[100].Hash(),
Height: 100,
Parents: 16,
},
getResponse: func(t *testing.T, _ message.BlockRequest) []byte {
response, err := blocksRequestHandler.OnBlockRequest(context.Background(), ids.GenerateTestNodeID(), 1, message.BlockRequest{
Hash: blocks[99].Hash(),
Height: 99,
Parents: 16,
})
if err != nil {
t.Fatal(err)
}
if len(response) == 0 {
t.Fatal("Failed to generate valid response")
}
return response
},
expectedErr: errHashMismatch.Error(),
},
"missing link in between blocks": {
request: message.BlockRequest{
Hash: blocks[100].Hash(),
Height: 100,
Parents: 16,
},
getResponse: func(t *testing.T, request message.BlockRequest) []byte {
// Encode blocks with a missing link
blks := make([]*types.Block, 0)
blks = append(blks, blocks[84:89]...)
blks = append(blks, blocks[90:101]...)
blockBytes := encodeBlockSlice(blks)
blockResponse := message.BlockResponse{
Blocks: blockBytes,
}
responseBytes, err := message.Codec.Marshal(message.Version, blockResponse)
if err != nil {
t.Fatalf("failed to marshal block response: %s", err)
}
return responseBytes
},
expectedErr: errHashMismatch.Error(),
},
"no blocks": {
request: message.BlockRequest{
Hash: blocks[100].Hash(),
Height: 100,
Parents: 16,
},
getResponse: func(t *testing.T, request message.BlockRequest) []byte {
blockResponse := message.BlockResponse{
Blocks: nil,
}
responseBytes, err := message.Codec.Marshal(message.Version, blockResponse)
if err != nil {
t.Fatalf("failed to marshal block response: %s", err)
}
return responseBytes
},
expectedErr: errEmptyResponse.Error(),
},
"more than requested blocks": {
request: message.BlockRequest{
Hash: blocks[100].Hash(),
Height: 100,
Parents: 16,
},
getResponse: func(t *testing.T, request message.BlockRequest) []byte {
blockBytes := encodeBlockSlice(blocks[80:100])
blockResponse := message.BlockResponse{
Blocks: blockBytes,
}
responseBytes, err := message.Codec.Marshal(message.Version, blockResponse)
if err != nil {
t.Fatalf("failed to marshal block response: %s", err)
}
return responseBytes
},
expectedErr: errTooManyBlocks.Error(),
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
responseBytes := test.getResponse(t, test.request)
if len(test.expectedErr) == 0 {
mockNetClient.mockResponse(1, nil, responseBytes)
} else {
attempted := false
mockNetClient.mockResponse(2, func() {
if attempted {
cancel()
}
attempted = true
}, responseBytes)
}
blockResponse, err := stateSyncClient.GetBlocks(ctx, test.request.Hash, test.request.Height, test.request.Parents)
if len(test.expectedErr) != 0 {
if err == nil {
t.Fatalf("Expected error: %s, but found no error", test.expectedErr)
}
assert.ErrorContains(t, err, test.expectedErr)
return
}
if err != nil {
t.Fatal(err)
}
test.assertResponse(t, blockResponse)
})
}
}
func buildGetter(blocks []*types.Block) handlers.BlockProvider {
return &handlers.TestBlockProvider{
GetBlockFn: func(blockHash common.Hash, blockHeight uint64) *types.Block {
requestedBlock := blocks[blockHeight]
if requestedBlock.Hash() != blockHash {
fmt.Printf("ERROR height=%d, hash=%s, parentHash=%s, reqHash=%s\n", blockHeight, blockHash, requestedBlock.ParentHash(), requestedBlock.Hash())
return nil
}
return requestedBlock
},
}
}
func TestGetLeafs(t *testing.T) {
// Test re-enabled for mainnet deployment
rand.Seed(1)
const leafsLimit = 1024
trieDB := triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil)
largeTrieRoot, largeTrieKeys, _ := statesynctest.GenerateTrie(t, trieDB, 100_000, common.HashLength)
smallTrieRoot, _, _ := statesynctest.GenerateTrie(t, trieDB, leafsLimit, common.HashLength)
handler := handlers.NewLeafsRequestHandler(trieDB, nil, message.Codec, handlerstats.NewNoopHandlerStats())
client := NewClient(&ClientConfig{
NetworkClient: &mockNetwork{},
Codec: message.Codec,
Stats: clientstats.NewNoOpStats(),
StateSyncNodeIDs: nil,
BlockParser: mockBlockParser,
})
tests := map[string]struct {
request message.LeafsRequest
getResponse func(t *testing.T, request message.LeafsRequest) []byte
assertResponse func(t *testing.T, response message.LeafsResponse)
expectedErr error
}{
"full response for small (single request) trie": {
request: message.LeafsRequest{
Root: smallTrieRoot,
Start: bytes.Repeat([]byte{0x00}, common.HashLength),
End: bytes.Repeat([]byte{0xff}, common.HashLength),
Limit: leafsLimit,
},
getResponse: func(t *testing.T, request message.LeafsRequest) []byte {
response, err := handler.OnLeafsRequest(context.Background(), ids.GenerateTestNodeID(), 1, request)
if err != nil {
t.Fatal("unexpected error in calling leafs request handler", err)
}
if len(response) == 0 {
t.Fatal("Failed to create valid response")
}
return response
},
assertResponse: func(t *testing.T, response message.LeafsResponse) {
assert.False(t, response.More)
assert.Equal(t, leafsLimit, len(response.Keys))
assert.Equal(t, leafsLimit, len(response.Vals))
},
},
"too many leaves in response": {
request: message.LeafsRequest{
Root: smallTrieRoot,
Start: bytes.Repeat([]byte{0x00}, common.HashLength),
End: bytes.Repeat([]byte{0xff}, common.HashLength),
Limit: leafsLimit / 2,
},
getResponse: func(t *testing.T, request message.LeafsRequest) []byte {
modifiedRequest := request
modifiedRequest.Limit = leafsLimit
response, err := handler.OnLeafsRequest(context.Background(), ids.GenerateTestNodeID(), 1, modifiedRequest)
if err != nil {
t.Fatal("unexpected error in calling leafs request handler", err)
}
if len(response) == 0 {
t.Fatal("Failed to create valid response")
}
return response
},
expectedErr: errTooManyLeaves,
},
"partial response to request for entire trie (full leaf limit)": {
request: message.LeafsRequest{
Root: largeTrieRoot,
Start: bytes.Repeat([]byte{0x00}, common.HashLength),
End: bytes.Repeat([]byte{0xff}, common.HashLength),
Limit: leafsLimit,
},
getResponse: func(t *testing.T, request message.LeafsRequest) []byte {
response, err := handler.OnLeafsRequest(context.Background(), ids.GenerateTestNodeID(), 1, request)
if err != nil {
t.Fatal("unexpected error in calling leafs request handler", err)
}
if len(response) == 0 {
t.Fatal("Failed to create valid response")
}
return response
},
assertResponse: func(t *testing.T, response message.LeafsResponse) {
assert.True(t, response.More)
assert.Equal(t, leafsLimit, len(response.Keys))
assert.Equal(t, leafsLimit, len(response.Vals))
},
},
"partial response to request for middle range of trie (full leaf limit)": {
request: message.LeafsRequest{
Root: largeTrieRoot,
Start: largeTrieKeys[1000],
End: largeTrieKeys[99000],
Limit: leafsLimit,
},
getResponse: func(t *testing.T, request message.LeafsRequest) []byte {
response, err := handler.OnLeafsRequest(context.Background(), ids.GenerateTestNodeID(), 1, request)
if err != nil {
t.Fatal("unexpected error in calling leafs request handler", err)
}
if len(response) == 0 {
t.Fatal("Failed to create valid response")
}
return response
},
assertResponse: func(t *testing.T, response message.LeafsResponse) {
assert.True(t, response.More)
assert.Equal(t, leafsLimit, len(response.Keys))
assert.Equal(t, leafsLimit, len(response.Vals))
},
},
"full response from near end of trie to end of trie (less than leaf limit)": {
request: message.LeafsRequest{
Root: largeTrieRoot,
Start: largeTrieKeys[len(largeTrieKeys)-30], // Set start 30 keys from the end of the large trie
End: bytes.Repeat([]byte{0xff}, common.HashLength),
Limit: leafsLimit,
},
getResponse: func(t *testing.T, request message.LeafsRequest) []byte {
response, err := handler.OnLeafsRequest(context.Background(), ids.GenerateTestNodeID(), 1, request)
if err != nil {
t.Fatal("unexpected error in calling leafs request handler", err)
}
if len(response) == 0 {
t.Fatal("Failed to create valid response")
}
return response
},
assertResponse: func(t *testing.T, response message.LeafsResponse) {
assert.False(t, response.More)
assert.Equal(t, 30, len(response.Keys))
assert.Equal(t, 30, len(response.Vals))
},
},
"full response for intermediate range of trie (less than leaf limit)": {
request: message.LeafsRequest{
Root: largeTrieRoot,
Start: largeTrieKeys[1000], // Set the range for 1000 leafs in an intermediate range of the trie
End: largeTrieKeys[1099], // (inclusive range)
Limit: leafsLimit,
},
getResponse: func(t *testing.T, request message.LeafsRequest) []byte {
response, err := handler.OnLeafsRequest(context.Background(), ids.GenerateTestNodeID(), 1, request)
if err != nil {
t.Fatal("unexpected error in calling leafs request handler", err)
}
if len(response) == 0 {
t.Fatal("Failed to create valid response")
}
return response
},
assertResponse: func(t *testing.T, response message.LeafsResponse) {
assert.True(t, response.More)
assert.Equal(t, 100, len(response.Keys))
assert.Equal(t, 100, len(response.Vals))
},
},
"removed first key in response": {
request: message.LeafsRequest{
Root: largeTrieRoot,
Start: bytes.Repeat([]byte{0x00}, common.HashLength),
End: bytes.Repeat([]byte{0xff}, common.HashLength),
Limit: leafsLimit,
},
getResponse: func(t *testing.T, request message.LeafsRequest) []byte {
response, err := handler.OnLeafsRequest(context.Background(), ids.GenerateTestNodeID(), 1, request)
if err != nil {
t.Fatal("unexpected error in calling leafs request handler", err)
}
if len(response) == 0 {
t.Fatal("Failed to create valid response")
}
var leafResponse message.LeafsResponse
if _, err := message.Codec.Unmarshal(response, &leafResponse); err != nil {
t.Fatal(err)
}
leafResponse.Keys = leafResponse.Keys[1:]
leafResponse.Vals = leafResponse.Vals[1:]
modifiedResponse, err := message.Codec.Marshal(message.Version, leafResponse)
if err != nil {
t.Fatal(err)
}
return modifiedResponse
},
expectedErr: errInvalidRangeProof,
},
"removed first key in response and replaced proof": {
request: message.LeafsRequest{
Root: largeTrieRoot,
Start: bytes.Repeat([]byte{0x00}, common.HashLength),
End: bytes.Repeat([]byte{0xff}, common.HashLength),
Limit: leafsLimit,
},
getResponse: func(t *testing.T, request message.LeafsRequest) []byte {
response, err := handler.OnLeafsRequest(context.Background(), ids.GenerateTestNodeID(), 1, request)
if err != nil {
t.Fatal("unexpected error in calling leafs request handler", err)
}
if len(response) == 0 {
t.Fatal("Failed to create valid response")
}
var leafResponse message.LeafsResponse
if _, err := message.Codec.Unmarshal(response, &leafResponse); err != nil {
t.Fatal(err)
}
modifiedRequest := request
modifiedRequest.Start = leafResponse.Keys[1]
modifiedResponse, err := handler.OnLeafsRequest(context.Background(), ids.GenerateTestNodeID(), 2, modifiedRequest)
if err != nil {
t.Fatal("unexpected error in calling leafs request handler", err)
}
return modifiedResponse
},
expectedErr: errInvalidRangeProof,
},
"removed last key in response": {
request: message.LeafsRequest{
Root: largeTrieRoot,
Start: bytes.Repeat([]byte{0x00}, common.HashLength),
End: bytes.Repeat([]byte{0xff}, common.HashLength),
Limit: leafsLimit,
},
getResponse: func(t *testing.T, request message.LeafsRequest) []byte {
response, err := handler.OnLeafsRequest(context.Background(), ids.GenerateTestNodeID(), 1, request)
if err != nil {
t.Fatal("unexpected error in calling leafs request handler", err)
}
if len(response) == 0 {
t.Fatal("Failed to create valid response")
}
var leafResponse message.LeafsResponse
if _, err := message.Codec.Unmarshal(response, &leafResponse); err != nil {
t.Fatal(err)
}
leafResponse.Keys = leafResponse.Keys[:len(leafResponse.Keys)-2]
leafResponse.Vals = leafResponse.Vals[:len(leafResponse.Vals)-2]
modifiedResponse, err := message.Codec.Marshal(message.Version, leafResponse)
if err != nil {
t.Fatal(err)
}
return modifiedResponse
},
expectedErr: errInvalidRangeProof,
},
"removed key from middle of response": {
request: message.LeafsRequest{
Root: largeTrieRoot,
Start: bytes.Repeat([]byte{0x00}, common.HashLength),
End: bytes.Repeat([]byte{0xff}, common.HashLength),
Limit: leafsLimit,
},
getResponse: func(t *testing.T, request message.LeafsRequest) []byte {
response, err := handler.OnLeafsRequest(context.Background(), ids.GenerateTestNodeID(), 1, request)
if err != nil {
t.Fatal("unexpected error in calling leafs request handler", err)
}
if len(response) == 0 {
t.Fatal("Failed to create valid response")
}
var leafResponse message.LeafsResponse
if _, err := message.Codec.Unmarshal(response, &leafResponse); err != nil {
t.Fatal(err)
}
// Remove middle key-value pair response
leafResponse.Keys = append(leafResponse.Keys[:100], leafResponse.Keys[101:]...)
leafResponse.Vals = append(leafResponse.Vals[:100], leafResponse.Vals[101:]...)
modifiedResponse, err := message.Codec.Marshal(message.Version, leafResponse)
if err != nil {
t.Fatal(err)
}
return modifiedResponse
},
expectedErr: errInvalidRangeProof,
},
"corrupted value in middle of response": {
request: message.LeafsRequest{
Root: largeTrieRoot,
Start: bytes.Repeat([]byte{0x00}, common.HashLength),
End: bytes.Repeat([]byte{0xff}, common.HashLength),
Limit: leafsLimit,
},
getResponse: func(t *testing.T, request message.LeafsRequest) []byte {
response, err := handler.OnLeafsRequest(context.Background(), ids.GenerateTestNodeID(), 1, request)
if err != nil {
t.Fatal("unexpected error in calling leafs request handler", err)
}
if len(response) == 0 {
t.Fatal("Failed to create valid response")
}
var leafResponse message.LeafsResponse
if _, err := message.Codec.Unmarshal(response, &leafResponse); err != nil {
t.Fatal(err)
}
// Remove middle key-value pair response
leafResponse.Vals[100] = []byte("garbage value data")
modifiedResponse, err := message.Codec.Marshal(message.Version, leafResponse)
if err != nil {
t.Fatal(err)
}
return modifiedResponse
},
expectedErr: errInvalidRangeProof,
},
"all proof keys removed from response": {
request: message.LeafsRequest{
Root: largeTrieRoot,
Start: bytes.Repeat([]byte{0x00}, common.HashLength),
End: bytes.Repeat([]byte{0xff}, common.HashLength),
Limit: leafsLimit,
},
getResponse: func(t *testing.T, request message.LeafsRequest) []byte {
response, err := handler.OnLeafsRequest(context.Background(), ids.GenerateTestNodeID(), 1, request)
if err != nil {
t.Fatal("unexpected error in calling leafs request handler", err)
}
if len(response) == 0 {
t.Fatal("Failed to create valid response")
}
var leafResponse message.LeafsResponse
if _, err := message.Codec.Unmarshal(response, &leafResponse); err != nil {
t.Fatal(err)
}
// Remove the proof
leafResponse.ProofVals = nil
modifiedResponse, err := message.Codec.Marshal(message.Version, leafResponse)
if err != nil {
t.Fatal(err)
}
return modifiedResponse
},
expectedErr: errInvalidRangeProof,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
responseBytes := test.getResponse(t, test.request)
response, _, err := parseLeafsResponse(client.codec, test.request, responseBytes)
if test.expectedErr != nil {
if err == nil {
t.Fatalf("Expected error: %s, but found no error", test.expectedErr)
}
assert.True(t, strings.Contains(err.Error(), test.expectedErr.Error()))
return
}
if err != nil {
t.Fatal(err)
}
leafsResponse, ok := response.(message.LeafsResponse)
if !ok {
t.Fatalf("parseLeafsResponse returned incorrect type %T", response)
}
test.assertResponse(t, leafsResponse)
})
}
}
func TestGetLeafsRetries(t *testing.T) {
// Test re-enabled for mainnet deployment
rand.Seed(1)
trieDB := triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil)
root, _, _ := statesynctest.GenerateTrie(t, trieDB, 100_000, common.HashLength)
handler := handlers.NewLeafsRequestHandler(trieDB, nil, message.Codec, handlerstats.NewNoopHandlerStats())
mockNetClient := &mockNetwork{}
const maxAttempts = 8
client := NewClient(&ClientConfig{
NetworkClient: mockNetClient,
Codec: message.Codec,
Stats: clientstats.NewNoOpStats(),
StateSyncNodeIDs: nil,
BlockParser: mockBlockParser,
})
request := message.LeafsRequest{
Root: root,
Start: bytes.Repeat([]byte{0x00}, common.HashLength),
End: bytes.Repeat([]byte{0xff}, common.HashLength),
Limit: 1024,
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
goodResponse, responseErr := handler.OnLeafsRequest(ctx, ids.GenerateTestNodeID(), 1, request)
assert.NoError(t, responseErr)
mockNetClient.mockResponse(1, nil, goodResponse)
res, err := client.GetLeafs(ctx, request)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, 1024, len(res.Keys))
assert.Equal(t, 1024, len(res.Vals))
// Succeeds within the allotted number of attempts
invalidResponse := []byte("invalid response")
mockNetClient.mockResponses(nil, invalidResponse, invalidResponse, goodResponse)
res, err = client.GetLeafs(ctx, request)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, 1024, len(res.Keys))
assert.Equal(t, 1024, len(res.Vals))
// Test that GetLeafs stops after the context is cancelled
numAttempts := 0
mockNetClient.mockResponse(maxAttempts, func() {
numAttempts++
if numAttempts >= maxAttempts {
cancel()
}
}, invalidResponse)
_, err = client.GetLeafs(ctx, request)
assert.Error(t, err)
assert.True(t, strings.Contains(err.Error(), context.Canceled.Error()))
}
func TestStateSyncNodes(t *testing.T) {
// Test re-enabled for mainnet deployment
mockNetClient := &mockNetwork{}
stateSyncNodes := []ids.NodeID{
ids.GenerateTestNodeID(),
ids.GenerateTestNodeID(),
ids.GenerateTestNodeID(),
ids.GenerateTestNodeID(),
}
client := NewClient(&ClientConfig{
NetworkClient: mockNetClient,
Codec: message.Codec,
Stats: clientstats.NewNoOpStats(),
StateSyncNodeIDs: stateSyncNodes,
BlockParser: mockBlockParser,
})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
attempt := 0
responses := [][]byte{{1}, {2}, {3}, {4}}
mockNetClient.mockResponses(func() {
attempt++
if attempt >= 4 {
cancel()
}
}, responses...)
// send some request, doesn't matter what it is because we're testing the interaction with state sync nodes here
response, err := client.GetLeafs(ctx, message.LeafsRequest{})
assert.Error(t, err)
assert.Empty(t, response)
// assert all nodes were called
assert.Contains(t, mockNetClient.nodesRequested, stateSyncNodes[0])
assert.Contains(t, mockNetClient.nodesRequested, stateSyncNodes[1])
assert.Contains(t, mockNetClient.nodesRequested, stateSyncNodes[2])
assert.Contains(t, mockNetClient.nodesRequested, stateSyncNodes[3])
}