Files
evm/sync/client/client_test.go
2024-01-03 09:44:09 +01:00

880 lines
28 KiB
Go

// (c) 2021-2022, Lux Partners Limited. 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/luxdefi/node/ids"
"github.com/luxdefi/evm/consensus/dummy"
"github.com/luxdefi/evm/core"
"github.com/luxdefi/evm/core/types"
"github.com/luxdefi/evm/ethdb/memorydb"
"github.com/luxdefi/evm/params"
"github.com/luxdefi/evm/plugin/evm/message"
clientstats "github.com/luxdefi/evm/sync/client/stats"
"github.com/luxdefi/evm/sync/handlers"
handlerstats "github.com/luxdefi/evm/sync/handlers/stats"
"github.com/luxdefi/evm/trie"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)
func TestGetCode(t *testing.T) {
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{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, params.MaxCodeSize+1)
codeHash := crypto.Keccak256Hash(oversizedCode)
return []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) {
// set random seed for deterministic tests
rand.Seed(1)
var gspec = &core.Genesis{
Config: params.TestChainConfig,
}
memdb := memorydb.New()
genesis := gspec.MustCommit(memdb)
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 for types.extblock",
},
"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.True(t, strings.Contains(err.Error(), test.expectedErr), "expected error to contain [%s], but found [%s]", test.expectedErr, err)
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) {
rand.Seed(1)
const leafsLimit = 1024
trieDB := trie.NewDatabase(memorydb.New())
largeTrieRoot, largeTrieKeys, _ := trie.GenerateTrie(t, trieDB, 100_000, common.HashLength)
smallTrieRoot, _, _ := trie.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) {
rand.Seed(1)
trieDB := trie.NewDatabase(memorydb.New())
root, _, _ := trie.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) {
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])
}