From 4c562502221e5a615db6b063c27254a6042739ca Mon Sep 17 00:00:00 2001 From: Tsvetan Dimitrov Date: Tue, 14 Oct 2025 13:09:52 +0300 Subject: [PATCH 1/6] chore(sync/message): migrate message package for EVM state sync from `coreth` - Introduce vms/evm/sync/message with request/response types: - BlockRequest/Response, LeafsRequest/Response, CodeRequest/Response. - Add Request and RequestHandler interfaces and wiring. - Add codec setup with versioning and max message size. - Add BlockSyncSummary and parser for sync bootstrap. - Add golden serialization tests and table-driven tests for handler dispatch and interface round-trips. resolves #4416 Signed-off-by: Tsvetan Dimitrov (tsvetan.dimitrov@avalabs.org) --- vms/evm/sync/message/block_request.go | 41 ++++ vms/evm/sync/message/block_request_test.go | 65 +++++++ vms/evm/sync/message/block_sync_summary.go | 82 ++++++++ .../sync/message/block_sync_summary_parser.go | 34 ++++ .../message/block_sync_summary_provider.go | 17 ++ .../sync/message/block_sync_summary_test.go | 45 +++++ vms/evm/sync/message/code_request.go | 48 +++++ vms/evm/sync/message/code_request_test.go | 58 ++++++ vms/evm/sync/message/codec.go | 46 +++++ vms/evm/sync/message/handler.go | 45 +++++ vms/evm/sync/message/leafs_request.go | 79 ++++++++ vms/evm/sync/message/leafs_request_test.go | 106 ++++++++++ vms/evm/sync/message/request.go | 27 +++ vms/evm/sync/message/request_test.go | 184 ++++++++++++++++++ vms/evm/sync/message/syncable.go | 22 +++ 15 files changed, 899 insertions(+) create mode 100644 vms/evm/sync/message/block_request.go create mode 100644 vms/evm/sync/message/block_request_test.go create mode 100644 vms/evm/sync/message/block_sync_summary.go create mode 100644 vms/evm/sync/message/block_sync_summary_parser.go create mode 100644 vms/evm/sync/message/block_sync_summary_provider.go create mode 100644 vms/evm/sync/message/block_sync_summary_test.go create mode 100644 vms/evm/sync/message/code_request.go create mode 100644 vms/evm/sync/message/code_request_test.go create mode 100644 vms/evm/sync/message/codec.go create mode 100644 vms/evm/sync/message/handler.go create mode 100644 vms/evm/sync/message/leafs_request.go create mode 100644 vms/evm/sync/message/leafs_request_test.go create mode 100644 vms/evm/sync/message/request.go create mode 100644 vms/evm/sync/message/request_test.go create mode 100644 vms/evm/sync/message/syncable.go diff --git a/vms/evm/sync/message/block_request.go b/vms/evm/sync/message/block_request.go new file mode 100644 index 000000000000..f14bc88992cd --- /dev/null +++ b/vms/evm/sync/message/block_request.go @@ -0,0 +1,41 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package message + +import ( + "context" + "fmt" + + "github.com/ava-labs/libevm/common" + + "github.com/ava-labs/avalanchego/ids" +) + +var _ Request = (*BlockRequest)(nil) + +// BlockRequest is a request to retrieve Parents number of blocks starting from Hash from newest-oldest manner +type BlockRequest struct { + Hash common.Hash `serialize:"true"` + Height uint64 `serialize:"true"` + Parents uint16 `serialize:"true"` +} + +func (b BlockRequest) String() string { + return fmt.Sprintf( + "BlockRequest(Hash=%s, Height=%d, Parents=%d)", + b.Hash, b.Height, b.Parents, + ) +} + +func (b BlockRequest) Handle(ctx context.Context, nodeID ids.NodeID, requestID uint32, handler RequestHandler) ([]byte, error) { + return handler.HandleBlockRequest(ctx, nodeID, requestID, b) +} + +// BlockResponse is a response to a BlockRequest +// Blocks is slice of RLP encoded blocks starting with the block +// requested in BlockRequest.Hash. The next block is the parent, etc. +// handler: handlers.BlockRequestHandler +type BlockResponse struct { + Blocks [][]byte `serialize:"true"` +} diff --git a/vms/evm/sync/message/block_request_test.go b/vms/evm/sync/message/block_request_test.go new file mode 100644 index 000000000000..92420bd5d5d7 --- /dev/null +++ b/vms/evm/sync/message/block_request_test.go @@ -0,0 +1,65 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package message + +import ( + "encoding/base64" + "math/rand" + "testing" + + "github.com/ava-labs/libevm/common" + "github.com/stretchr/testify/require" +) + +// TestMarshalBlockRequest requires that the structure or serialization logic hasn't changed, primarily to +// ensure compatibility with the network. +func TestMarshalBlockRequest(t *testing.T) { + blockRequest := BlockRequest{ + Hash: common.BytesToHash([]byte("some hash is here yo")), + Height: 1337, + Parents: 64, + } + + base64BlockRequest := "AAAAAAAAAAAAAAAAAABzb21lIGhhc2ggaXMgaGVyZSB5bwAAAAAAAAU5AEA=" + + blockRequestBytes, err := Codec.Marshal(Version, blockRequest) + require.NoError(t, err) + require.Equal(t, base64BlockRequest, base64.StdEncoding.EncodeToString(blockRequestBytes)) + + var b BlockRequest + _, err = Codec.Unmarshal(blockRequestBytes, &b) + require.NoError(t, err) + require.Equal(t, blockRequest.Hash, b.Hash) + require.Equal(t, blockRequest.Height, b.Height) + require.Equal(t, blockRequest.Parents, b.Parents) +} + +// TestMarshalBlockResponse requires that the structure or serialization logic hasn't changed, primarily to +// ensure compatibility with the network. +func TestMarshalBlockResponse(t *testing.T) { + // create some random bytes + // set seed to ensure deterministic random behaviour + r := rand.New(rand.NewSource(1)) //nolint:gosec // deterministic bytes for golden assertion + blocksBytes := make([][]byte, 32) + for i := range blocksBytes { + blocksBytes[i] = make([]byte, r.Intn(32)+32) + _, err := r.Read(blocksBytes[i]) + require.NoError(t, err) + } + + blockResponse := BlockResponse{ + Blocks: blocksBytes, + } + + base64BlockResponse := "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" + + blockResponseBytes, err := Codec.Marshal(Version, blockResponse) + require.NoError(t, err) + require.Equal(t, base64BlockResponse, base64.StdEncoding.EncodeToString(blockResponseBytes)) + + var b BlockResponse + _, err = Codec.Unmarshal(blockResponseBytes, &b) + require.NoError(t, err) + require.Equal(t, blockResponse.Blocks, b.Blocks) +} diff --git a/vms/evm/sync/message/block_sync_summary.go b/vms/evm/sync/message/block_sync_summary.go new file mode 100644 index 000000000000..aedb630e19d3 --- /dev/null +++ b/vms/evm/sync/message/block_sync_summary.go @@ -0,0 +1,82 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package message + +import ( + "context" + "fmt" + + "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/crypto" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" +) + +var _ Syncable = (*BlockSyncSummary)(nil) + +// BlockSyncSummary provides the information necessary to sync a node starting +// at the given block. +type BlockSyncSummary struct { + BlockNumber uint64 `serialize:"true"` + BlockHash common.Hash `serialize:"true"` + BlockRoot common.Hash `serialize:"true"` + + summaryID ids.ID + bytes []byte + acceptImpl AcceptImplFn +} + +func NewBlockSyncSummary(blockHash common.Hash, blockNumber uint64, blockRoot common.Hash) (*BlockSyncSummary, error) { + // We intentionally do not use the acceptImpl here and leave it for the parser to set. + summary := BlockSyncSummary{ + BlockNumber: blockNumber, + BlockHash: blockHash, + BlockRoot: blockRoot, + } + bytes, err := Codec.Marshal(Version, &summary) + if err != nil { + return nil, fmt.Errorf("failed to marshal syncable summary: %w", err) + } + + summary.bytes = bytes + summaryID, err := ids.ToID(crypto.Keccak256(bytes)) + if err != nil { + return nil, fmt.Errorf("failed to compute summary ID: %w", err) + } + summary.summaryID = summaryID + + return &summary, nil +} + +func (s *BlockSyncSummary) GetBlockHash() common.Hash { + return s.BlockHash +} + +func (s *BlockSyncSummary) GetBlockRoot() common.Hash { + return s.BlockRoot +} + +func (s *BlockSyncSummary) Bytes() []byte { + return s.bytes +} + +func (s *BlockSyncSummary) Height() uint64 { + return s.BlockNumber +} + +func (s *BlockSyncSummary) ID() ids.ID { + return s.summaryID +} + +func (s *BlockSyncSummary) String() string { + return fmt.Sprintf("BlockSyncSummary(BlockHash=%s, BlockNumber=%d, BlockRoot=%s)", s.BlockHash, s.BlockNumber, s.BlockRoot) +} + +func (s *BlockSyncSummary) Accept(context.Context) (block.StateSyncMode, error) { + if s.acceptImpl == nil { + return block.StateSyncSkipped, fmt.Errorf("accept implementation not specified for summary: %s", s) + } + return s.acceptImpl(s) +} diff --git a/vms/evm/sync/message/block_sync_summary_parser.go b/vms/evm/sync/message/block_sync_summary_parser.go new file mode 100644 index 000000000000..abeed28d36c2 --- /dev/null +++ b/vms/evm/sync/message/block_sync_summary_parser.go @@ -0,0 +1,34 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package message + +import ( + "fmt" + + "github.com/ava-labs/libevm/crypto" + + "github.com/ava-labs/avalanchego/ids" +) + +type BlockSyncSummaryParser struct{} + +func NewBlockSyncSummaryParser() *BlockSyncSummaryParser { + return &BlockSyncSummaryParser{} +} + +func (*BlockSyncSummaryParser) Parse(summaryBytes []byte, acceptImpl AcceptImplFn) (Syncable, error) { + summary := BlockSyncSummary{} + if _, err := Codec.Unmarshal(summaryBytes, &summary); err != nil { + return nil, fmt.Errorf("failed to parse syncable summary: %w", err) + } + + summary.bytes = summaryBytes + summaryID, err := ids.ToID(crypto.Keccak256(summaryBytes)) + if err != nil { + return nil, fmt.Errorf("failed to compute summary ID: %w", err) + } + summary.summaryID = summaryID + summary.acceptImpl = acceptImpl + return &summary, nil +} diff --git a/vms/evm/sync/message/block_sync_summary_provider.go b/vms/evm/sync/message/block_sync_summary_provider.go new file mode 100644 index 000000000000..98eece22b0e6 --- /dev/null +++ b/vms/evm/sync/message/block_sync_summary_provider.go @@ -0,0 +1,17 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package message + +import ( + "github.com/ava-labs/libevm/core/types" + + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" +) + +type BlockSyncSummaryProvider struct{} + +// StateSummaryAtBlock returns the block state summary at [block] if valid. +func (*BlockSyncSummaryProvider) StateSummaryAtBlock(blk *types.Block) (block.StateSummary, error) { + return NewBlockSyncSummary(blk.Hash(), blk.NumberU64(), blk.Root()) +} diff --git a/vms/evm/sync/message/block_sync_summary_test.go b/vms/evm/sync/message/block_sync_summary_test.go new file mode 100644 index 000000000000..5321a14b6f70 --- /dev/null +++ b/vms/evm/sync/message/block_sync_summary_test.go @@ -0,0 +1,45 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package message + +import ( + "context" + "encoding/base64" + "testing" + + "github.com/ava-labs/libevm/common" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" +) + +func TestMarshalBlockSyncSummary(t *testing.T) { + blockSyncSummary, err := NewBlockSyncSummary(common.Hash{1}, 2, common.Hash{3}) + require.NoError(t, err) + + require.Equal(t, common.Hash{1}, blockSyncSummary.GetBlockHash()) + require.Equal(t, uint64(2), blockSyncSummary.Height()) + require.Equal(t, common.Hash{3}, blockSyncSummary.GetBlockRoot()) + + expectedBase64Bytes := "AAAAAAAAAAAAAgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + require.Equal(t, expectedBase64Bytes, base64.StdEncoding.EncodeToString(blockSyncSummary.Bytes())) + + parser := NewBlockSyncSummaryParser() + called := false + acceptImplTest := func(Syncable) (block.StateSyncMode, error) { + called = true + return block.StateSyncSkipped, nil + } + s, err := parser.Parse(blockSyncSummary.Bytes(), acceptImplTest) + require.NoError(t, err) + require.Equal(t, blockSyncSummary.GetBlockHash(), s.GetBlockHash()) + require.Equal(t, blockSyncSummary.Height(), s.Height()) + require.Equal(t, blockSyncSummary.GetBlockRoot(), s.GetBlockRoot()) + require.Equal(t, blockSyncSummary.Bytes(), s.Bytes()) + + mode, err := s.Accept(context.TODO()) + require.NoError(t, err) + require.Equal(t, block.StateSyncSkipped, mode) + require.True(t, called) +} diff --git a/vms/evm/sync/message/code_request.go b/vms/evm/sync/message/code_request.go new file mode 100644 index 000000000000..be59623c4351 --- /dev/null +++ b/vms/evm/sync/message/code_request.go @@ -0,0 +1,48 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package message + +import ( + "context" + "fmt" + "strings" + + "github.com/ava-labs/libevm/common" + + "github.com/ava-labs/avalanchego/ids" +) + +var _ Request = CodeRequest{} + +// CodeRequest is a request to retrieve a contract code with specified Hash +type CodeRequest struct { + // Hashes is a list of contract code hashes + Hashes []common.Hash `serialize:"true"` +} + +func (c CodeRequest) String() string { + hashStrs := make([]string, len(c.Hashes)) + for i, hash := range c.Hashes { + hashStrs[i] = hash.String() + } + return fmt.Sprintf("CodeRequest(Hashes=%s)", strings.Join(hashStrs, ", ")) +} + +func (c CodeRequest) Handle(ctx context.Context, nodeID ids.NodeID, requestID uint32, handler RequestHandler) ([]byte, error) { + return handler.HandleCodeRequest(ctx, nodeID, requestID, c) +} + +func NewCodeRequest(hashes []common.Hash) CodeRequest { + return CodeRequest{ + Hashes: hashes, + } +} + +// CodeResponse is a response to a CodeRequest +// crypto.Keccak256Hash of each element in Data is expected to equal +// the corresponding element in CodeRequest.Hashes +// handler: handlers.CodeRequestHandler +type CodeResponse struct { + Data [][]byte `serialize:"true"` +} diff --git a/vms/evm/sync/message/code_request_test.go b/vms/evm/sync/message/code_request_test.go new file mode 100644 index 000000000000..a4e823f84d84 --- /dev/null +++ b/vms/evm/sync/message/code_request_test.go @@ -0,0 +1,58 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package message + +import ( + "encoding/base64" + "math/rand" + "testing" + + "github.com/ava-labs/libevm/common" + "github.com/stretchr/testify/require" +) + +// TestMarshalCodeRequest requires that the structure or serialization logic hasn't changed, primarily to +// ensure compatibility with the network. +func TestMarshalCodeRequest(t *testing.T) { + codeRequest := CodeRequest{ + Hashes: []common.Hash{common.BytesToHash([]byte("some code pls"))}, + } + + base64CodeRequest := "AAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAHNvbWUgY29kZSBwbHM=" + + codeRequestBytes, err := Codec.Marshal(Version, codeRequest) + require.NoError(t, err) + require.Equal(t, base64CodeRequest, base64.StdEncoding.EncodeToString(codeRequestBytes)) + + var c CodeRequest + _, err = Codec.Unmarshal(codeRequestBytes, &c) + require.NoError(t, err) + require.Equal(t, codeRequest.Hashes, c.Hashes) +} + +// TestMarshalCodeResponse requires that the structure or serialization logic hasn't changed, primarily to +// ensure compatibility with the network. +func TestMarshalCodeResponse(t *testing.T) { + // generate some random code data + // set random seed for deterministic random + codeData := make([]byte, 50) + r := rand.New(rand.NewSource(1)) //nolint:gosec // deterministic bytes for golden assertion + _, err := r.Read(codeData) + require.NoError(t, err) + + codeResponse := CodeResponse{ + Data: [][]byte{codeData}, + } + + base64CodeResponse := "AAAAAAABAAAAMlL9/AchgmVPFj9fD5piHXKVZsdNEAN8TXu7BAfR4sZJgYVa2GgdDYbR6R4AFnk5y2aU" + + codeResponseBytes, err := Codec.Marshal(Version, codeResponse) + require.NoError(t, err) + require.Equal(t, base64CodeResponse, base64.StdEncoding.EncodeToString(codeResponseBytes)) + + var c CodeResponse + _, err = Codec.Unmarshal(codeResponseBytes, &c) + require.NoError(t, err) + require.Equal(t, codeResponse.Data, c.Data) +} diff --git a/vms/evm/sync/message/codec.go b/vms/evm/sync/message/codec.go new file mode 100644 index 000000000000..aee9a70775d1 --- /dev/null +++ b/vms/evm/sync/message/codec.go @@ -0,0 +1,46 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package message + +import ( + "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/codec/linearcodec" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/utils/wrappers" +) + +const ( + Version = uint16(0) + maxMessageSize = 2*units.MiB - 64*units.KiB // Subtract 64 KiB from p2p network cap to leave room for encoding overhead from AvalancheGo +) + +var Codec codec.Manager + +func init() { + Codec = codec.NewManager(maxMessageSize) + c := linearcodec.NewDefault() + + errs := wrappers.Errs{} + // Gossip types and sync summary type removed from codec + c.SkipRegistrations(3) + errs.Add( + // state sync types + c.RegisterType(BlockRequest{}), + c.RegisterType(BlockResponse{}), + c.RegisterType(LeafsRequest{}), + c.RegisterType(LeafsResponse{}), + c.RegisterType(CodeRequest{}), + c.RegisterType(CodeResponse{}), + ) + + // Deprecated Warp request/responde types are skipped + // See https://github.com/ava-labs/coreth/pull/999 + c.SkipRegistrations(3) + + errs.Add(Codec.RegisterCodec(Version, c)) + + if errs.Errored() { + panic(errs.Err) + } +} diff --git a/vms/evm/sync/message/handler.go b/vms/evm/sync/message/handler.go new file mode 100644 index 000000000000..42a5319249c9 --- /dev/null +++ b/vms/evm/sync/message/handler.go @@ -0,0 +1,45 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package message + +import ( + "context" + + "github.com/ava-labs/avalanchego/ids" +) + +var _ RequestHandler = NoopRequestHandler{} + +// RequestHandler interface handles incoming requests from peers +// Must have methods in format of handleType(context.Context, ids.NodeID, uint32, request Type) error +// so that the Request object of relevant Type can invoke its respective handle method +// on this struct. +type RequestHandler interface { + HandleLeafsRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, leafsRequest LeafsRequest) ([]byte, error) + HandleBlockRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, request BlockRequest) ([]byte, error) + HandleCodeRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, codeRequest CodeRequest) ([]byte, error) +} + +// ResponseHandler handles response for a sent request +// Only one of OnResponse or OnFailure is called for a given requestID, not both +type ResponseHandler interface { + // OnResponse is invoked when the peer responded to a request + OnResponse(response []byte) error + // OnFailure is invoked when there was a failure in processing a request + OnFailure() error +} + +type NoopRequestHandler struct{} + +func (NoopRequestHandler) HandleLeafsRequest(context.Context, ids.NodeID, uint32, LeafsRequest) ([]byte, error) { + return nil, nil +} + +func (NoopRequestHandler) HandleBlockRequest(context.Context, ids.NodeID, uint32, BlockRequest) ([]byte, error) { + return nil, nil +} + +func (NoopRequestHandler) HandleCodeRequest(context.Context, ids.NodeID, uint32, CodeRequest) ([]byte, error) { + return nil, nil +} diff --git a/vms/evm/sync/message/leafs_request.go b/vms/evm/sync/message/leafs_request.go new file mode 100644 index 000000000000..6dced30e392a --- /dev/null +++ b/vms/evm/sync/message/leafs_request.go @@ -0,0 +1,79 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package message + +import ( + "context" + "fmt" + + "github.com/ava-labs/libevm/common" + + "github.com/ava-labs/avalanchego/ids" +) + +// MaxCodeHashesPerRequest limits the number of code hashes per request to bound +// bandwidth and CPU for verification. +const MaxCodeHashesPerRequest = 5 + +var _ Request = LeafsRequest{} + +// NodeType outlines the trie that a leaf node belongs to +// handlers.LeafsRequestHandler uses this information to determine +// which trie type to fetch the information from +type NodeType uint8 + +const ( + StateTrieNode = NodeType(1) + StateTrieKeyLength = common.HashLength +) + +// LeafsRequest is a request to receive trie leaves at specified Root within Start and End byte range +// Limit outlines maximum number of leaves to returns starting at Start +// NodeType outlines which trie to read from state/atomic. +type LeafsRequest struct { + Root common.Hash `serialize:"true"` + Account common.Hash `serialize:"true"` + Start []byte `serialize:"true"` + End []byte `serialize:"true"` + Limit uint16 `serialize:"true"` + NodeType NodeType `serialize:"true"` +} + +func (l LeafsRequest) String() string { + return fmt.Sprintf( + "LeafsRequest(Root=%s, Account=%s, Start=%s, End=%s, Limit=%d, NodeType=%d)", + l.Root, l.Account, common.Bytes2Hex(l.Start), common.Bytes2Hex(l.End), l.Limit, l.NodeType, + ) +} + +func (l LeafsRequest) Handle(ctx context.Context, nodeID ids.NodeID, requestID uint32, handler RequestHandler) ([]byte, error) { + return handler.HandleLeafsRequest(ctx, nodeID, requestID, l) +} + +// LeafsResponse is a response to a LeafsRequest +// Keys must be within LeafsRequest.Start and LeafsRequest.End and sorted in lexicographical order. +// +// ProofVals must be non-empty and contain a valid range proof unless the key-value pairs in the +// response are the entire trie. +// If the key-value pairs make up the entire trie, ProofVals should be empty since the root will be +// sufficient to prove that the leaves are included in the trie. +// +// More is a flag set in the client after verifying the response, which indicates if the last key-value +// pair in the response has any more elements to its right within the trie. +type LeafsResponse struct { + // Keys and Vals provides the key-value pairs in the trie in the response. + Keys [][]byte `serialize:"true"` + Vals [][]byte `serialize:"true"` + + // More indicates if there are more leaves to the right of the last value in this response. + // + // This is not serialized since it is set in the client after verifying the response via + // VerifyRangeProof and determining if there are in fact more leaves to the right of the + // last value in this response. + More bool + + // ProofVals contain the edge merkle-proofs for the range of keys included in the response. + // The keys for the proof are simply the keccak256 hashes of the values, so they are not included in the response to save bandwidth. + ProofVals [][]byte `serialize:"true"` +} diff --git a/vms/evm/sync/message/leafs_request_test.go b/vms/evm/sync/message/leafs_request_test.go new file mode 100644 index 000000000000..2e168c915bc2 --- /dev/null +++ b/vms/evm/sync/message/leafs_request_test.go @@ -0,0 +1,106 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package message + +import ( + "encoding/base64" + "math/rand" + "testing" + + "github.com/ava-labs/libevm/common" + "github.com/stretchr/testify/require" +) + +// TestMarshalLeafsRequest requires that the structure or serialization logic hasn't changed, primarily to +// ensure compatibility with the network. +func TestMarshalLeafsRequest(t *testing.T) { + startBytes := make([]byte, common.HashLength) + endBytes := make([]byte, common.HashLength) + + r := rand.New(rand.NewSource(1)) //nolint:gosec // deterministic bytes for golden assertion + _, err := r.Read(startBytes) + require.NoError(t, err) + _, err = r.Read(endBytes) + require.NoError(t, err) + require.NotEmpty(t, startBytes) + require.NotEmpty(t, endBytes) + + leafsRequest := LeafsRequest{ + Root: common.BytesToHash([]byte("im ROOTing for ya")), + Start: startBytes, + End: endBytes, + Limit: 1024, + NodeType: StateTrieNode, + } + + base64LeafsRequest := "AAAAAAAAAAAAAAAAAAAAAABpbSBST09UaW5nIGZvciB5YQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIFL9/AchgmVPFj9fD5piHXKVZsdNEAN8TXu7BAfR4sZJAAAAIIGFWthoHQ2G0ekeABZ5OctmlNLEIqzSCKAHKTlIf2mZBAAB" + + leafsRequestBytes, err := Codec.Marshal(Version, leafsRequest) + require.NoError(t, err) + require.Equal(t, base64LeafsRequest, base64.StdEncoding.EncodeToString(leafsRequestBytes)) + + var l LeafsRequest + _, err = Codec.Unmarshal(leafsRequestBytes, &l) + require.NoError(t, err) + require.Equal(t, leafsRequest.Root, l.Root) + require.Equal(t, leafsRequest.Start, l.Start) + require.Equal(t, leafsRequest.End, l.End) + require.Equal(t, leafsRequest.Limit, l.Limit) + require.Equal(t, leafsRequest.NodeType, l.NodeType) +} + +// TestMarshalLeafsResponse requires that the structure or serialization logic hasn't changed, primarily to +// ensure compatibility with the network. +func TestMarshalLeafsResponse(t *testing.T) { + keysBytes := make([][]byte, 16) + valsBytes := make([][]byte, 16) + r := rand.New(rand.NewSource(1)) //nolint:gosec // deterministic bytes for golden assertion + for i := range keysBytes { + keysBytes[i] = make([]byte, common.HashLength) + n := r.Intn(8) + valsBytes[i] = make([]byte, n+8) + _, err := r.Read(keysBytes[i]) + require.NoError(t, err) + + _, err = r.Read(valsBytes[i]) + require.NoError(t, err) + } + + nextKey := make([]byte, common.HashLength) + if _, e := r.Read(nextKey); e != nil { + require.NoError(t, e) + } + require.NotEmpty(t, nextKey) + + proofVals := make([][]byte, 4) + r2 := rand.New(rand.NewSource(2)) //nolint:gosec // deterministic bytes for golden assertion + for i := range proofVals { + n := r2.Intn(8) + proofVals[i] = make([]byte, n+8) + if _, e := r2.Read(proofVals[i]); e != nil { + require.NoError(t, e) + } + } + + leafsResponse := LeafsResponse{ + Keys: keysBytes, + Vals: valsBytes, + More: true, + ProofVals: proofVals, + } + + base64LeafsResponse := "AAAAAAAQAAAAIE8WP18PmmIdcpVmx00QA3xNe7sEB9HixkmBhVrYaB0NAAAAIGagByk5SH9pmeudGKRHhARdh/PGfPInRumVr1olNnlRAAAAIK2zfFghtmgLTnyLdjobHUnUlVyEhiFjJSU/7HON16niAAAAIIYVu9oIMfUFmHWSHmaKW98sf8SERZLSVyvNBmjS1sUvAAAAIHHb2Wiw9xcu2FeUuzWLDDtSXaF4b5//CUJ52xlE69ehAAAAIPhMiSs77qX090OR9EXRWv1ClAQDdPaSS5jL+HE/jZYtAAAAIMr8yuOmvI+effHZKTM/+ZOTO+pvWzr23gN0NmxHGeQ6AAAAIBZZpE856x5YScYHfbtXIvVxeiiaJm+XZHmBmY6+qJwLAAAAIHOq53hmZ/fpNs1PJKv334ZrqlYDg2etYUXeHuj0qLCZAAAAIHiN5WOvpGfUnexqQOmh0AfwM8KCMGG90Oqln45NpkMBAAAAIKAQ13yW6oCnpmX2BvamO389/SVnwYl55NYPJmhtm/L7AAAAIAfuKbpk+Eq0PKDG5rkcH9O+iZBDQXnTr0SRo2kBLbktAAAAILsXyQKL6ZFOt2ScbJNHgAl50YMDVvKlTD3qsqS0R11jAAAAIOqxOTXzHYRIRRfpJK73iuFRwAdVklg2twdYhWUMMOwpAAAAIHnqPf5BNqv3UrO4Jx0D6USzyds2a3UEX479adIq5UEZAAAAIDLWEMqsbjP+qjJjo5lDcCS6nJsUZ4onTwGpEK4pX277AAAAEAAAAAmG0ekeABZ5OcsAAAAMuqL/bNRxxIPxX7kLAAAACov5IRGcFg8HAkQAAAAIUFTi0INr+EwAAAAOnQ97usvgJVqlt9RL7EAAAAAJfI0BkZLCQiTiAAAACxsGfYm8fwHx9XOYAAAADUs3OXARXoLtb0ElyPoAAAAKPr34iDoK2L6cOQAAAAoFIg0LKWiLc0uOAAAACCbJAf81TN4WAAAADBhPw50XNP9XFkKJUwAAAAuvvo+1aYfHf1gYUgAAAAqjcDk0v1CijaECAAAADkfLVT12lCZ670686kBrAAAADf5fWr9EzN4mO1YGYz4AAAAEAAAACm8xRMCqTO1W29kAAAAIZ9wol8oW4YsAAAAOaGugcKI9oAJrZhCPutAAAAAPhENjuCNqN/goPvsnNn9u" + + leafsResponseBytes, err := Codec.Marshal(Version, leafsResponse) + require.NoError(t, err) + require.Equal(t, base64LeafsResponse, base64.StdEncoding.EncodeToString(leafsResponseBytes)) + + var l LeafsResponse + _, err = Codec.Unmarshal(leafsResponseBytes, &l) + require.NoError(t, err) + require.Equal(t, leafsResponse.Keys, l.Keys) + require.Equal(t, leafsResponse.Vals, l.Vals) + require.False(t, l.More) // make sure it is not serialized + require.Equal(t, leafsResponse.ProofVals, l.ProofVals) +} diff --git a/vms/evm/sync/message/request.go b/vms/evm/sync/message/request.go new file mode 100644 index 000000000000..b4149311390a --- /dev/null +++ b/vms/evm/sync/message/request.go @@ -0,0 +1,27 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package message + +import ( + "context" + "fmt" + + "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/ids" +) + +// Request represents a Network request type. +type Request interface { + // Stringer enables requests to implement String() for logging. + fmt.Stringer + + // Handle allows `Request` to call respective methods on handler to handle + // this particular request type. + Handle(ctx context.Context, nodeID ids.NodeID, requestID uint32, handler RequestHandler) ([]byte, error) +} + +// RequestToBytes marshals the given request object into bytes. +func RequestToBytes(codec codec.Manager, request Request) ([]byte, error) { + return codec.Marshal(Version, &request) +} diff --git a/vms/evm/sync/message/request_test.go b/vms/evm/sync/message/request_test.go new file mode 100644 index 000000000000..4e4389af7269 --- /dev/null +++ b/vms/evm/sync/message/request_test.go @@ -0,0 +1,184 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package message + +import ( + "context" + "testing" + + "github.com/ava-labs/libevm/common" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/ids" +) + +type recordingHandler struct { + leafsArgs struct { + ctx context.Context + nodeID ids.NodeID + requestID uint32 + req LeafsRequest + } + + blockArgs struct { + ctx context.Context + nodeID ids.NodeID + requestID uint32 + req BlockRequest + } + + codeArgs struct { + ctx context.Context + nodeID ids.NodeID + requestID uint32 + req CodeRequest + } + + leafsCalled bool + blockCalled bool + codeCalled bool +} + +func (r *recordingHandler) HandleLeafsRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, req LeafsRequest) ([]byte, error) { + r.leafsCalled = true + r.leafsArgs.ctx = ctx + r.leafsArgs.nodeID = nodeID + r.leafsArgs.requestID = requestID + r.leafsArgs.req = req + return nil, nil +} + +func (r *recordingHandler) HandleBlockRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, req BlockRequest) ([]byte, error) { + r.blockCalled = true + r.blockArgs.ctx = ctx + r.blockArgs.nodeID = nodeID + r.blockArgs.requestID = requestID + r.blockArgs.req = req + return nil, nil +} + +func (r *recordingHandler) HandleCodeRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, req CodeRequest) ([]byte, error) { + r.codeCalled = true + r.codeArgs.ctx = ctx + r.codeArgs.nodeID = nodeID + r.codeArgs.requestID = requestID + r.codeArgs.req = req + return nil, nil +} + +func TestRequest_HandleDispatchesToCorrectHandler(t *testing.T) { + t.Parallel() + + ctx := context.TODO() + nodeID := ids.EmptyNodeID + const requestID uint32 = 42 + + tests := []struct { + name string + build func() Request + check func(t *testing.T, h *recordingHandler, built Request) + }{ + { + name: "leafs_request", + build: func() Request { + return LeafsRequest{ + Root: common.Hash{1}, + Start: make([]byte, common.HashLength), + End: make([]byte, common.HashLength), + Limit: 1, + NodeType: StateTrieNode, + } + }, + check: func(t *testing.T, h *recordingHandler, built Request) { + require := require.New(t) + require.True(h.leafsCalled) + require.Equal(requestID, h.leafsArgs.requestID) + require.IsType(LeafsRequest{}, built) + }, + }, + { + name: "block_request", + build: func() Request { + return BlockRequest{Hash: common.Hash{2}, Height: 3, Parents: 1} + }, + check: func(t *testing.T, h *recordingHandler, built Request) { + require := require.New(t) + require.True(h.blockCalled) + require.Equal(requestID, h.blockArgs.requestID) + require.IsType(BlockRequest{}, built) + }, + }, + { + name: "code_request", + build: func() Request { + return CodeRequest{Hashes: []common.Hash{{3}}} + }, + check: func(t *testing.T, h *recordingHandler, built Request) { + require := require.New(t) + require.True(h.codeCalled) + require.Equal(requestID, h.codeArgs.requestID) + require.IsType(CodeRequest{}, built) + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + h := new(recordingHandler) + built := tc.build() + + _, err := built.Handle(ctx, nodeID, requestID, h) + require.NoError(t, err) + + tc.check(t, h, built) + }) + } +} + +func TestRequestToBytes_InterfaceRoundTrip(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + req Request + }{ + { + name: "code", + req: CodeRequest{ + Hashes: []common.Hash{{1}}, + }, + }, + { + name: "leafs", + req: LeafsRequest{ + Root: common.Hash{2}, + Start: make([]byte, common.HashLength), + End: make([]byte, common.HashLength), + Limit: 1, + NodeType: StateTrieNode, + }, + }, + { + name: "block", + req: BlockRequest{ + Hash: common.Hash{3}, + Height: 4, + Parents: 1, + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + b, err := RequestToBytes(Codec, c.req) + require.NoError(t, err) + + var out Request + _, err = Codec.Unmarshal(b, &out) + require.NoError(t, err) + require.IsType(t, c.req, out) + }) + } +} diff --git a/vms/evm/sync/message/syncable.go b/vms/evm/sync/message/syncable.go new file mode 100644 index 000000000000..3a3690b8f9c9 --- /dev/null +++ b/vms/evm/sync/message/syncable.go @@ -0,0 +1,22 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package message + +import ( + "github.com/ava-labs/libevm/common" + + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" +) + +type Syncable interface { + block.StateSummary + GetBlockHash() common.Hash + GetBlockRoot() common.Hash +} + +type SyncableParser interface { + Parse(summaryBytes []byte, acceptImpl AcceptImplFn) (Syncable, error) +} + +type AcceptImplFn func(Syncable) (block.StateSyncMode, error) From b93d0dca554b5a93882fa0dd35ae480970d8557c Mon Sep 17 00:00:00 2001 From: Tsvetan Dimitrov Date: Wed, 15 Oct 2025 00:03:47 +0300 Subject: [PATCH 2/6] test: simplify request_test --- vms/evm/sync/message/request_test.go | 61 ++++++++++------------------ 1 file changed, 22 insertions(+), 39 deletions(-) diff --git a/vms/evm/sync/message/request_test.go b/vms/evm/sync/message/request_test.go index 4e4389af7269..360721c4b261 100644 --- a/vms/evm/sync/message/request_test.go +++ b/vms/evm/sync/message/request_test.go @@ -75,51 +75,26 @@ func TestRequest_HandleDispatchesToCorrectHandler(t *testing.T) { const requestID uint32 = 42 tests := []struct { - name string - build func() Request - check func(t *testing.T, h *recordingHandler, built Request) + name string + req Request }{ { name: "leafs_request", - build: func() Request { - return LeafsRequest{ - Root: common.Hash{1}, - Start: make([]byte, common.HashLength), - End: make([]byte, common.HashLength), - Limit: 1, - NodeType: StateTrieNode, - } - }, - check: func(t *testing.T, h *recordingHandler, built Request) { - require := require.New(t) - require.True(h.leafsCalled) - require.Equal(requestID, h.leafsArgs.requestID) - require.IsType(LeafsRequest{}, built) + req: LeafsRequest{ + Root: common.Hash{1}, + Start: make([]byte, common.HashLength), + End: make([]byte, common.HashLength), + Limit: 1, + NodeType: StateTrieNode, }, }, { name: "block_request", - build: func() Request { - return BlockRequest{Hash: common.Hash{2}, Height: 3, Parents: 1} - }, - check: func(t *testing.T, h *recordingHandler, built Request) { - require := require.New(t) - require.True(h.blockCalled) - require.Equal(requestID, h.blockArgs.requestID) - require.IsType(BlockRequest{}, built) - }, + req: BlockRequest{Hash: common.Hash{2}, Height: 3, Parents: 1}, }, { name: "code_request", - build: func() Request { - return CodeRequest{Hashes: []common.Hash{{3}}} - }, - check: func(t *testing.T, h *recordingHandler, built Request) { - require := require.New(t) - require.True(h.codeCalled) - require.Equal(requestID, h.codeArgs.requestID) - require.IsType(CodeRequest{}, built) - }, + req: CodeRequest{Hashes: []common.Hash{{3}}}, }, } @@ -127,12 +102,20 @@ func TestRequest_HandleDispatchesToCorrectHandler(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() h := new(recordingHandler) - built := tc.build() - - _, err := built.Handle(ctx, nodeID, requestID, h) + _, err := tc.req.Handle(ctx, nodeID, requestID, h) require.NoError(t, err) - tc.check(t, h, built) + switch tc.req.(type) { + case LeafsRequest: + require.True(t, h.leafsCalled) + require.Equal(t, requestID, h.leafsArgs.requestID) + case BlockRequest: + require.True(t, h.blockCalled) + require.Equal(t, requestID, h.blockArgs.requestID) + case CodeRequest: + require.True(t, h.codeCalled) + require.Equal(t, requestID, h.codeArgs.requestID) + } }) } } From 2bf63937c6ac79c06d05ab2f172cf7fd705d004d Mon Sep 17 00:00:00 2001 From: Tsvetan Dimitrov Date: Wed, 15 Oct 2025 00:04:19 +0300 Subject: [PATCH 3/6] test: simplify request_test --- vms/evm/sync/message/request_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vms/evm/sync/message/request_test.go b/vms/evm/sync/message/request_test.go index 360721c4b261..814b7fe3dfec 100644 --- a/vms/evm/sync/message/request_test.go +++ b/vms/evm/sync/message/request_test.go @@ -75,8 +75,8 @@ func TestRequest_HandleDispatchesToCorrectHandler(t *testing.T) { const requestID uint32 = 42 tests := []struct { - name string - req Request + name string + req Request }{ { name: "leafs_request", From 479821c1f20a0e95c09e58a7567fcb7f8f2e0fde Mon Sep 17 00:00:00 2001 From: Tsvetan Dimitrov Date: Wed, 15 Oct 2025 00:19:53 +0300 Subject: [PATCH 4/6] test: separate block sync summary parse tests into a new file --- .../sync/message/block_sync_summary_parser.go | 7 ++- .../message/block_sync_summary_parser_test.go | 47 +++++++++++++++++++ .../sync/message/block_sync_summary_test.go | 25 ++-------- 3 files changed, 56 insertions(+), 23 deletions(-) create mode 100644 vms/evm/sync/message/block_sync_summary_parser_test.go diff --git a/vms/evm/sync/message/block_sync_summary_parser.go b/vms/evm/sync/message/block_sync_summary_parser.go index abeed28d36c2..5986ddd96821 100644 --- a/vms/evm/sync/message/block_sync_summary_parser.go +++ b/vms/evm/sync/message/block_sync_summary_parser.go @@ -4,6 +4,7 @@ package message import ( + "errors" "fmt" "github.com/ava-labs/libevm/crypto" @@ -11,6 +12,10 @@ import ( "github.com/ava-labs/avalanchego/ids" ) +// errInvalidBlockSyncSummary is returned when the provided bytes cannot be +// parsed into a valid BlockSyncSummary. +var errInvalidBlockSyncSummary = errors.New("invalid block sync summary") + type BlockSyncSummaryParser struct{} func NewBlockSyncSummaryParser() *BlockSyncSummaryParser { @@ -20,7 +25,7 @@ func NewBlockSyncSummaryParser() *BlockSyncSummaryParser { func (*BlockSyncSummaryParser) Parse(summaryBytes []byte, acceptImpl AcceptImplFn) (Syncable, error) { summary := BlockSyncSummary{} if _, err := Codec.Unmarshal(summaryBytes, &summary); err != nil { - return nil, fmt.Errorf("failed to parse syncable summary: %w", err) + return nil, fmt.Errorf("%w", errInvalidBlockSyncSummary) } summary.bytes = summaryBytes diff --git a/vms/evm/sync/message/block_sync_summary_parser_test.go b/vms/evm/sync/message/block_sync_summary_parser_test.go new file mode 100644 index 000000000000..4d95a0ad2799 --- /dev/null +++ b/vms/evm/sync/message/block_sync_summary_parser_test.go @@ -0,0 +1,47 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package message + +import ( + "context" + "testing" + + "github.com/ava-labs/libevm/common" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" +) + +func TestBlockSyncSummaryParser_ParseValid(t *testing.T) { + t.Parallel() + + blockSyncSummary, err := NewBlockSyncSummary(common.Hash{1}, 2, common.Hash{3}) + require.NoError(t, err) + + parser := NewBlockSyncSummaryParser() + called := false + acceptImplTest := func(Syncable) (block.StateSyncMode, error) { + called = true + return block.StateSyncSkipped, nil + } + s, err := parser.Parse(blockSyncSummary.Bytes(), acceptImplTest) + require.NoError(t, err) + require.Equal(t, blockSyncSummary.GetBlockHash(), s.GetBlockHash()) + require.Equal(t, blockSyncSummary.Height(), s.Height()) + require.Equal(t, blockSyncSummary.GetBlockRoot(), s.GetBlockRoot()) + require.Equal(t, blockSyncSummary.Bytes(), s.Bytes()) + + mode, err := s.Accept(context.Background()) + require.NoError(t, err) + require.Equal(t, block.StateSyncSkipped, mode) + require.True(t, called) +} + +func TestBlockSyncSummaryParser_ParseInvalid(t *testing.T) { + t.Parallel() + + parser := NewBlockSyncSummaryParser() + _, err := parser.Parse([]byte("not-a-summary"), nil) + require.ErrorIs(t, err, errInvalidBlockSyncSummary) +} diff --git a/vms/evm/sync/message/block_sync_summary_test.go b/vms/evm/sync/message/block_sync_summary_test.go index 5321a14b6f70..3489a6531700 100644 --- a/vms/evm/sync/message/block_sync_summary_test.go +++ b/vms/evm/sync/message/block_sync_summary_test.go @@ -4,17 +4,16 @@ package message import ( - "context" "encoding/base64" "testing" "github.com/ava-labs/libevm/common" "github.com/stretchr/testify/require" - - "github.com/ava-labs/avalanchego/snow/engine/snowman/block" ) -func TestMarshalBlockSyncSummary(t *testing.T) { +func TestBlockSyncSummary_MarshalGolden(t *testing.T) { + t.Parallel() + blockSyncSummary, err := NewBlockSyncSummary(common.Hash{1}, 2, common.Hash{3}) require.NoError(t, err) @@ -24,22 +23,4 @@ func TestMarshalBlockSyncSummary(t *testing.T) { expectedBase64Bytes := "AAAAAAAAAAAAAgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" require.Equal(t, expectedBase64Bytes, base64.StdEncoding.EncodeToString(blockSyncSummary.Bytes())) - - parser := NewBlockSyncSummaryParser() - called := false - acceptImplTest := func(Syncable) (block.StateSyncMode, error) { - called = true - return block.StateSyncSkipped, nil - } - s, err := parser.Parse(blockSyncSummary.Bytes(), acceptImplTest) - require.NoError(t, err) - require.Equal(t, blockSyncSummary.GetBlockHash(), s.GetBlockHash()) - require.Equal(t, blockSyncSummary.Height(), s.Height()) - require.Equal(t, blockSyncSummary.GetBlockRoot(), s.GetBlockRoot()) - require.Equal(t, blockSyncSummary.Bytes(), s.Bytes()) - - mode, err := s.Accept(context.TODO()) - require.NoError(t, err) - require.Equal(t, block.StateSyncSkipped, mode) - require.True(t, called) } From ede1a7e563dcacb7b24433689e5b53d8bef48c82 Mon Sep 17 00:00:00 2001 From: Tsvetan Dimitrov Date: Wed, 15 Oct 2025 00:24:58 +0300 Subject: [PATCH 5/6] style: consistency fixes --- vms/evm/sync/message/code_request.go | 2 +- vms/evm/sync/message/leafs_request.go | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/vms/evm/sync/message/code_request.go b/vms/evm/sync/message/code_request.go index be59623c4351..d05ec37eb842 100644 --- a/vms/evm/sync/message/code_request.go +++ b/vms/evm/sync/message/code_request.go @@ -13,7 +13,7 @@ import ( "github.com/ava-labs/avalanchego/ids" ) -var _ Request = CodeRequest{} +var _ Request = (*CodeRequest)(nil) // CodeRequest is a request to retrieve a contract code with specified Hash type CodeRequest struct { diff --git a/vms/evm/sync/message/leafs_request.go b/vms/evm/sync/message/leafs_request.go index 6dced30e392a..466cc107e97d 100644 --- a/vms/evm/sync/message/leafs_request.go +++ b/vms/evm/sync/message/leafs_request.go @@ -12,22 +12,22 @@ import ( "github.com/ava-labs/avalanchego/ids" ) -// MaxCodeHashesPerRequest limits the number of code hashes per request to bound -// bandwidth and CPU for verification. -const MaxCodeHashesPerRequest = 5 +const ( + // MaxCodeHashesPerRequest limits the number of code hashes per request to bound + // bandwidth and CPU for verification. + MaxCodeHashesPerRequest = 5 + + StateTrieNode = NodeType(1) + StateTrieKeyLength = common.HashLength +) -var _ Request = LeafsRequest{} +var _ Request = (*LeafsRequest)(nil) // NodeType outlines the trie that a leaf node belongs to // handlers.LeafsRequestHandler uses this information to determine // which trie type to fetch the information from type NodeType uint8 -const ( - StateTrieNode = NodeType(1) - StateTrieKeyLength = common.HashLength -) - // LeafsRequest is a request to receive trie leaves at specified Root within Start and End byte range // Limit outlines maximum number of leaves to returns starting at Start // NodeType outlines which trie to read from state/atomic. From f52ff6e63abb4f1ff96cc89243787451afaa6936 Mon Sep 17 00:00:00 2001 From: Tsvetan Dimitrov Date: Fri, 31 Oct 2025 14:07:36 +0200 Subject: [PATCH 6/6] refactor(vms/evm/sync/message): consolidate files and improve code quality - Consolidate related files: merge syncable.go, block_sync_summary_parser.go, block_sync_summary_provider.go into block_sync_summary.go and merge handler.go into request.go. - Replace init() with lazy Codec() function using sync.Once. - Add sentinel error for better error handling with require.ErrorIs. - Convert tests to table-driven format and add comprehensive coverage. - Improve documentation and fix linting issues. BREAKING CHANGE: Codec initialization changed from init() to lazy initialization via Codec() function. resolves #4416 Signed-off-by: Tsvetan Dimitrov (tsvetan.dimitrov@avalabs.org) --- vms/evm/sync/message/block_request.go | 8 +- vms/evm/sync/message/block_request_test.go | 8 +- vms/evm/sync/message/block_sync_summary.go | 69 ++++++++- .../sync/message/block_sync_summary_parser.go | 39 ------ .../message/block_sync_summary_parser_test.go | 47 ------- .../message/block_sync_summary_provider.go | 17 --- .../sync/message/block_sync_summary_test.go | 132 ++++++++++++++++++ vms/evm/sync/message/code_request.go | 12 +- vms/evm/sync/message/code_request_test.go | 8 +- vms/evm/sync/message/codec.go | 70 ++++++---- vms/evm/sync/message/codec_test.go | 46 ++++++ vms/evm/sync/message/handler.go | 45 ------ vms/evm/sync/message/leafs_request.go | 17 ++- vms/evm/sync/message/leafs_request_test.go | 24 ++-- vms/evm/sync/message/request.go | 41 +++++- vms/evm/sync/message/request_test.go | 32 ++++- vms/evm/sync/message/syncable.go | 22 --- 17 files changed, 393 insertions(+), 244 deletions(-) delete mode 100644 vms/evm/sync/message/block_sync_summary_parser.go delete mode 100644 vms/evm/sync/message/block_sync_summary_parser_test.go delete mode 100644 vms/evm/sync/message/block_sync_summary_provider.go create mode 100644 vms/evm/sync/message/codec_test.go delete mode 100644 vms/evm/sync/message/handler.go delete mode 100644 vms/evm/sync/message/syncable.go diff --git a/vms/evm/sync/message/block_request.go b/vms/evm/sync/message/block_request.go index f14bc88992cd..d31c3fc12db1 100644 --- a/vms/evm/sync/message/block_request.go +++ b/vms/evm/sync/message/block_request.go @@ -14,7 +14,8 @@ import ( var _ Request = (*BlockRequest)(nil) -// BlockRequest is a request to retrieve Parents number of blocks starting from Hash from newest-oldest manner +// BlockRequest is a request to retrieve the specified number of parent blocks +// starting from the given hash, ordered from newest to oldest. type BlockRequest struct { Hash common.Hash `serialize:"true"` Height uint64 `serialize:"true"` @@ -32,10 +33,9 @@ func (b BlockRequest) Handle(ctx context.Context, nodeID ids.NodeID, requestID u return handler.HandleBlockRequest(ctx, nodeID, requestID, b) } -// BlockResponse is a response to a BlockRequest -// Blocks is slice of RLP encoded blocks starting with the block +// BlockResponse is a response to a BlockRequest. +// Blocks is a slice of RLP-encoded blocks starting with the block // requested in BlockRequest.Hash. The next block is the parent, etc. -// handler: handlers.BlockRequestHandler type BlockResponse struct { Blocks [][]byte `serialize:"true"` } diff --git a/vms/evm/sync/message/block_request_test.go b/vms/evm/sync/message/block_request_test.go index 92420bd5d5d7..9ddecb313014 100644 --- a/vms/evm/sync/message/block_request_test.go +++ b/vms/evm/sync/message/block_request_test.go @@ -23,12 +23,12 @@ func TestMarshalBlockRequest(t *testing.T) { base64BlockRequest := "AAAAAAAAAAAAAAAAAABzb21lIGhhc2ggaXMgaGVyZSB5bwAAAAAAAAU5AEA=" - blockRequestBytes, err := Codec.Marshal(Version, blockRequest) + blockRequestBytes, err := Codec().Marshal(Version, blockRequest) require.NoError(t, err) require.Equal(t, base64BlockRequest, base64.StdEncoding.EncodeToString(blockRequestBytes)) var b BlockRequest - _, err = Codec.Unmarshal(blockRequestBytes, &b) + _, err = Codec().Unmarshal(blockRequestBytes, &b) require.NoError(t, err) require.Equal(t, blockRequest.Hash, b.Hash) require.Equal(t, blockRequest.Height, b.Height) @@ -54,12 +54,12 @@ func TestMarshalBlockResponse(t *testing.T) { base64BlockResponse := "AAAAAAAgAAAAIU8WP18PmmIdcpVmx00QA3xNe7sEB9HixkmBhVrYaB0NhgAAADnR6ZTSxCKs0gigByk5SH9pmeudGKRHhARdh/PGfPInRumVr1olNnlRuqL/bNRxxIPxX7kLrbN8WCEAAAA6tmgLTnyLdjobHUnUlVyEhiFjJSU/7HON16nii/khEZwWDwcCRIYVu9oIMT9qjrZo0gv1BZh1kh5migAAACtb3yx/xIRo0tbFL1BU4tCDa/hMcXTLdHY2TMPb2Wiw9xcu2FeUuzWLDDtSAAAAO12heG+f69ehnQ97usvgJVqlt9RL7ED4TIkrm//UNimwIjvupfT3Q5H0RdFa/UKUBAN09pJLmMv4cT+NAAAAMpYtJOLK/Mrjph+1hrFDI6a8j5598dkpMz/5k5M76m9bOvbeA3Q2bEcZ5DobBn2JvH8BAAAAOfHxekxyFaO1OeseWEnGB327VyL1cXoomiZvl2R5gZmOvqicC0s3OXARXoLtb0ElyPpzEeTX3vqSLQAAACc2zU8kq/ffhmuqVgODZ61hRd4e6PSosJk+vfiIOgrYvpw5eLBIg+UAAAAkahVqnexqQOmh0AfwM8KCMGG90Oqln45NpkMBBSINCyloi3NLAAAAKI6gENd8luqAp6Zl9gb2pjt/Pf0lZ8GJeeTWDyZobZvy+ybJAf81TN4AAAA8FgfuKbpk+Eq0PKDG5rkcH9O+iZBDQXnTr0SRo2kBLbktGE/DnRc0/1cWQolTu2hl/PkrDDoXyQKL6ZFOAAAAMwl50YMDVvKlTD3qsqS0R11jr76PtWmHx39YGFJvGBS+gjNQ6rE5NfMdhEhFF+kkrveK4QAAADhRwAdVkgww7CmjcDk0v1CijaECl13tp351hXnqPf5BNqv3UrO4Jx0D6USzyds2a3UEX479adIq5QAAADpBGUfLVbzqQGsy1hCL1oWE9X43yqxuM/6qMmOjmUNwJLqcmxRniidPAakQrilfbvv+X1q/RMzeJjtWAAAAKAZjPn05Bp8BojnENlhUw69/a0HWMfkrmo0S9BJXMl//My91drBiBVYAAAAqMEo+Pq6QGlJyDahcoeSzjq8/RMbG74Ni8vVPwA4J1vwlZAhUwV38rKqKAAAAOyzszlo6lLTTOKUUPmNAjYcksM8/rhej95vhBy+2PDXWBCxBYPOO6eKp8/tP+wAZtFTVIrX/oXYEGT+4AAAAMpZnz1PD9SDIibeb9QTPtXx2ASMtWJuszqnW4mPiXCd0HT9sYsu7FdmvvL9/faQasECOAAAALzk4vxd0rOdwmk8JHpqD/erg7FXrIzqbU5TLPHhWtUbTE8ijtMHA4FRH9Lo3DrNtAAAAPLz97PUi4qbx7Qr+wfjiD6q+32sWLnF9OnSKWGd6DFY0j4khomaxHQ8zTGL+UrpTrxl3nLKUi2Vw/6C3cwAAADqWPBMK15dRJSEPDvHDFAkPB8eab1ccJG8+msC3QT7xEL1YsAznO/9wb3/0tvRAkKMnEfMgjk5LictRAAAAJ2XOZAA98kaJKNWiO5ynQPgMk4LZxgNK0pYMeWUD4c4iFyX1DK8fvwAAADtcR6U9v459yvyeE4ZHpLRO1LzpZO1H90qllEaM7TI8t28NP6xHbJ+wP8kij7roj9WAZjoEVLaDEiB/CgAAADc7WExi1QJ84VpPClglDY+1Dnfyv08BUuXUlDWAf51Ll75vt3lwRmpWJv4zQIz56I4seXQIoy0pAAAAKkFrryBqmDIJgsharXA4SFnAWksTodWy9b/vWm7ZLaSCyqlWjltv6dip3QAAAC7Z6wkne1AJRMvoAKCxUn6mRymoYdL2SXoyNcN/QZJ3nsHZazscVCT84LcnsDByAAAAI+ZAq8lEj93rIZHZRcBHZ6+Eev0O212IV7eZrLGOSv+r4wN/AAAAL/7MQW5zTTc8Xr68nNzFlbzOPHvT2N+T+rfhJd3rr+ZaMb1dQeLSzpwrF4kvD+oZAAAAMTGikNy/poQG6HcHP/CINOGXpANKpIr6P4W4picIyuu6yIC1uJuT2lOBAWRAIQTmSLYAAAA1ImobDzE6id38RUxfj3KsibOLGfU3hMGem+rAPIdaJ9sCneN643pCMYgTSHaFkpNZyoxeuU4AAAA9FS3Br0LquOKSXG2u5N5e+fnc8I38vQK4CAk5hYWSig995QvhptwdV2joU3mI/dzlYum5SMkYu6PpM+XEAAAAAC3Nrne6HSWbGIpLIchvvCPXKLRTR+raZQryTFbQgAqGkTMgiKgFvVXERuJesHU=" - blockResponseBytes, err := Codec.Marshal(Version, blockResponse) + blockResponseBytes, err := Codec().Marshal(Version, blockResponse) require.NoError(t, err) require.Equal(t, base64BlockResponse, base64.StdEncoding.EncodeToString(blockResponseBytes)) var b BlockResponse - _, err = Codec.Unmarshal(blockResponseBytes, &b) + _, err = Codec().Unmarshal(blockResponseBytes, &b) require.NoError(t, err) require.Equal(t, blockResponse.Blocks, b.Blocks) } diff --git a/vms/evm/sync/message/block_sync_summary.go b/vms/evm/sync/message/block_sync_summary.go index aedb630e19d3..b6f019350d3d 100644 --- a/vms/evm/sync/message/block_sync_summary.go +++ b/vms/evm/sync/message/block_sync_summary.go @@ -5,16 +5,44 @@ package message import ( "context" + "errors" "fmt" "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/crypto" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/engine/snowman/block" ) -var _ Syncable = (*BlockSyncSummary)(nil) +var ( + _ Syncable = (*BlockSyncSummary)(nil) + _ SyncableParser = (*BlockSyncSummaryParser)(nil) + + // errInvalidBlockSyncSummary is returned when the provided bytes cannot be + // parsed into a valid BlockSyncSummary. + errInvalidBlockSyncSummary = errors.New("invalid block sync summary") + + // errAcceptImplNotSpecified is returned when Accept is called on a BlockSyncSummary + // that doesn't have an acceptImpl set. + errAcceptImplNotSpecified = errors.New("accept implementation not specified") +) + +// Syncable extends [block.StateSummary] with EVM-specific block information. +type Syncable interface { + block.StateSummary + GetBlockHash() common.Hash + GetBlockRoot() common.Hash +} + +// SyncableParser parses raw bytes into a [Syncable] instance. +type SyncableParser interface { + Parse(summaryBytes []byte, acceptImpl AcceptImplFn) (Syncable, error) +} + +// AcceptImplFn is a function that determines the state sync mode for a given [Syncable]. +type AcceptImplFn func(Syncable) (block.StateSyncMode, error) // BlockSyncSummary provides the information necessary to sync a node starting // at the given block. @@ -28,14 +56,15 @@ type BlockSyncSummary struct { acceptImpl AcceptImplFn } +// NewBlockSyncSummary creates a new [BlockSyncSummary] for the given block. +// The acceptImpl is intentionally left unset and should be set by the parser. func NewBlockSyncSummary(blockHash common.Hash, blockNumber uint64, blockRoot common.Hash) (*BlockSyncSummary, error) { - // We intentionally do not use the acceptImpl here and leave it for the parser to set. summary := BlockSyncSummary{ BlockNumber: blockNumber, BlockHash: blockHash, BlockRoot: blockRoot, } - bytes, err := Codec.Marshal(Version, &summary) + bytes, err := Codec().Marshal(Version, &summary) if err != nil { return nil, fmt.Errorf("failed to marshal syncable summary: %w", err) } @@ -76,7 +105,39 @@ func (s *BlockSyncSummary) String() string { func (s *BlockSyncSummary) Accept(context.Context) (block.StateSyncMode, error) { if s.acceptImpl == nil { - return block.StateSyncSkipped, fmt.Errorf("accept implementation not specified for summary: %s", s) + return block.StateSyncSkipped, errAcceptImplNotSpecified } return s.acceptImpl(s) } + +// BlockSyncSummaryParser parses [BlockSyncSummary] instances from raw bytes. +type BlockSyncSummaryParser struct{} + +// NewBlockSyncSummaryParser creates a new [BlockSyncSummaryParser]. +func NewBlockSyncSummaryParser() *BlockSyncSummaryParser { + return &BlockSyncSummaryParser{} +} + +func (*BlockSyncSummaryParser) Parse(summaryBytes []byte, acceptImpl AcceptImplFn) (Syncable, error) { + summary := BlockSyncSummary{} + if _, err := Codec().Unmarshal(summaryBytes, &summary); err != nil { + return nil, fmt.Errorf("%w: %w", errInvalidBlockSyncSummary, err) + } + + summary.bytes = summaryBytes + summaryID, err := ids.ToID(crypto.Keccak256(summaryBytes)) + if err != nil { + return nil, fmt.Errorf("failed to compute summary ID: %w", err) + } + summary.summaryID = summaryID + summary.acceptImpl = acceptImpl + return &summary, nil +} + +// BlockSyncSummaryProvider provides state summaries for blocks. +type BlockSyncSummaryProvider struct{} + +// StateSummaryAtBlock returns the block state summary for the given block if valid. +func (*BlockSyncSummaryProvider) StateSummaryAtBlock(blk *types.Block) (block.StateSummary, error) { + return NewBlockSyncSummary(blk.Hash(), blk.NumberU64(), blk.Root()) +} diff --git a/vms/evm/sync/message/block_sync_summary_parser.go b/vms/evm/sync/message/block_sync_summary_parser.go deleted file mode 100644 index 5986ddd96821..000000000000 --- a/vms/evm/sync/message/block_sync_summary_parser.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package message - -import ( - "errors" - "fmt" - - "github.com/ava-labs/libevm/crypto" - - "github.com/ava-labs/avalanchego/ids" -) - -// errInvalidBlockSyncSummary is returned when the provided bytes cannot be -// parsed into a valid BlockSyncSummary. -var errInvalidBlockSyncSummary = errors.New("invalid block sync summary") - -type BlockSyncSummaryParser struct{} - -func NewBlockSyncSummaryParser() *BlockSyncSummaryParser { - return &BlockSyncSummaryParser{} -} - -func (*BlockSyncSummaryParser) Parse(summaryBytes []byte, acceptImpl AcceptImplFn) (Syncable, error) { - summary := BlockSyncSummary{} - if _, err := Codec.Unmarshal(summaryBytes, &summary); err != nil { - return nil, fmt.Errorf("%w", errInvalidBlockSyncSummary) - } - - summary.bytes = summaryBytes - summaryID, err := ids.ToID(crypto.Keccak256(summaryBytes)) - if err != nil { - return nil, fmt.Errorf("failed to compute summary ID: %w", err) - } - summary.summaryID = summaryID - summary.acceptImpl = acceptImpl - return &summary, nil -} diff --git a/vms/evm/sync/message/block_sync_summary_parser_test.go b/vms/evm/sync/message/block_sync_summary_parser_test.go deleted file mode 100644 index 4d95a0ad2799..000000000000 --- a/vms/evm/sync/message/block_sync_summary_parser_test.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package message - -import ( - "context" - "testing" - - "github.com/ava-labs/libevm/common" - "github.com/stretchr/testify/require" - - "github.com/ava-labs/avalanchego/snow/engine/snowman/block" -) - -func TestBlockSyncSummaryParser_ParseValid(t *testing.T) { - t.Parallel() - - blockSyncSummary, err := NewBlockSyncSummary(common.Hash{1}, 2, common.Hash{3}) - require.NoError(t, err) - - parser := NewBlockSyncSummaryParser() - called := false - acceptImplTest := func(Syncable) (block.StateSyncMode, error) { - called = true - return block.StateSyncSkipped, nil - } - s, err := parser.Parse(blockSyncSummary.Bytes(), acceptImplTest) - require.NoError(t, err) - require.Equal(t, blockSyncSummary.GetBlockHash(), s.GetBlockHash()) - require.Equal(t, blockSyncSummary.Height(), s.Height()) - require.Equal(t, blockSyncSummary.GetBlockRoot(), s.GetBlockRoot()) - require.Equal(t, blockSyncSummary.Bytes(), s.Bytes()) - - mode, err := s.Accept(context.Background()) - require.NoError(t, err) - require.Equal(t, block.StateSyncSkipped, mode) - require.True(t, called) -} - -func TestBlockSyncSummaryParser_ParseInvalid(t *testing.T) { - t.Parallel() - - parser := NewBlockSyncSummaryParser() - _, err := parser.Parse([]byte("not-a-summary"), nil) - require.ErrorIs(t, err, errInvalidBlockSyncSummary) -} diff --git a/vms/evm/sync/message/block_sync_summary_provider.go b/vms/evm/sync/message/block_sync_summary_provider.go deleted file mode 100644 index 98eece22b0e6..000000000000 --- a/vms/evm/sync/message/block_sync_summary_provider.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package message - -import ( - "github.com/ava-labs/libevm/core/types" - - "github.com/ava-labs/avalanchego/snow/engine/snowman/block" -) - -type BlockSyncSummaryProvider struct{} - -// StateSummaryAtBlock returns the block state summary at [block] if valid. -func (*BlockSyncSummaryProvider) StateSummaryAtBlock(blk *types.Block) (block.StateSummary, error) { - return NewBlockSyncSummary(blk.Hash(), blk.NumberU64(), blk.Root()) -} diff --git a/vms/evm/sync/message/block_sync_summary_test.go b/vms/evm/sync/message/block_sync_summary_test.go index 3489a6531700..c28cd3aeb17f 100644 --- a/vms/evm/sync/message/block_sync_summary_test.go +++ b/vms/evm/sync/message/block_sync_summary_test.go @@ -5,10 +5,14 @@ package message import ( "encoding/base64" + "errors" "testing" "github.com/ava-labs/libevm/common" "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" ) func TestBlockSyncSummary_MarshalGolden(t *testing.T) { @@ -24,3 +28,131 @@ func TestBlockSyncSummary_MarshalGolden(t *testing.T) { expectedBase64Bytes := "AAAAAAAAAAAAAgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" require.Equal(t, expectedBase64Bytes, base64.StdEncoding.EncodeToString(blockSyncSummary.Bytes())) } + +func TestBlockSyncSummary_Methods(t *testing.T) { + t.Parallel() + + blockHash := common.Hash{1, 2, 3} + blockNumber := uint64(42) + blockRoot := common.Hash{4, 5, 6} + + summary, err := NewBlockSyncSummary(blockHash, blockNumber, blockRoot) + require.NoError(t, err) + + require.Equal(t, blockHash, summary.GetBlockHash()) + require.Equal(t, blockRoot, summary.GetBlockRoot()) + require.Equal(t, blockNumber, summary.Height()) + require.NotNil(t, summary.Bytes()) + require.NotEqual(t, ids.ID{}, summary.ID()) + + // Test String() method + str := summary.String() + require.Contains(t, str, "BlockSyncSummary") + require.Contains(t, str, blockHash.String()) +} + +func TestBlockSyncSummary_Accept(t *testing.T) { + t.Parallel() + + errTestError := errors.New("test error") + + tests := []struct { + name string + acceptImpl AcceptImplFn + wantErr error + }{ + { + name: "nil_acceptImpl", + wantErr: errAcceptImplNotSpecified, + }, + { + name: "with_acceptImpl_error", + acceptImpl: func(Syncable) (block.StateSyncMode, error) { + return block.StateSyncSkipped, errTestError + }, + wantErr: errTestError, + }, + { + name: "with_acceptImpl_success", + acceptImpl: func(Syncable) (block.StateSyncMode, error) { + return block.StateSyncSkipped, nil + }, + wantErr: nil, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + summary, err := NewBlockSyncSummary(common.Hash{1}, 1, common.Hash{2}) + require.NoError(t, err) + + summary.acceptImpl = tc.acceptImpl + + mode, err := summary.Accept(t.Context()) + require.Equal(t, block.StateSyncSkipped, mode) + require.ErrorIs(t, err, tc.wantErr) + }) + } +} + +func TestBlockSyncSummaryParser_ParseValid(t *testing.T) { + t.Parallel() + + blockSyncSummary, err := NewBlockSyncSummary(common.Hash{1}, 2, common.Hash{3}) + require.NoError(t, err) + + parser := NewBlockSyncSummaryParser() + called := false + acceptImplTest := func(Syncable) (block.StateSyncMode, error) { + called = true + return block.StateSyncSkipped, nil + } + s, err := parser.Parse(blockSyncSummary.Bytes(), acceptImplTest) + require.NoError(t, err) + require.Equal(t, blockSyncSummary.GetBlockHash(), s.GetBlockHash()) + require.Equal(t, blockSyncSummary.Height(), s.Height()) + require.Equal(t, blockSyncSummary.GetBlockRoot(), s.GetBlockRoot()) + require.Equal(t, blockSyncSummary.Bytes(), s.Bytes()) + + mode, err := s.Accept(t.Context()) + require.NoError(t, err) + require.Equal(t, block.StateSyncSkipped, mode) + require.True(t, called) +} + +func TestBlockSyncSummaryParser_ParseInvalid(t *testing.T) { + t.Parallel() + + parser := NewBlockSyncSummaryParser() + + tests := []struct { + name string + summaryBytes []byte + wantErr error + }{ + { + name: "invalid_bytes", + summaryBytes: []byte("not-a-summary"), + wantErr: errInvalidBlockSyncSummary, + }, + { + name: "empty_bytes", + summaryBytes: []byte{}, + wantErr: errInvalidBlockSyncSummary, + }, + { + name: "nil_bytes", + summaryBytes: nil, + wantErr: errInvalidBlockSyncSummary, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + _, err := parser.Parse(tc.summaryBytes, nil) + require.ErrorIs(t, err, tc.wantErr) + }) + } +} diff --git a/vms/evm/sync/message/code_request.go b/vms/evm/sync/message/code_request.go index d05ec37eb842..879cbd49dae1 100644 --- a/vms/evm/sync/message/code_request.go +++ b/vms/evm/sync/message/code_request.go @@ -15,9 +15,9 @@ import ( var _ Request = (*CodeRequest)(nil) -// CodeRequest is a request to retrieve a contract code with specified Hash +// CodeRequest is a request to retrieve contract code for the specified hashes. type CodeRequest struct { - // Hashes is a list of contract code hashes + // Hashes is a list of contract code hashes to retrieve. Hashes []common.Hash `serialize:"true"` } @@ -33,16 +33,16 @@ func (c CodeRequest) Handle(ctx context.Context, nodeID ids.NodeID, requestID ui return handler.HandleCodeRequest(ctx, nodeID, requestID, c) } +// NewCodeRequest creates a new CodeRequest with the given hashes. func NewCodeRequest(hashes []common.Hash) CodeRequest { return CodeRequest{ Hashes: hashes, } } -// CodeResponse is a response to a CodeRequest -// crypto.Keccak256Hash of each element in Data is expected to equal -// the corresponding element in CodeRequest.Hashes -// handler: handlers.CodeRequestHandler +// CodeResponse is a response to a CodeRequest. +// The crypto.Keccak256Hash of each element in Data is expected to equal +// the corresponding element in CodeRequest.Hashes. type CodeResponse struct { Data [][]byte `serialize:"true"` } diff --git a/vms/evm/sync/message/code_request_test.go b/vms/evm/sync/message/code_request_test.go index a4e823f84d84..87cf6f480b26 100644 --- a/vms/evm/sync/message/code_request_test.go +++ b/vms/evm/sync/message/code_request_test.go @@ -21,12 +21,12 @@ func TestMarshalCodeRequest(t *testing.T) { base64CodeRequest := "AAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAHNvbWUgY29kZSBwbHM=" - codeRequestBytes, err := Codec.Marshal(Version, codeRequest) + codeRequestBytes, err := Codec().Marshal(Version, codeRequest) require.NoError(t, err) require.Equal(t, base64CodeRequest, base64.StdEncoding.EncodeToString(codeRequestBytes)) var c CodeRequest - _, err = Codec.Unmarshal(codeRequestBytes, &c) + _, err = Codec().Unmarshal(codeRequestBytes, &c) require.NoError(t, err) require.Equal(t, codeRequest.Hashes, c.Hashes) } @@ -47,12 +47,12 @@ func TestMarshalCodeResponse(t *testing.T) { base64CodeResponse := "AAAAAAABAAAAMlL9/AchgmVPFj9fD5piHXKVZsdNEAN8TXu7BAfR4sZJgYVa2GgdDYbR6R4AFnk5y2aU" - codeResponseBytes, err := Codec.Marshal(Version, codeResponse) + codeResponseBytes, err := Codec().Marshal(Version, codeResponse) require.NoError(t, err) require.Equal(t, base64CodeResponse, base64.StdEncoding.EncodeToString(codeResponseBytes)) var c CodeResponse - _, err = Codec.Unmarshal(codeResponseBytes, &c) + _, err = Codec().Unmarshal(codeResponseBytes, &c) require.NoError(t, err) require.Equal(t, codeResponse.Data, c.Data) } diff --git a/vms/evm/sync/message/codec.go b/vms/evm/sync/message/codec.go index aee9a70775d1..d50844bf66ac 100644 --- a/vms/evm/sync/message/codec.go +++ b/vms/evm/sync/message/codec.go @@ -4,6 +4,9 @@ package message import ( + "fmt" + "sync" + "github.com/ava-labs/avalanchego/codec" "github.com/ava-labs/avalanchego/codec/linearcodec" "github.com/ava-labs/avalanchego/utils/units" @@ -15,32 +18,43 @@ const ( maxMessageSize = 2*units.MiB - 64*units.KiB // Subtract 64 KiB from p2p network cap to leave room for encoding overhead from AvalancheGo ) -var Codec codec.Manager - -func init() { - Codec = codec.NewManager(maxMessageSize) - c := linearcodec.NewDefault() - - errs := wrappers.Errs{} - // Gossip types and sync summary type removed from codec - c.SkipRegistrations(3) - errs.Add( - // state sync types - c.RegisterType(BlockRequest{}), - c.RegisterType(BlockResponse{}), - c.RegisterType(LeafsRequest{}), - c.RegisterType(LeafsResponse{}), - c.RegisterType(CodeRequest{}), - c.RegisterType(CodeResponse{}), - ) - - // Deprecated Warp request/responde types are skipped - // See https://github.com/ava-labs/coreth/pull/999 - c.SkipRegistrations(3) - - errs.Add(Codec.RegisterCodec(Version, c)) - - if errs.Errored() { - panic(errs.Err) - } +var ( + codecOnce sync.Once + manager codec.Manager +) + +// Codec returns the codec manager for this package, initializing it lazily on first access. +// This avoids using init() and initializes the codec only when needed. +// NOTE: Panics if codec initialization fails (e.g., duplicate type registration). +// Such errors indicate programming bugs and should never occur at runtime. +func Codec() codec.Manager { + codecOnce.Do(func() { + c := codec.NewManager(maxMessageSize) + lc := linearcodec.NewDefault() + + errs := wrappers.Errs{} + // Gossip types and sync summary type removed from codec + lc.SkipRegistrations(3) + errs.Add( + // state sync types + lc.RegisterType(BlockRequest{}), + lc.RegisterType(BlockResponse{}), + lc.RegisterType(LeafsRequest{}), + lc.RegisterType(LeafsResponse{}), + lc.RegisterType(CodeRequest{}), + lc.RegisterType(CodeResponse{}), + ) + + // Deprecated Warp request/response types are skipped + // See https://github.com/ava-labs/coreth/pull/999 + lc.SkipRegistrations(3) + + errs.Add(c.RegisterCodec(Version, lc)) + + if errs.Errored() { + panic(fmt.Errorf("failed to initialize message codec: %w", errs.Err)) + } + manager = c + }) + return manager } diff --git a/vms/evm/sync/message/codec_test.go b/vms/evm/sync/message/codec_test.go new file mode 100644 index 000000000000..77d5cafecdfb --- /dev/null +++ b/vms/evm/sync/message/codec_test.go @@ -0,0 +1,46 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package message + +import ( + "sync" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCodec_LazyInitialization(t *testing.T) { + t.Parallel() + + // First call should initialize. + c1 := Codec() + require.NotNil(t, c1) + + // Subsequent calls should return the same instance. + c2 := Codec() + require.Equal(t, c1, c2) +} + +func TestCodec_ThreadSafety(t *testing.T) { + t.Parallel() + + const numGoroutines = 100 + var wg sync.WaitGroup + results := make([]any, numGoroutines) + + wg.Add(numGoroutines) + for i := 0; i < numGoroutines; i++ { + go func(idx int) { + defer wg.Done() + results[idx] = Codec() + }(i) + } + wg.Wait() + + // All results should be the same instance. + first := results[0] + for i := 1; i < numGoroutines; i++ { + require.Equal(t, first, results[i], "all goroutines should get the same codec instance") + } +} diff --git a/vms/evm/sync/message/handler.go b/vms/evm/sync/message/handler.go deleted file mode 100644 index 42a5319249c9..000000000000 --- a/vms/evm/sync/message/handler.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package message - -import ( - "context" - - "github.com/ava-labs/avalanchego/ids" -) - -var _ RequestHandler = NoopRequestHandler{} - -// RequestHandler interface handles incoming requests from peers -// Must have methods in format of handleType(context.Context, ids.NodeID, uint32, request Type) error -// so that the Request object of relevant Type can invoke its respective handle method -// on this struct. -type RequestHandler interface { - HandleLeafsRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, leafsRequest LeafsRequest) ([]byte, error) - HandleBlockRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, request BlockRequest) ([]byte, error) - HandleCodeRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, codeRequest CodeRequest) ([]byte, error) -} - -// ResponseHandler handles response for a sent request -// Only one of OnResponse or OnFailure is called for a given requestID, not both -type ResponseHandler interface { - // OnResponse is invoked when the peer responded to a request - OnResponse(response []byte) error - // OnFailure is invoked when there was a failure in processing a request - OnFailure() error -} - -type NoopRequestHandler struct{} - -func (NoopRequestHandler) HandleLeafsRequest(context.Context, ids.NodeID, uint32, LeafsRequest) ([]byte, error) { - return nil, nil -} - -func (NoopRequestHandler) HandleBlockRequest(context.Context, ids.NodeID, uint32, BlockRequest) ([]byte, error) { - return nil, nil -} - -func (NoopRequestHandler) HandleCodeRequest(context.Context, ids.NodeID, uint32, CodeRequest) ([]byte, error) { - return nil, nil -} diff --git a/vms/evm/sync/message/leafs_request.go b/vms/evm/sync/message/leafs_request.go index 466cc107e97d..2a79d6a25bbc 100644 --- a/vms/evm/sync/message/leafs_request.go +++ b/vms/evm/sync/message/leafs_request.go @@ -17,20 +17,23 @@ const ( // bandwidth and CPU for verification. MaxCodeHashesPerRequest = 5 - StateTrieNode = NodeType(1) + // StateTrieNode represents a node in the state trie. + StateTrieNode = NodeType(1) + + // StateTrieKeyLength is the length of a state trie key. StateTrieKeyLength = common.HashLength ) var _ Request = (*LeafsRequest)(nil) -// NodeType outlines the trie that a leaf node belongs to -// handlers.LeafsRequestHandler uses this information to determine -// which trie type to fetch the information from +// NodeType indicates which trie a leaf node belongs to. +// This is used by handlers to determine which trie to read from (state/atomic). type NodeType uint8 -// LeafsRequest is a request to receive trie leaves at specified Root within Start and End byte range -// Limit outlines maximum number of leaves to returns starting at Start -// NodeType outlines which trie to read from state/atomic. +// LeafsRequest is a request to retrieve trie leaves at the specified root +// within the Start and End byte range. +// Limit specifies the maximum number of leaves to return starting at Start. +// NodeType indicates which trie to read from (state/atomic). type LeafsRequest struct { Root common.Hash `serialize:"true"` Account common.Hash `serialize:"true"` diff --git a/vms/evm/sync/message/leafs_request_test.go b/vms/evm/sync/message/leafs_request_test.go index 2e168c915bc2..98d8a429e005 100644 --- a/vms/evm/sync/message/leafs_request_test.go +++ b/vms/evm/sync/message/leafs_request_test.go @@ -15,6 +15,8 @@ import ( // TestMarshalLeafsRequest requires that the structure or serialization logic hasn't changed, primarily to // ensure compatibility with the network. func TestMarshalLeafsRequest(t *testing.T) { + t.Parallel() + startBytes := make([]byte, common.HashLength) endBytes := make([]byte, common.HashLength) @@ -28,6 +30,7 @@ func TestMarshalLeafsRequest(t *testing.T) { leafsRequest := LeafsRequest{ Root: common.BytesToHash([]byte("im ROOTing for ya")), + Account: common.Hash{}, // Account defaults to zero value Start: startBytes, End: endBytes, Limit: 1024, @@ -36,12 +39,12 @@ func TestMarshalLeafsRequest(t *testing.T) { base64LeafsRequest := "AAAAAAAAAAAAAAAAAAAAAABpbSBST09UaW5nIGZvciB5YQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIFL9/AchgmVPFj9fD5piHXKVZsdNEAN8TXu7BAfR4sZJAAAAIIGFWthoHQ2G0ekeABZ5OctmlNLEIqzSCKAHKTlIf2mZBAAB" - leafsRequestBytes, err := Codec.Marshal(Version, leafsRequest) + leafsRequestBytes, err := Codec().Marshal(Version, leafsRequest) require.NoError(t, err) require.Equal(t, base64LeafsRequest, base64.StdEncoding.EncodeToString(leafsRequestBytes)) var l LeafsRequest - _, err = Codec.Unmarshal(leafsRequestBytes, &l) + _, err = Codec().Unmarshal(leafsRequestBytes, &l) require.NoError(t, err) require.Equal(t, leafsRequest.Root, l.Root) require.Equal(t, leafsRequest.Start, l.Start) @@ -53,6 +56,8 @@ func TestMarshalLeafsRequest(t *testing.T) { // TestMarshalLeafsResponse requires that the structure or serialization logic hasn't changed, primarily to // ensure compatibility with the network. func TestMarshalLeafsResponse(t *testing.T) { + t.Parallel() + keysBytes := make([][]byte, 16) valsBytes := make([][]byte, 16) r := rand.New(rand.NewSource(1)) //nolint:gosec // deterministic bytes for golden assertion @@ -62,15 +67,13 @@ func TestMarshalLeafsResponse(t *testing.T) { valsBytes[i] = make([]byte, n+8) _, err := r.Read(keysBytes[i]) require.NoError(t, err) - _, err = r.Read(valsBytes[i]) require.NoError(t, err) } nextKey := make([]byte, common.HashLength) - if _, e := r.Read(nextKey); e != nil { - require.NoError(t, e) - } + _, err := r.Read(nextKey) + require.NoError(t, err) require.NotEmpty(t, nextKey) proofVals := make([][]byte, 4) @@ -78,9 +81,8 @@ func TestMarshalLeafsResponse(t *testing.T) { for i := range proofVals { n := r2.Intn(8) proofVals[i] = make([]byte, n+8) - if _, e := r2.Read(proofVals[i]); e != nil { - require.NoError(t, e) - } + _, err := r2.Read(proofVals[i]) + require.NoError(t, err) } leafsResponse := LeafsResponse{ @@ -92,12 +94,12 @@ func TestMarshalLeafsResponse(t *testing.T) { base64LeafsResponse := "AAAAAAAQAAAAIE8WP18PmmIdcpVmx00QA3xNe7sEB9HixkmBhVrYaB0NAAAAIGagByk5SH9pmeudGKRHhARdh/PGfPInRumVr1olNnlRAAAAIK2zfFghtmgLTnyLdjobHUnUlVyEhiFjJSU/7HON16niAAAAIIYVu9oIMfUFmHWSHmaKW98sf8SERZLSVyvNBmjS1sUvAAAAIHHb2Wiw9xcu2FeUuzWLDDtSXaF4b5//CUJ52xlE69ehAAAAIPhMiSs77qX090OR9EXRWv1ClAQDdPaSS5jL+HE/jZYtAAAAIMr8yuOmvI+effHZKTM/+ZOTO+pvWzr23gN0NmxHGeQ6AAAAIBZZpE856x5YScYHfbtXIvVxeiiaJm+XZHmBmY6+qJwLAAAAIHOq53hmZ/fpNs1PJKv334ZrqlYDg2etYUXeHuj0qLCZAAAAIHiN5WOvpGfUnexqQOmh0AfwM8KCMGG90Oqln45NpkMBAAAAIKAQ13yW6oCnpmX2BvamO389/SVnwYl55NYPJmhtm/L7AAAAIAfuKbpk+Eq0PKDG5rkcH9O+iZBDQXnTr0SRo2kBLbktAAAAILsXyQKL6ZFOt2ScbJNHgAl50YMDVvKlTD3qsqS0R11jAAAAIOqxOTXzHYRIRRfpJK73iuFRwAdVklg2twdYhWUMMOwpAAAAIHnqPf5BNqv3UrO4Jx0D6USzyds2a3UEX479adIq5UEZAAAAIDLWEMqsbjP+qjJjo5lDcCS6nJsUZ4onTwGpEK4pX277AAAAEAAAAAmG0ekeABZ5OcsAAAAMuqL/bNRxxIPxX7kLAAAACov5IRGcFg8HAkQAAAAIUFTi0INr+EwAAAAOnQ97usvgJVqlt9RL7EAAAAAJfI0BkZLCQiTiAAAACxsGfYm8fwHx9XOYAAAADUs3OXARXoLtb0ElyPoAAAAKPr34iDoK2L6cOQAAAAoFIg0LKWiLc0uOAAAACCbJAf81TN4WAAAADBhPw50XNP9XFkKJUwAAAAuvvo+1aYfHf1gYUgAAAAqjcDk0v1CijaECAAAADkfLVT12lCZ670686kBrAAAADf5fWr9EzN4mO1YGYz4AAAAEAAAACm8xRMCqTO1W29kAAAAIZ9wol8oW4YsAAAAOaGugcKI9oAJrZhCPutAAAAAPhENjuCNqN/goPvsnNn9u" - leafsResponseBytes, err := Codec.Marshal(Version, leafsResponse) + leafsResponseBytes, err := Codec().Marshal(Version, leafsResponse) require.NoError(t, err) require.Equal(t, base64LeafsResponse, base64.StdEncoding.EncodeToString(leafsResponseBytes)) var l LeafsResponse - _, err = Codec.Unmarshal(leafsResponseBytes, &l) + _, err = Codec().Unmarshal(leafsResponseBytes, &l) require.NoError(t, err) require.Equal(t, leafsResponse.Keys, l.Keys) require.Equal(t, leafsResponse.Vals, l.Vals) diff --git a/vms/evm/sync/message/request.go b/vms/evm/sync/message/request.go index b4149311390a..657eecca71f1 100644 --- a/vms/evm/sync/message/request.go +++ b/vms/evm/sync/message/request.go @@ -21,7 +21,42 @@ type Request interface { Handle(ctx context.Context, nodeID ids.NodeID, requestID uint32, handler RequestHandler) ([]byte, error) } -// RequestToBytes marshals the given request object into bytes. -func RequestToBytes(codec codec.Manager, request Request) ([]byte, error) { - return codec.Marshal(Version, &request) +// RequestToBytes marshals the given request object into bytes using the provided codec. +func RequestToBytes(c codec.Manager, request Request) ([]byte, error) { + return c.Marshal(Version, &request) +} + +var _ RequestHandler = NoopRequestHandler{} + +// RequestHandler handles incoming requests from peers. +// Each request type has a corresponding handler method that processes the request +// and returns a response or an error. +type RequestHandler interface { + HandleLeafsRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, leafsRequest LeafsRequest) ([]byte, error) + HandleBlockRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, request BlockRequest) ([]byte, error) + HandleCodeRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, codeRequest CodeRequest) ([]byte, error) +} + +// ResponseHandler handles responses for sent requests. +// Only one of [ResponseHandler.OnResponse] or [ResponseHandler.OnFailure] is called for a given requestID, not both. +type ResponseHandler interface { + // OnResponse is invoked when the peer responded to a request. + OnResponse(response []byte) error + // OnFailure is invoked when there was a failure in processing a request. + OnFailure() error +} + +// NoopRequestHandler is a no-op implementation of RequestHandler that does nothing. +type NoopRequestHandler struct{} + +func (NoopRequestHandler) HandleLeafsRequest(context.Context, ids.NodeID, uint32, LeafsRequest) ([]byte, error) { + return nil, nil +} + +func (NoopRequestHandler) HandleBlockRequest(context.Context, ids.NodeID, uint32, BlockRequest) ([]byte, error) { + return nil, nil +} + +func (NoopRequestHandler) HandleCodeRequest(context.Context, ids.NodeID, uint32, CodeRequest) ([]byte, error) { + return nil, nil } diff --git a/vms/evm/sync/message/request_test.go b/vms/evm/sync/message/request_test.go index 814b7fe3dfec..532c28f50d4e 100644 --- a/vms/evm/sync/message/request_test.go +++ b/vms/evm/sync/message/request_test.go @@ -70,7 +70,7 @@ func (r *recordingHandler) HandleCodeRequest(ctx context.Context, nodeID ids.Nod func TestRequest_HandleDispatchesToCorrectHandler(t *testing.T) { t.Parallel() - ctx := context.TODO() + ctx := t.Context() nodeID := ids.EmptyNodeID const requestID uint32 = 42 @@ -155,13 +155,39 @@ func TestRequestToBytes_InterfaceRoundTrip(t *testing.T) { for _, c := range cases { t.Run(c.name, func(t *testing.T) { - b, err := RequestToBytes(Codec, c.req) + b, err := RequestToBytes(Codec(), c.req) require.NoError(t, err) var out Request - _, err = Codec.Unmarshal(b, &out) + _, err = Codec().Unmarshal(b, &out) require.NoError(t, err) require.IsType(t, c.req, out) }) } } + +func TestNoopRequestHandler(t *testing.T) { + t.Parallel() + + handler := NoopRequestHandler{} + ctx := t.Context() + nodeID := ids.EmptyNodeID + + t.Run("HandleLeafsRequest", func(t *testing.T) { + resp, err := handler.HandleLeafsRequest(ctx, nodeID, 1, LeafsRequest{}) + require.NoError(t, err) + require.Nil(t, resp) + }) + + t.Run("HandleBlockRequest", func(t *testing.T) { + resp, err := handler.HandleBlockRequest(ctx, nodeID, 1, BlockRequest{}) + require.NoError(t, err) + require.Nil(t, resp) + }) + + t.Run("HandleCodeRequest", func(t *testing.T) { + resp, err := handler.HandleCodeRequest(ctx, nodeID, 1, CodeRequest{}) + require.NoError(t, err) + require.Nil(t, resp) + }) +} diff --git a/vms/evm/sync/message/syncable.go b/vms/evm/sync/message/syncable.go deleted file mode 100644 index 3a3690b8f9c9..000000000000 --- a/vms/evm/sync/message/syncable.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package message - -import ( - "github.com/ava-labs/libevm/common" - - "github.com/ava-labs/avalanchego/snow/engine/snowman/block" -) - -type Syncable interface { - block.StateSummary - GetBlockHash() common.Hash - GetBlockRoot() common.Hash -} - -type SyncableParser interface { - Parse(summaryBytes []byte, acceptImpl AcceptImplFn) (Syncable, error) -} - -type AcceptImplFn func(Syncable) (block.StateSyncMode, error)