mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-05-24 08:49:29 +00:00
Merge e18088ea6e into 12eabbd76d
This commit is contained in:
commit
4a887f5854
21 changed files with 3323 additions and 1623 deletions
|
|
@ -84,6 +84,19 @@ func (s *Suite) dialSnap() (*Conn, error) {
|
|||
return conn, nil
|
||||
}
|
||||
|
||||
// dialSnap2 creates a connection advertising snap/2 as the only snap capability.
|
||||
// This is used by the snap/2 (EIP-8189) test suite to force the peer to
|
||||
// negotiate snap/2 rather than falling back to snap/1.
|
||||
func (s *Suite) dialSnap2() (*Conn, error) {
|
||||
conn, err := s.dial()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dial failed: %v", err)
|
||||
}
|
||||
conn.caps = append(conn.caps, p2p.Cap{Name: "snap", Version: 2})
|
||||
conn.ourHighestSnapProtoVersion = 2
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// Conn represents an individual connection with a peer
|
||||
type Conn struct {
|
||||
*rlpx.Conn
|
||||
|
|
@ -183,7 +196,10 @@ func (c *Conn) ReadEth() (any, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// ReadSnap reads a snap/1 response with the given id from the connection.
|
||||
// ReadSnap reads a snap protocol response from the connection. It decodes
|
||||
// the full message catalog of both snap/1 and snap/2. The caller is
|
||||
// expected to only receive codes that were actually valid on the
|
||||
// negotiated protocol version.
|
||||
func (c *Conn) ReadSnap() (any, error) {
|
||||
c.SetReadDeadline(time.Now().Add(timeout))
|
||||
for {
|
||||
|
|
@ -215,6 +231,10 @@ func (c *Conn) ReadSnap() (any, error) {
|
|||
msg = new(snap.GetTrieNodesPacket)
|
||||
case snap.TrieNodesMsg:
|
||||
msg = new(snap.TrieNodesPacket)
|
||||
case snap.GetAccessListsMsg:
|
||||
msg = new(snap.GetAccessListsPacket)
|
||||
case snap.AccessListsMsg:
|
||||
msg = new(snap.AccessListsPacket)
|
||||
default:
|
||||
panic(fmt.Errorf("unhandled snap code: %d", code))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,11 @@ const (
|
|||
const (
|
||||
baseProtoLen = 16
|
||||
ethProtoLen = 18
|
||||
snapProtoLen = 8
|
||||
// snapProtoLen accommodates snap/2 (EIP-8189) which extends snap/1 with two
|
||||
// additional message codes (GetBlockAccessLists=0x08, BlockAccessLists=0x09).
|
||||
// Using 10 is safe for snap/1 connections because the extra codes are simply
|
||||
// never used on that protocol version.
|
||||
snapProtoLen = 10
|
||||
)
|
||||
|
||||
// Unexported handshake structure from p2p/peer.go.
|
||||
|
|
|
|||
399
cmd/devp2p/internal/ethtest/snap2.go
Normal file
399
cmd/devp2p/internal/ethtest/snap2.go
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
// Copyright 2026 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package ethtest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/core/types/bal"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/eth/protocols/snap"
|
||||
"github.com/ethereum/go-ethereum/internal/utesting"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
)
|
||||
|
||||
// Snap/2 (EIP-8189) replaces trie node healing with BAL-based state catch-up.
|
||||
// It keeps 0x00..0x05 (AccountRange/StorageRanges/ByteCodes) unchanged, removes
|
||||
// GetTrieNodes (0x06) / TrieNodes (0x07), and adds GetBlockAccessLists (0x08) /
|
||||
// BlockAccessLists (0x09).
|
||||
//
|
||||
// The tests in this file focus on the wire behavior that is new or changed in
|
||||
// snap/2. Tests for the unchanged messages are already covered by the snap/1
|
||||
// suite in snap.go; the harness reuses the same code paths because those
|
||||
// message formats are identical across versions.
|
||||
|
||||
// TestSnap2Status performs an RLPx+eth+snap/2 handshake against the node,
|
||||
// verifying that the node advertises and negotiates snap/2.
|
||||
func (s *Suite) TestSnap2Status(t *utesting.T) {
|
||||
t.Log(`This test performs a snap/2 (EIP-8189) handshake. The peer is expected to
|
||||
advertise snap/2 as a p2p capability and accept the connection.`)
|
||||
|
||||
conn, err := s.dialSnap2()
|
||||
if err != nil {
|
||||
t.Fatalf("dial failed: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
if err := conn.peer(s.chain, nil); err != nil {
|
||||
t.Fatalf("peering failed: %v", err)
|
||||
}
|
||||
if conn.negotiatedSnapProtoVersion != 2 {
|
||||
t.Fatalf("unexpected negotiated snap version: got %d, want 2", conn.negotiatedSnapProtoVersion)
|
||||
}
|
||||
}
|
||||
|
||||
type accessListsTest struct {
|
||||
nBytes uint64
|
||||
hashes []common.Hash
|
||||
|
||||
// minEntries/maxEntries bound the number of entries the response list
|
||||
// MUST contain. Per EIP-8189 the server may truncate from the tail when
|
||||
// the byte soft limit is reached, but MUST preserve request order.
|
||||
minEntries int
|
||||
maxEntries int
|
||||
|
||||
// expectAllEmpty is set for requests where every entry in the response is
|
||||
// expected to be the RLP empty string (e.g. random/unknown hashes, or
|
||||
// pre-Amsterdam blocks for which BALs are not available).
|
||||
expectAllEmpty bool
|
||||
|
||||
// mustBeEmptyAt lists positions (zero-based indices into hashes) that MUST
|
||||
// be returned as the RLP empty string. Used to assert that unknown hashes
|
||||
// in mixed requests do not receive fabricated BAL data.
|
||||
mustBeEmptyAt []int
|
||||
|
||||
desc string
|
||||
}
|
||||
|
||||
// TestSnap2GetBlockAccessLists exercises various forms of GetBlockAccessLists
|
||||
// requests defined in EIP-8189. Per the spec:
|
||||
//
|
||||
// - Nodes MUST always respond.
|
||||
// - Unavailable BALs are returned as the RLP empty string (0x80) at the
|
||||
// matching position.
|
||||
// - The server MAY return fewer entries than requested (respecting the byte
|
||||
// soft limit or QoS limits), truncating from the tail.
|
||||
// - Returned entries MUST preserve request order.
|
||||
// - When a BAL is returned, its keccak256(rlp.encode(bal)) MUST match the
|
||||
// block-access-list-hash field of the corresponding block header.
|
||||
func (s *Suite) TestSnap2GetBlockAccessLists(t *utesting.T) {
|
||||
var (
|
||||
head = s.chain.Head()
|
||||
headHash = head.Hash()
|
||||
preHash = s.chain.blocks[s.chain.Len()-2].Hash()
|
||||
unknown = common.HexToHash("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
|
||||
)
|
||||
|
||||
// Collect a window of recent canonical block hashes. Limit to at most 16
|
||||
// entries to keep the request small and well under any reasonable limit.
|
||||
var recent []common.Hash
|
||||
start := s.chain.Len() - 16
|
||||
if start < 1 {
|
||||
start = 1
|
||||
}
|
||||
for i := start; i < s.chain.Len(); i++ {
|
||||
recent = append(recent, s.chain.blocks[i].Hash())
|
||||
}
|
||||
|
||||
tests := []accessListsTest{
|
||||
{
|
||||
desc: `An empty request. The server must respond with an empty list and must
|
||||
not disconnect.`,
|
||||
nBytes: softResponseLimitSnap,
|
||||
hashes: nil,
|
||||
minEntries: 0,
|
||||
maxEntries: 0,
|
||||
expectAllEmpty: true,
|
||||
},
|
||||
{
|
||||
desc: `A request for a single random/unknown block hash. Per the spec the
|
||||
server must respond and include an RLP empty string (0x80) at that position.`,
|
||||
nBytes: softResponseLimitSnap,
|
||||
hashes: []common.Hash{unknown},
|
||||
minEntries: 1,
|
||||
maxEntries: 1,
|
||||
expectAllEmpty: true,
|
||||
},
|
||||
{
|
||||
desc: `A request for multiple random/unknown block hashes. The server must
|
||||
preserve request order and return an RLP empty string for each position.`,
|
||||
nBytes: softResponseLimitSnap,
|
||||
hashes: []common.Hash{
|
||||
unknown,
|
||||
common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000001"),
|
||||
common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000002"),
|
||||
},
|
||||
minEntries: 3,
|
||||
maxEntries: 3,
|
||||
expectAllEmpty: true,
|
||||
},
|
||||
{
|
||||
desc: `A request for the chain head. The server must respond. If the node is
|
||||
post-Amsterdam and has the BAL for this block, the returned BAL must hash to
|
||||
the block-access-list-hash in the header. Otherwise an empty entry is valid.`,
|
||||
nBytes: softResponseLimitSnap,
|
||||
hashes: []common.Hash{headHash},
|
||||
minEntries: 1,
|
||||
maxEntries: 1,
|
||||
},
|
||||
{
|
||||
desc: `A request for the chain head and its parent. The server must return
|
||||
exactly two entries, in request order.`,
|
||||
nBytes: softResponseLimitSnap,
|
||||
hashes: []common.Hash{headHash, preHash},
|
||||
minEntries: 2,
|
||||
maxEntries: 2,
|
||||
},
|
||||
{
|
||||
desc: `A mixed request with known and unknown hashes. The server must
|
||||
return entries in request order, with the RLP empty string at positions
|
||||
corresponding to unknown hashes.`,
|
||||
nBytes: softResponseLimitSnap,
|
||||
hashes: []common.Hash{headHash, unknown, preHash, unknown},
|
||||
// We expect exactly 4 entries — mixed responses are small and well
|
||||
// under the byte limit, so truncation is not expected.
|
||||
minEntries: 4,
|
||||
maxEntries: 4,
|
||||
// Positions 1 and 3 are unknown hashes and MUST be empty.
|
||||
mustBeEmptyAt: []int{1, 3},
|
||||
},
|
||||
{
|
||||
desc: `A request spanning the most recent canonical window. Implementations
|
||||
may serve or drop individual entries, but the entries that are returned must
|
||||
preserve request order.`,
|
||||
nBytes: softResponseLimitSnap,
|
||||
hashes: recent,
|
||||
minEntries: 0,
|
||||
maxEntries: len(recent),
|
||||
},
|
||||
{
|
||||
desc: `A request with a very small byte soft limit. The server must return
|
||||
at least zero entries and no more than the requested number, truncating from
|
||||
the tail. It must not disconnect.`,
|
||||
nBytes: 1,
|
||||
hashes: recent,
|
||||
minEntries: 0,
|
||||
maxEntries: len(recent),
|
||||
},
|
||||
{
|
||||
desc: `A request with a zero byte soft limit. The server must still respond
|
||||
(possibly with an empty list) and must not disconnect.`,
|
||||
nBytes: 0,
|
||||
hashes: recent,
|
||||
minEntries: 0,
|
||||
maxEntries: len(recent),
|
||||
},
|
||||
{
|
||||
desc: `A request containing the same hash repeated. The server must treat
|
||||
each position independently and preserve request order.`,
|
||||
nBytes: softResponseLimitSnap,
|
||||
hashes: []common.Hash{headHash, headHash, headHash},
|
||||
minEntries: 3,
|
||||
maxEntries: 3,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range tests {
|
||||
if i > 0 {
|
||||
t.Log("\n")
|
||||
}
|
||||
t.Logf("-- Test %d", i)
|
||||
t.Log(tc.desc)
|
||||
t.Log(" request:")
|
||||
t.Logf(" hashes: %d", len(tc.hashes))
|
||||
t.Logf(" responseBytes: %d", tc.nBytes)
|
||||
if err := s.snapGetAccessLists(t, &tc); err != nil {
|
||||
t.Errorf("test %d failed: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSnap2TrieNodesRemoved verifies that snap/2 no longer serves the
|
||||
// GetTrieNodes message (0x06). Per EIP-8189, snap/2 removes GetTrieNodes and
|
||||
// TrieNodes entirely. A server that negotiated snap/2 must not treat these
|
||||
// codes as valid snap messages and should disconnect the peer that sends them.
|
||||
func (s *Suite) TestSnap2TrieNodesRemoved(t *utesting.T) {
|
||||
t.Log(`This test verifies that sending a GetTrieNodes message over a snap/2
|
||||
connection causes the peer to reject the request. Per EIP-8189, GetTrieNodes
|
||||
is removed in snap/2.`)
|
||||
|
||||
conn, err := s.dialSnap2()
|
||||
if err != nil {
|
||||
t.Fatalf("dial failed: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
if err := conn.peer(s.chain, nil); err != nil {
|
||||
t.Fatalf("peering failed: %v", err)
|
||||
}
|
||||
|
||||
// Build a syntactically valid GetTrieNodes request to the head state root.
|
||||
paths, err := rlp.EncodeToRawList([]snap.TrieNodePathSet{{[]byte{0}}})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to encode paths: %v", err)
|
||||
}
|
||||
req := &snap.GetTrieNodesPacket{
|
||||
ID: uint64(rand.Int63()),
|
||||
Root: s.chain.Head().Root(),
|
||||
Paths: paths,
|
||||
Bytes: 5000,
|
||||
}
|
||||
if err := conn.Write(snapProto, snap.GetTrieNodesMsg, req); err != nil {
|
||||
t.Fatalf("failed to write GetTrieNodes: %v", err)
|
||||
}
|
||||
|
||||
// We expect either a disconnect or a read error/timeout. We must NOT
|
||||
// receive a valid TrieNodes response. Loop a few times to consume any
|
||||
// incidental messages the peer might send (e.g. block updates) before
|
||||
// deciding.
|
||||
for i := 0; i < 5; i++ {
|
||||
msg, err := conn.ReadSnap()
|
||||
if err != nil {
|
||||
// Disconnect or read error — the peer rejected the request.
|
||||
return
|
||||
}
|
||||
if _, ok := msg.(*snap.TrieNodesPacket); ok {
|
||||
t.Fatal("peer responded with TrieNodes over snap/2; GetTrieNodes must be unsupported")
|
||||
}
|
||||
}
|
||||
t.Fatal("peer did not reject GetTrieNodes over snap/2 within the observation window")
|
||||
}
|
||||
|
||||
// softResponseLimitSnap mirrors the recommended 2 MiB soft limit for
|
||||
// BlockAccessLists responses from EIP-8189 §"Response Size Limit".
|
||||
const softResponseLimitSnap = 2 * 1024 * 1024
|
||||
|
||||
// snapGetAccessLists sends a GetBlockAccessLists request, validates the
|
||||
// response structure against EIP-8189, and verifies BAL content against the
|
||||
// block-access-list-hash field of the corresponding block header (when the
|
||||
// block is known and a BAL was returned).
|
||||
func (s *Suite) snapGetAccessLists(t *utesting.T, tc *accessListsTest) error {
|
||||
conn, err := s.dialSnap2()
|
||||
if err != nil {
|
||||
return fmt.Errorf("dial failed: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
if err = conn.peer(s.chain, nil); err != nil {
|
||||
return fmt.Errorf("peering failed: %v", err)
|
||||
}
|
||||
|
||||
req := &snap.GetAccessListsPacket{
|
||||
ID: uint64(rand.Int63()),
|
||||
Hashes: tc.hashes,
|
||||
Bytes: tc.nBytes,
|
||||
}
|
||||
msg, err := conn.snapRequest(snap.GetAccessListsMsg, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("access list request failed: %v", err)
|
||||
}
|
||||
res, ok := msg.(*snap.AccessListsPacket)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected response type: %T", msg)
|
||||
}
|
||||
if res.ID != req.ID {
|
||||
return fmt.Errorf("request id mismatch: got %d, want %d", res.ID, req.ID)
|
||||
}
|
||||
|
||||
// Check list length bounds.
|
||||
got := res.AccessLists.Len()
|
||||
if got < tc.minEntries || got > tc.maxEntries {
|
||||
return fmt.Errorf("response has %d entries, want between %d and %d", got, tc.minEntries, tc.maxEntries)
|
||||
}
|
||||
|
||||
// Build a map of request-index -> block so we can verify BAL hashes.
|
||||
blocks := make(map[int]*types.Block)
|
||||
for i, h := range tc.hashes {
|
||||
for _, b := range s.chain.blocks {
|
||||
if b.Hash() == h {
|
||||
blocks[i] = b
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate the response, validating each entry positionally.
|
||||
var (
|
||||
idx int
|
||||
it = res.AccessLists.ContentIterator()
|
||||
)
|
||||
// Build a set of positions that MUST be empty (for per-position checks).
|
||||
mustEmpty := make(map[int]struct{}, len(tc.mustBeEmptyAt))
|
||||
for _, p := range tc.mustBeEmptyAt {
|
||||
mustEmpty[p] = struct{}{}
|
||||
}
|
||||
head := s.chain.Head().Header()
|
||||
rules := s.chain.config.Rules(head.Number, true, head.Time)
|
||||
|
||||
for it.Next() {
|
||||
raw := it.Value()
|
||||
|
||||
// Empty entry: per spec, indicates BAL is unavailable for that block.
|
||||
if bytes.Equal(raw, rlp.EmptyString) {
|
||||
if !tc.expectAllEmpty && blocks[idx] != nil && blocks[idx].Header().BlockAccessListHash != nil {
|
||||
// Not a failure — the server is allowed to legitimately not
|
||||
// have the BAL. But we log it so the test output is diagnosable.
|
||||
t.Logf(" entry %d: server returned empty for known post-Amsterdam block %x", idx, tc.hashes[idx])
|
||||
}
|
||||
idx++
|
||||
continue
|
||||
}
|
||||
|
||||
// Non-empty entry. If the requester asked for a block we do not know
|
||||
// about, receiving data here is a protocol violation.
|
||||
if tc.expectAllEmpty {
|
||||
return fmt.Errorf("entry %d: expected empty entry, got %d bytes of BAL data", idx, len(raw))
|
||||
}
|
||||
if _, required := mustEmpty[idx]; required {
|
||||
return fmt.Errorf("entry %d: position must be empty (unknown hash), got %d bytes of BAL data", idx, len(raw))
|
||||
}
|
||||
|
||||
// Per EIP-8189: compute keccak256(rlp.encode(bal)) against the raw
|
||||
// bytes actually received on the wire, and compare to the header
|
||||
// commitment. Hashing raw bytes (rather than re-encoding after a
|
||||
// decode round-trip) catches peers that send non-canonical BAL
|
||||
// encodings.
|
||||
block, known := blocks[idx]
|
||||
if known && block.Header().BlockAccessListHash != nil {
|
||||
have := crypto.Keccak256Hash(raw)
|
||||
want := *block.Header().BlockAccessListHash
|
||||
if have != want {
|
||||
return fmt.Errorf("entry %d: BAL hash mismatch: have %x, want %x", idx, have, want)
|
||||
}
|
||||
}
|
||||
|
||||
// Also decode and validate the BAL's internal structure: ordering of
|
||||
// accounts/slots/changes, code-size limits, etc. This catches
|
||||
// malformed responses even when we can't compare to a header hash
|
||||
// (e.g. requested hash is for a block we don't know locally).
|
||||
var accessList bal.BlockAccessList
|
||||
if err := rlp.DecodeBytes(raw, &accessList); err != nil {
|
||||
return fmt.Errorf("entry %d: invalid BAL RLP: %v", idx, err)
|
||||
}
|
||||
if err := accessList.Validate(rules); err != nil {
|
||||
return fmt.Errorf("entry %d: BAL failed validation: %v", idx, err)
|
||||
}
|
||||
idx++
|
||||
}
|
||||
|
||||
// Sanity: iterator consumed exactly the reported number of entries.
|
||||
if idx != got {
|
||||
return fmt.Errorf("iterator visited %d entries, expected %d", idx, got)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -106,6 +106,16 @@ func (s *Suite) SnapTests() []utesting.Test {
|
|||
}
|
||||
}
|
||||
|
||||
// Snap2Tests returns the list of tests for the snap/2 protocol (EIP-8189).
|
||||
// These tests require the peer to advertise and negotiate snap/2.
|
||||
func (s *Suite) Snap2Tests() []utesting.Test {
|
||||
return []utesting.Test{
|
||||
{Name: "Status", Fn: s.TestSnap2Status},
|
||||
{Name: "GetBlockAccessLists", Fn: s.TestSnap2GetBlockAccessLists},
|
||||
{Name: "TrieNodesRemoved", Fn: s.TestSnap2TrieNodesRemoved},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Suite) TestStatus(t *utesting.T) {
|
||||
t.Log(`This test is just a sanity check. It performs an eth protocol handshake.`)
|
||||
conn, err := s.dialAndPeer(nil)
|
||||
|
|
|
|||
|
|
@ -99,6 +99,31 @@ func TestSnapSuite(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSnap2Suite(t *testing.T) {
|
||||
jwtPath, secret, err := makeJWTSecret(t)
|
||||
if err != nil {
|
||||
t.Fatalf("could not make jwt secret: %v", err)
|
||||
}
|
||||
geth, err := runGeth("./testdata", jwtPath)
|
||||
if err != nil {
|
||||
t.Fatalf("could not run geth: %v", err)
|
||||
}
|
||||
defer geth.Close()
|
||||
|
||||
suite, err := NewSuite(geth.Server().Self(), "./testdata", geth.HTTPAuthEndpoint(), common.Bytes2Hex(secret[:]))
|
||||
if err != nil {
|
||||
t.Fatalf("could not create new test suite: %v", err)
|
||||
}
|
||||
for _, test := range suite.Snap2Tests() {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
result := utesting.RunTests([]utesting.Test{{Name: test.Name, Fn: test.Fn}}, os.Stdout)
|
||||
if result[0].Failed {
|
||||
t.Fatal()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// runGeth creates and starts a geth node
|
||||
func runGeth(dir string, jwtPath string) (*node.Node, error) {
|
||||
stack, err := node.New(&node.Config{
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ var (
|
|||
rlpxPingCommand,
|
||||
rlpxEthTestCommand,
|
||||
rlpxSnapTestCommand,
|
||||
rlpxSnap2TestCommand,
|
||||
},
|
||||
}
|
||||
rlpxPingCommand = &cli.Command{
|
||||
|
|
@ -99,6 +100,20 @@ var (
|
|||
testNodeEngineFlag,
|
||||
},
|
||||
}
|
||||
rlpxSnap2TestCommand = &cli.Command{
|
||||
Name: "snap2-test",
|
||||
Usage: "Runs snap/2 (EIP-8189) protocol tests against a node",
|
||||
ArgsUsage: "",
|
||||
Action: rlpxSnap2Test,
|
||||
Flags: []cli.Flag{
|
||||
testPatternFlag,
|
||||
testTAPFlag,
|
||||
testChainDirFlag,
|
||||
testNodeFlag,
|
||||
testNodeJWTFlag,
|
||||
testNodeEngineFlag,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func rlpxPing(ctx *cli.Context) error {
|
||||
|
|
@ -164,6 +179,16 @@ func rlpxSnapTest(ctx *cli.Context) error {
|
|||
return runTests(ctx, suite.SnapTests())
|
||||
}
|
||||
|
||||
// rlpxSnap2Test runs the snap/2 (EIP-8189) protocol test suite.
|
||||
func rlpxSnap2Test(ctx *cli.Context) error {
|
||||
p := cliTestParams(ctx)
|
||||
suite, err := ethtest.NewSuite(p.node, p.chainDir, p.engineAPI, p.jwt)
|
||||
if err != nil {
|
||||
exit(err)
|
||||
}
|
||||
return runTests(ctx, suite.Snap2Tests())
|
||||
}
|
||||
|
||||
type testParams struct {
|
||||
node *enode.Node
|
||||
engineAPI string
|
||||
|
|
|
|||
|
|
@ -1162,7 +1162,7 @@ func (bc *BlockChain) SnapSyncStart() error {
|
|||
// given hash, regardless of the chain contents prior to snap sync. It is
|
||||
// invoked once snap sync completes and assumes that SnapSyncStart was called
|
||||
// previously.
|
||||
func (bc *BlockChain) SnapSyncComplete(hash common.Hash) error {
|
||||
func (bc *BlockChain) SnapSyncComplete(hash common.Hash, flatStateReady bool) error {
|
||||
// Make sure that both the block as well at its state trie exists
|
||||
block := bc.GetBlockByHash(hash)
|
||||
if block == nil {
|
||||
|
|
@ -1173,19 +1173,28 @@ func (bc *BlockChain) SnapSyncComplete(hash common.Hash) error {
|
|||
}
|
||||
defer bc.chainmu.Unlock()
|
||||
|
||||
// Reset the trie database with the fresh snap synced state.
|
||||
// Reset the trie database with the fresh snap synced state. Snap/1 needs
|
||||
// a full trie-to-flat regeneration; snap/2 adopts the already-consistent
|
||||
// flat state and skips that work.
|
||||
root := block.Root()
|
||||
if bc.triedb.Scheme() == rawdb.PathScheme {
|
||||
if err := bc.triedb.Enable(root); err != nil {
|
||||
return err
|
||||
if flatStateReady {
|
||||
if err := bc.triedb.AdoptSyncedState(root); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := bc.triedb.Enable(root); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if !bc.HasState(root) {
|
||||
return fmt.Errorf("non existent state [%x..]", root[:4])
|
||||
}
|
||||
// Destroy any existing state snapshot and regenerate it in the background,
|
||||
// also resuming the normal maintenance of any previously paused snapshot.
|
||||
if bc.snaps != nil {
|
||||
|
||||
// The legacy snapshot tree needs to be wiped and rebuilt from the trie
|
||||
// after a snap/1 sync.
|
||||
if !flatStateReady && bc.snaps != nil {
|
||||
bc.snaps.Rebuild(root)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -208,3 +208,10 @@ func WriteSnapshotSyncStatus(db ethdb.KeyValueWriter, status []byte) {
|
|||
log.Crit("Failed to store snapshot sync status", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteSnapshotSyncStatus removes the serialized sync status from the database.
|
||||
func DeleteSnapshotSyncStatus(db ethdb.KeyValueWriter) {
|
||||
if err := db.Delete(snapshotSyncStatusKey); err != nil {
|
||||
log.Crit("Failed to remove snapshot sync status", "err", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ type BlockChain interface {
|
|||
SnapSyncStart() error
|
||||
|
||||
// SnapSyncComplete directly commits the head block to a certain entity.
|
||||
SnapSyncComplete(common.Hash) error
|
||||
SnapSyncComplete(hash common.Hash, flatStateReady bool) error
|
||||
|
||||
// InsertHeadersBeforeCutoff inserts a batch of headers before the configured
|
||||
// chain cutoff into the ancient store.
|
||||
|
|
@ -278,7 +278,7 @@ func (d *Downloader) Progress() ethereum.SyncProgress {
|
|||
default:
|
||||
log.Error("Unknown downloader mode", "mode", mode)
|
||||
}
|
||||
progress, pending := d.SnapSyncer.Progress()
|
||||
progress := d.SnapSyncer.Progress()
|
||||
|
||||
return ethereum.SyncProgress{
|
||||
StartingBlock: d.syncStatsChainOrigin,
|
||||
|
|
@ -290,12 +290,6 @@ func (d *Downloader) Progress() ethereum.SyncProgress {
|
|||
SyncedBytecodeBytes: uint64(progress.BytecodeBytes),
|
||||
SyncedStorage: progress.StorageSynced,
|
||||
SyncedStorageBytes: uint64(progress.StorageBytes),
|
||||
HealedTrienodes: progress.TrienodeHealSynced,
|
||||
HealedTrienodeBytes: uint64(progress.TrienodeHealBytes),
|
||||
HealedBytecodes: progress.BytecodeHealSynced,
|
||||
HealedBytecodeBytes: uint64(progress.BytecodeHealBytes),
|
||||
HealingTrienodes: pending.TrienodeHeal,
|
||||
HealingBytecode: pending.BytecodeHeal,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -889,7 +883,7 @@ func (d *Downloader) processSnapSyncContent() error {
|
|||
// Start syncing state of the reported head block. This should get us most of
|
||||
// the state of the pivot block.
|
||||
d.pivotLock.RLock()
|
||||
sync := d.syncState(d.pivotHeader.Root)
|
||||
sync := d.syncState(d.pivotHeader)
|
||||
d.pivotLock.RUnlock()
|
||||
|
||||
defer func() {
|
||||
|
|
@ -959,10 +953,9 @@ func (d *Downloader) processSnapSyncContent() error {
|
|||
|
||||
if oldPivot == nil { // no results piling up, we can move the pivot
|
||||
if !d.committed.Load() { // not yet passed the pivot, we can move the pivot
|
||||
if pivot.Root != sync.root { // pivot position changed, we can move the pivot
|
||||
if pivot.Hash() != sync.pivot.Hash() { // pivot position changed, we can move the pivot
|
||||
sync.Cancel()
|
||||
sync = d.syncState(pivot.Root)
|
||||
|
||||
sync = d.syncState(pivot)
|
||||
go closeOnErr(sync)
|
||||
}
|
||||
}
|
||||
|
|
@ -977,8 +970,7 @@ func (d *Downloader) processSnapSyncContent() error {
|
|||
// If new pivot block found, cancel old state retrieval and restart
|
||||
if oldPivot != P {
|
||||
sync.Cancel()
|
||||
sync = d.syncState(P.Header.Root)
|
||||
|
||||
sync = d.syncState(P.Header)
|
||||
go closeOnErr(sync)
|
||||
oldPivot = P
|
||||
}
|
||||
|
|
@ -1070,7 +1062,9 @@ func (d *Downloader) commitPivotBlock(result *fetchResult) error {
|
|||
if _, err := d.blockchain.InsertReceiptChain([]*types.Block{block}, []rlp.RawValue{result.Receipts}, d.ancientLimit); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.blockchain.SnapSyncComplete(block.Hash()); err != nil {
|
||||
|
||||
// TODO JR: This needs to pass trie for snap/2 and false for snap/1
|
||||
if err := d.blockchain.SnapSyncComplete(block.Hash(), true); err != nil {
|
||||
return err
|
||||
}
|
||||
d.committed.Store(true)
|
||||
|
|
@ -1096,7 +1090,12 @@ func (d *Downloader) DeliverSnapPacket(peer *snap.Peer, packet snap.Packet) erro
|
|||
return d.SnapSyncer.OnByteCodes(peer, packet.ID, packet.Codes)
|
||||
|
||||
case *snap.TrieNodesPacket:
|
||||
return d.SnapSyncer.OnTrieNodes(peer, packet.ID, packet.Nodes)
|
||||
// Snap/2 no longer requests trie nodes. Stale responses from
|
||||
// snap/1 peers are silently ignored.
|
||||
return nil
|
||||
|
||||
case *snap.AccessListsPacket:
|
||||
return d.SnapSyncer.OnAccessLists(peer, packet.ID, packet.AccessLists)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unexpected snap packet type: %T", packet)
|
||||
|
|
|
|||
|
|
@ -372,20 +372,15 @@ func (dlp *downloadTesterPeer) RequestByteCodes(id uint64, hashes []common.Hash,
|
|||
return nil
|
||||
}
|
||||
|
||||
// RequestTrieNodes fetches a batch of account or storage trie nodes.
|
||||
func (dlp *downloadTesterPeer) RequestTrieNodes(id uint64, root common.Hash, count int, paths []snap.TrieNodePathSet, bytes int) error {
|
||||
encPaths, err := rlp.EncodeToRawList(paths)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
// RequestAccessLists fetches a batch of BALs by block hash.
|
||||
func (dlp *downloadTesterPeer) RequestAccessLists(id uint64, hashes []common.Hash, bytes int) error {
|
||||
req := &snap.GetAccessListsPacket{
|
||||
ID: id,
|
||||
Hashes: hashes,
|
||||
Bytes: uint64(bytes),
|
||||
}
|
||||
req := &snap.GetTrieNodesPacket{
|
||||
ID: id,
|
||||
Root: root,
|
||||
Paths: encPaths,
|
||||
Bytes: uint64(bytes),
|
||||
}
|
||||
nodes, _ := snap.ServiceGetTrieNodesQuery(dlp.chain, req)
|
||||
go dlp.dl.downloader.SnapSyncer.OnTrieNodes(dlp, id, nodes)
|
||||
als := snap.ServiceGetAccessListsQuery(dlp.chain, req)
|
||||
go dlp.dl.downloader.SnapSyncer.OnAccessLists(dlp, id, als)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,14 +19,14 @@ package downloader
|
|||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// syncState starts downloading state with the given root hash.
|
||||
func (d *Downloader) syncState(root common.Hash) *stateSync {
|
||||
// syncState starts downloading state with the given pivot header.
|
||||
func (d *Downloader) syncState(pivot *types.Header) *stateSync {
|
||||
// Create the state sync
|
||||
s := newStateSync(d, root)
|
||||
s := newStateSync(d, pivot)
|
||||
select {
|
||||
case d.stateSyncStart <- s:
|
||||
// If we tell the statesync to restart with a new root, we also need
|
||||
|
|
@ -58,7 +58,7 @@ func (d *Downloader) stateFetcher() {
|
|||
// runStateSync runs a state synchronisation until it completes or another root
|
||||
// hash is requested to be switched over to.
|
||||
func (d *Downloader) runStateSync(s *stateSync) *stateSync {
|
||||
log.Trace("State sync starting", "root", s.root)
|
||||
log.Trace("State sync starting", "pivot", s.pivot.Hash(), "number", s.pivot.Number)
|
||||
|
||||
go s.run()
|
||||
defer s.Cancel()
|
||||
|
|
@ -75,10 +75,10 @@ func (d *Downloader) runStateSync(s *stateSync) *stateSync {
|
|||
}
|
||||
|
||||
// stateSync schedules requests for downloading a particular state trie defined
|
||||
// by a given state root.
|
||||
// by a given pivot header.
|
||||
type stateSync struct {
|
||||
d *Downloader // Downloader instance to access and manage current peerset
|
||||
root common.Hash // State root currently being synced
|
||||
d *Downloader // Downloader instance to access and manage current peerset
|
||||
pivot *types.Header // Pivot header currently being synced
|
||||
|
||||
started chan struct{} // Started is signalled once the sync loop starts
|
||||
cancel chan struct{} // Channel to signal a termination request
|
||||
|
|
@ -89,10 +89,10 @@ type stateSync struct {
|
|||
|
||||
// newStateSync creates a new state trie download scheduler. This method does not
|
||||
// yet start the sync. The user needs to call run to initiate.
|
||||
func newStateSync(d *Downloader, root common.Hash) *stateSync {
|
||||
func newStateSync(d *Downloader, pivot *types.Header) *stateSync {
|
||||
return &stateSync{
|
||||
d: d,
|
||||
root: root,
|
||||
pivot: pivot,
|
||||
cancel: make(chan struct{}),
|
||||
done: make(chan struct{}),
|
||||
started: make(chan struct{}),
|
||||
|
|
@ -104,7 +104,7 @@ func newStateSync(d *Downloader, root common.Hash) *stateSync {
|
|||
// finish.
|
||||
func (s *stateSync) run() {
|
||||
close(s.started)
|
||||
s.err = s.d.SnapSyncer.Sync(s.root, s.cancel)
|
||||
s.err = s.d.SnapSyncer.Sync(s.pivot, s.cancel)
|
||||
close(s.done)
|
||||
}
|
||||
|
||||
|
|
|
|||
152
eth/protocols/snap/bal_apply.go
Normal file
152
eth/protocols/snap/bal_apply.go
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
// Copyright 2026 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package snap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/core/types/bal"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/holiman/uint256"
|
||||
)
|
||||
|
||||
// verifyAccessList checks that the given block access list matches the hash
|
||||
// committed in the block header.
|
||||
func verifyAccessList(b *bal.BlockAccessList, header *types.Header) error {
|
||||
if header.BlockAccessListHash == nil {
|
||||
return fmt.Errorf("header %d has no access list hash", header.Number)
|
||||
}
|
||||
have := b.Hash()
|
||||
if have != *header.BlockAccessListHash {
|
||||
return fmt.Errorf("access list hash mismatch for block %d: have %v, want %v", header.Number, have, *header.BlockAccessListHash)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isFetched tell us if accountHash has been downloaded.
|
||||
func (s *Syncer) isFetched(accountHash common.Hash) bool {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
for _, task := range s.tasks {
|
||||
if bytes.Compare(accountHash[:], task.Last[:]) <= 0 {
|
||||
return bytes.Compare(accountHash[:], task.Next[:]) < 0
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// applyAccessList applies a single block's access list diffs to the flat state
|
||||
// in the database. For each account, it applies the post-block values (highest
|
||||
// TxIdx entry) for balance, nonce, code, and storage. The storageRoot field is
|
||||
// intentionally left stale. It will be recomputed during the trie rebuild.
|
||||
func (s *Syncer) applyAccessList(b *bal.BlockAccessList) error {
|
||||
batch := s.db.NewBatch()
|
||||
|
||||
// Iterate over all accounts in the access list
|
||||
for _, access := range *b {
|
||||
addr := common.Address(access.Address)
|
||||
accountHash := crypto.Keccak256Hash(addr[:])
|
||||
|
||||
// Skip accounts whose hash range hasn't been downloaded yet.
|
||||
if !s.isFetched(accountHash) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Read the existing account from flat state (may not exist yet)
|
||||
var (
|
||||
account types.StateAccount
|
||||
isNew bool
|
||||
)
|
||||
if data := rawdb.ReadAccountSnapshot(s.db, accountHash); len(data) > 0 {
|
||||
existing, err := types.FullAccount(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode account %v: %w", addr, err)
|
||||
}
|
||||
account = *existing
|
||||
} else {
|
||||
// New account — initialize with defaults
|
||||
isNew = true
|
||||
account.Balance = new(uint256.Int)
|
||||
account.Root = types.EmptyRootHash
|
||||
account.CodeHash = types.EmptyCodeHash[:]
|
||||
}
|
||||
|
||||
// Apply balance change (last entry = post-block state)
|
||||
if n := len(access.BalanceChanges); n > 0 {
|
||||
account.Balance = new(uint256.Int).Set(access.BalanceChanges[n-1].Balance)
|
||||
}
|
||||
|
||||
// Apply nonce change (last entry = post-block state)
|
||||
if n := len(access.NonceChanges); n > 0 {
|
||||
account.Nonce = access.NonceChanges[n-1].Nonce
|
||||
}
|
||||
|
||||
// Apply code change (last entry = post-block state)
|
||||
if n := len(access.CodeChanges); n > 0 {
|
||||
code := access.CodeChanges[n-1].Code
|
||||
if len(code) > 0 {
|
||||
codeHash := crypto.Keccak256(code)
|
||||
rawdb.WriteCode(batch, common.BytesToHash(codeHash), code)
|
||||
account.CodeHash = codeHash
|
||||
} else {
|
||||
account.CodeHash = types.EmptyCodeHash[:]
|
||||
}
|
||||
}
|
||||
|
||||
// Apply storage writes (last entry per slot = post-block state).
|
||||
for _, slotWrites := range access.StorageWrites {
|
||||
if n := len(slotWrites.Accesses); n > 0 {
|
||||
value := slotWrites.Accesses[n-1].ValueAfter
|
||||
slotKey := slotWrites.Slot.Bytes32()
|
||||
storageHash := crypto.Keccak256Hash(slotKey[:])
|
||||
if value.IsZero() {
|
||||
rawdb.DeleteStorageSnapshot(batch, accountHash, storageHash)
|
||||
} else {
|
||||
valBytes := value.Bytes32()
|
||||
rawdb.WriteStorageSnapshot(batch, accountHash, storageHash, valBytes[:])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't create empty accounts in flat state (EIP-161).
|
||||
isEmpty := account.Balance.IsZero() && account.Nonce == 0 &&
|
||||
bytes.Equal(account.CodeHash, types.EmptyCodeHash[:])
|
||||
switch {
|
||||
case isEmpty && isNew:
|
||||
// This handles the case where an account is created and
|
||||
// self-destructed in the same transaction. The BAL will
|
||||
// include it with a balance change to zero, but the account
|
||||
// should not exist in state.
|
||||
continue
|
||||
case isEmpty && !isNew:
|
||||
// Existing account got fully drained (e.g., pre-funded
|
||||
// address that gets deployed to with init code that
|
||||
// self-destructs). Delete the entry so the trie rebuild
|
||||
// doesn't pick it up as an empty leaf.
|
||||
rawdb.DeleteAccountSnapshot(batch, accountHash)
|
||||
continue
|
||||
}
|
||||
|
||||
// Write the updated account (storageRoot intentionally left stale)
|
||||
rawdb.WriteAccountSnapshot(batch, accountHash, types.SlimAccountRLP(account))
|
||||
}
|
||||
return batch.Write()
|
||||
}
|
||||
465
eth/protocols/snap/bal_apply_test.go
Normal file
465
eth/protocols/snap/bal_apply_test.go
Normal file
|
|
@ -0,0 +1,465 @@
|
|||
// Copyright 2026 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package snap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/core/types/bal"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
"github.com/holiman/uint256"
|
||||
)
|
||||
|
||||
// buildTestBAL constructs a BlockAccessList from a ConstructionBlockAccessList
|
||||
// by RLP round-tripping (construction types use unexported encoding types).
|
||||
func buildTestBAL(t *testing.T, cb *bal.ConstructionBlockAccessList) *bal.BlockAccessList {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
if err := cb.EncodeRLP(&buf); err != nil {
|
||||
t.Fatalf("failed to encode BAL: %v", err)
|
||||
}
|
||||
var b bal.BlockAccessList
|
||||
if err := rlp.DecodeBytes(buf.Bytes(), &b); err != nil {
|
||||
t.Fatalf("failed to decode BAL: %v", err)
|
||||
}
|
||||
return &b
|
||||
}
|
||||
|
||||
// TestAccessListVerification checks that verifyAccessList accepts valid BALs
|
||||
// and rejects tampered ones.
|
||||
func TestAccessListVerification(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cb := bal.NewConstructionBlockAccessList()
|
||||
addr := common.HexToAddress("0x01")
|
||||
cb.BalanceChange(0, addr, uint256.NewInt(100))
|
||||
|
||||
b := buildTestBAL(t, &cb)
|
||||
correctHash := b.Hash()
|
||||
|
||||
// Valid: hash matches header
|
||||
header := &types.Header{
|
||||
Number: big.NewInt(1),
|
||||
BlockAccessListHash: &correctHash,
|
||||
}
|
||||
if err := verifyAccessList(b, header); err != nil {
|
||||
t.Fatalf("valid access list rejected: %v", err)
|
||||
}
|
||||
// Invalid: wrong hash in header
|
||||
wrongHash := common.HexToHash("0xdead")
|
||||
badHeader := &types.Header{
|
||||
Number: big.NewInt(1),
|
||||
BlockAccessListHash: &wrongHash,
|
||||
}
|
||||
if err := verifyAccessList(b, badHeader); err == nil {
|
||||
t.Fatal("tampered access list accepted")
|
||||
}
|
||||
// Invalid: no hash in header
|
||||
noHashHeader := &types.Header{
|
||||
Number: big.NewInt(1),
|
||||
}
|
||||
if err := verifyAccessList(b, noHashHeader); err == nil {
|
||||
t.Fatal("header without access list hash accepted")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAccessListApplication verifies that applyAccessList correctly updates
|
||||
// flat state (balance, nonce, code, storage) and leaves storageRoot stale.
|
||||
func TestAccessListApplication(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := rawdb.NewMemoryDatabase()
|
||||
syncer := NewSyncer(db, rawdb.HashScheme)
|
||||
addr := common.HexToAddress("0x01")
|
||||
accountHash := crypto.Keccak256Hash(addr[:])
|
||||
|
||||
// Write an existing account to flat state
|
||||
original := types.StateAccount{
|
||||
Nonce: 5,
|
||||
Balance: uint256.NewInt(1000),
|
||||
Root: common.HexToHash("0xbeef"), // intentionally non-empty
|
||||
CodeHash: types.EmptyCodeHash[:],
|
||||
}
|
||||
rawdb.WriteAccountSnapshot(db, accountHash, types.SlimAccountRLP(original))
|
||||
|
||||
// Write an existing storage slot. The BAL uses raw slot keys, but the
|
||||
// snapshot layer stores slots under keccak256(slot).
|
||||
rawSlot := common.HexToHash("0xaa")
|
||||
slotHash := crypto.Keccak256Hash(rawSlot[:])
|
||||
rawdb.WriteStorageSnapshot(db, accountHash, slotHash, common.HexToHash("0x01").Bytes())
|
||||
|
||||
// Build a BAL that changes balance, nonce, code, and storage
|
||||
cb := bal.NewConstructionBlockAccessList()
|
||||
cb.BalanceChange(0, addr, uint256.NewInt(2000))
|
||||
cb.NonceChange(addr, 0, 6)
|
||||
cb.CodeChange(addr, 0, []byte{0x60, 0x00}) // PUSH1 0x00
|
||||
cb.StorageWrite(0, addr, rawSlot, common.HexToHash("0x02"))
|
||||
b := buildTestBAL(t, &cb)
|
||||
if err := syncer.applyAccessList(b); err != nil {
|
||||
t.Fatalf("applyAccessList failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify account fields updated
|
||||
data := rawdb.ReadAccountSnapshot(db, accountHash)
|
||||
if len(data) == 0 {
|
||||
t.Fatal("account snapshot missing after apply")
|
||||
}
|
||||
updated, err := types.FullAccount(data)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to decode updated account: %v", err)
|
||||
}
|
||||
if updated.Balance.Cmp(uint256.NewInt(2000)) != 0 {
|
||||
t.Errorf("balance wrong: got %v, want 2000", updated.Balance)
|
||||
}
|
||||
if updated.Nonce != 6 {
|
||||
t.Errorf("nonce wrong: got %d, want 6", updated.Nonce)
|
||||
}
|
||||
wantCodeHash := crypto.Keccak256([]byte{0x60, 0x00})
|
||||
if !bytes.Equal(updated.CodeHash, wantCodeHash) {
|
||||
t.Errorf("code hash wrong: got %x, want %x", updated.CodeHash, wantCodeHash)
|
||||
}
|
||||
|
||||
// Verify code was written
|
||||
if code := rawdb.ReadCode(db, common.BytesToHash(wantCodeHash)); !bytes.Equal(code, []byte{0x60, 0x00}) {
|
||||
t.Errorf("code wrong: got %x, want 6000", code)
|
||||
}
|
||||
|
||||
// Verify storage updated
|
||||
storageVal := rawdb.ReadStorageSnapshot(db, accountHash, slotHash)
|
||||
if !bytes.Equal(storageVal, common.HexToHash("0x02").Bytes()) {
|
||||
t.Errorf("storage wrong: got %x, want %x", storageVal, common.HexToHash("0x02").Bytes())
|
||||
}
|
||||
|
||||
// Verify storageRoot left stale (unchanged from original)
|
||||
if updated.Root != original.Root {
|
||||
t.Errorf("storageRoot should be stale: got %v, want %v", updated.Root, original.Root)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAccessListApplicationMultiTx verifies that when an account has multiple
|
||||
// changes at different transaction indices, only the highest index (post-block
|
||||
// state) is applied.
|
||||
func TestAccessListApplicationMultiTx(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := rawdb.NewMemoryDatabase()
|
||||
syncer := NewSyncer(db, rawdb.HashScheme)
|
||||
addr := common.HexToAddress("0x02")
|
||||
accountHash := crypto.Keccak256Hash(addr[:])
|
||||
|
||||
// Write initial account
|
||||
original := types.StateAccount{
|
||||
Nonce: 0,
|
||||
Balance: uint256.NewInt(100),
|
||||
Root: types.EmptyRootHash,
|
||||
CodeHash: types.EmptyCodeHash[:],
|
||||
}
|
||||
rawdb.WriteAccountSnapshot(db, accountHash, types.SlimAccountRLP(original))
|
||||
|
||||
// Build BAL with multiple balance/nonce changes at different tx indices
|
||||
cb := bal.NewConstructionBlockAccessList()
|
||||
cb.BalanceChange(0, addr, uint256.NewInt(200)) // tx 0
|
||||
cb.BalanceChange(3, addr, uint256.NewInt(500)) // tx 3
|
||||
cb.BalanceChange(7, addr, uint256.NewInt(9999)) // tx 7 (final)
|
||||
cb.NonceChange(addr, 0, 1) // tx 0
|
||||
cb.NonceChange(addr, 3, 2) // tx 3
|
||||
cb.NonceChange(addr, 7, 3) // tx 7 (final)
|
||||
b := buildTestBAL(t, &cb)
|
||||
if err := syncer.applyAccessList(b); err != nil {
|
||||
t.Fatalf("applyAccessList failed: %v", err)
|
||||
}
|
||||
data := rawdb.ReadAccountSnapshot(db, accountHash)
|
||||
updated, err := types.FullAccount(data)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to decode updated account: %v", err)
|
||||
}
|
||||
|
||||
// Only the highest tx index values should be applied
|
||||
if updated.Balance.Cmp(uint256.NewInt(9999)) != 0 {
|
||||
t.Errorf("balance wrong: got %v, want 9999", updated.Balance)
|
||||
}
|
||||
if updated.Nonce != 3 {
|
||||
t.Errorf("nonce wrong: got %d, want 3", updated.Nonce)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAccessListApplicationZeroStorage verifies that a BAL slot write with a
|
||||
// zero post-value deletes the snapshot entry instead of writing 32 zero
|
||||
// bytes.
|
||||
func TestAccessListApplicationZeroStorage(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := rawdb.NewMemoryDatabase()
|
||||
syncer := NewSyncer(db, rawdb.HashScheme)
|
||||
addr := common.HexToAddress("0x06")
|
||||
accountHash := crypto.Keccak256Hash(addr[:])
|
||||
|
||||
// Existing account with a non-zero storage slot.
|
||||
original := types.StateAccount{
|
||||
Nonce: 1,
|
||||
Balance: uint256.NewInt(1),
|
||||
Root: types.EmptyRootHash,
|
||||
CodeHash: types.EmptyCodeHash[:],
|
||||
}
|
||||
rawdb.WriteAccountSnapshot(db, accountHash, types.SlimAccountRLP(original))
|
||||
rawSlot := common.HexToHash("0xaa")
|
||||
slotHash := crypto.Keccak256Hash(rawSlot[:])
|
||||
rawdb.WriteStorageSnapshot(db, accountHash, slotHash, common.HexToHash("0x42").Bytes())
|
||||
|
||||
// BAL writes the slot to zero (deletion).
|
||||
cb := bal.NewConstructionBlockAccessList()
|
||||
cb.StorageWrite(0, addr, rawSlot, common.Hash{})
|
||||
b := buildTestBAL(t, &cb)
|
||||
if err := syncer.applyAccessList(b); err != nil {
|
||||
t.Fatalf("applyAccessList failed: %v", err)
|
||||
}
|
||||
|
||||
if val := rawdb.ReadStorageSnapshot(db, accountHash, slotHash); len(val) != 0 {
|
||||
t.Errorf("zeroed slot should have been deleted, got %x", val)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAccessListApplicationNewAccount verifies that applyAccessList creates
|
||||
// new accounts that don't exist in the DB yet.
|
||||
func TestAccessListApplicationNewAccount(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := rawdb.NewMemoryDatabase()
|
||||
syncer := NewSyncer(db, rawdb.HashScheme)
|
||||
|
||||
addr := common.HexToAddress("0x03")
|
||||
accountHash := crypto.Keccak256Hash(addr[:])
|
||||
|
||||
// Verify account doesn't exist
|
||||
if data := rawdb.ReadAccountSnapshot(db, accountHash); len(data) > 0 {
|
||||
t.Fatal("account should not exist yet")
|
||||
}
|
||||
|
||||
// Build BAL for a new account. BAL uses raw slot keys.
|
||||
cb := bal.NewConstructionBlockAccessList()
|
||||
cb.BalanceChange(0, addr, uint256.NewInt(42))
|
||||
cb.NonceChange(addr, 0, 1)
|
||||
rawSlot := common.HexToHash("0xbb")
|
||||
cb.StorageWrite(0, addr, rawSlot, common.HexToHash("0xff"))
|
||||
b := buildTestBAL(t, &cb)
|
||||
if err := syncer.applyAccessList(b); err != nil {
|
||||
t.Fatalf("applyAccessList failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify account was created
|
||||
data := rawdb.ReadAccountSnapshot(db, accountHash)
|
||||
if len(data) == 0 {
|
||||
t.Fatal("account should exist after apply")
|
||||
}
|
||||
account, err := types.FullAccount(data)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to decode new account: %v", err)
|
||||
}
|
||||
if account.Balance.Cmp(uint256.NewInt(42)) != 0 {
|
||||
t.Errorf("balance wrong: got %v, want 42", account.Balance)
|
||||
}
|
||||
if account.Nonce != 1 {
|
||||
t.Errorf("nonce wrong: got %d, want 1", account.Nonce)
|
||||
}
|
||||
if account.Root != types.EmptyRootHash {
|
||||
t.Errorf("root should be empty for new account: got %v", account.Root)
|
||||
}
|
||||
|
||||
// Verify storage was written under keccak256(rawSlot)
|
||||
slotHash := crypto.Keccak256Hash(rawSlot[:])
|
||||
storageVal := rawdb.ReadStorageSnapshot(db, accountHash, slotHash)
|
||||
if !bytes.Equal(storageVal, common.HexToHash("0xff").Bytes()) {
|
||||
t.Errorf("storage wrong: got %x, want %x", storageVal, common.HexToHash("0xff").Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
// TestAccessListApplicationSkipsUnfetched verifies that applyAccessList does
|
||||
// not write account entries for addresses whose hash falls in a range that
|
||||
// hasn't been downloaded yet.
|
||||
func TestAccessListApplicationSkipsUnfetched(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := rawdb.NewMemoryDatabase()
|
||||
syncer := NewSyncer(db, rawdb.HashScheme)
|
||||
|
||||
// Pick two addresses and order them by hash.
|
||||
addrA := common.HexToAddress("0x01")
|
||||
addrB := common.HexToAddress("0x02")
|
||||
hashA := crypto.Keccak256Hash(addrA[:])
|
||||
hashB := crypto.Keccak256Hash(addrB[:])
|
||||
fetchedAddr, fetchedHash := addrA, hashA
|
||||
unfetchedAddr, unfetchedHash := addrB, hashB
|
||||
if bytes.Compare(hashA[:], hashB[:]) > 0 {
|
||||
fetchedAddr, fetchedHash = addrB, hashB
|
||||
unfetchedAddr, unfetchedHash = addrA, hashA
|
||||
}
|
||||
|
||||
// One remaining task covering [unfetchedHash, MaxHash]: the fetched hash
|
||||
// is below Next so isFetched returns true; the unfetched hash equals Next
|
||||
// so isFetched returns false.
|
||||
syncer.tasks = []*accountTask{{
|
||||
Next: unfetchedHash,
|
||||
Last: common.MaxHash,
|
||||
SubTasks: make(map[common.Hash][]*storageTask),
|
||||
stateCompleted: make(map[common.Hash]struct{}),
|
||||
}}
|
||||
|
||||
cb := bal.NewConstructionBlockAccessList()
|
||||
cb.BalanceChange(0, fetchedAddr, uint256.NewInt(100))
|
||||
cb.BalanceChange(0, unfetchedAddr, uint256.NewInt(200))
|
||||
b := buildTestBAL(t, &cb)
|
||||
|
||||
if err := syncer.applyAccessList(b); err != nil {
|
||||
t.Fatalf("applyAccessList failed: %v", err)
|
||||
}
|
||||
|
||||
// The fetched account should have been written.
|
||||
if data := rawdb.ReadAccountSnapshot(db, fetchedHash); len(data) == 0 {
|
||||
t.Error("expected fetched account to be written")
|
||||
}
|
||||
// The unfetched account should not have been touched.
|
||||
if data := rawdb.ReadAccountSnapshot(db, unfetchedHash); len(data) != 0 {
|
||||
t.Errorf("unfetched account should not be written, got %x", data)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAccessListApplicationSkipsUnfetchedStorage verifies that storage writes
|
||||
// are also skipped when the parent account's hash range isn't downloaded yet.
|
||||
func TestAccessListApplicationSkipsUnfetchedStorage(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := rawdb.NewMemoryDatabase()
|
||||
syncer := NewSyncer(db, rawdb.HashScheme)
|
||||
|
||||
addrA := common.HexToAddress("0x01")
|
||||
addrB := common.HexToAddress("0x02")
|
||||
hashA := crypto.Keccak256Hash(addrA[:])
|
||||
hashB := crypto.Keccak256Hash(addrB[:])
|
||||
|
||||
unfetchedAddr, unfetchedHash := addrB, hashB
|
||||
if bytes.Compare(hashA[:], hashB[:]) > 0 {
|
||||
unfetchedAddr, unfetchedHash = addrA, hashA
|
||||
}
|
||||
|
||||
syncer.tasks = []*accountTask{{
|
||||
Next: unfetchedHash,
|
||||
Last: common.MaxHash,
|
||||
SubTasks: make(map[common.Hash][]*storageTask),
|
||||
stateCompleted: make(map[common.Hash]struct{}),
|
||||
}}
|
||||
|
||||
// BAL touches an unfetched account with a storage write AND an empty
|
||||
// balance mutation. Neither should result in any flat-state writes.
|
||||
rawSlot := common.HexToHash("0xaa")
|
||||
slotHash := crypto.Keccak256Hash(rawSlot[:])
|
||||
cb := bal.NewConstructionBlockAccessList()
|
||||
cb.BalanceChange(0, unfetchedAddr, uint256.NewInt(0)) // empty mutation
|
||||
cb.StorageWrite(0, unfetchedAddr, rawSlot, common.HexToHash("0xff"))
|
||||
b := buildTestBAL(t, &cb)
|
||||
|
||||
if err := syncer.applyAccessList(b); err != nil {
|
||||
t.Fatalf("applyAccessList failed: %v", err)
|
||||
}
|
||||
|
||||
if data := rawdb.ReadAccountSnapshot(db, unfetchedHash); len(data) != 0 {
|
||||
t.Errorf("unfetched account should not be written, got %x", data)
|
||||
}
|
||||
if val := rawdb.ReadStorageSnapshot(db, unfetchedHash, slotHash); len(val) != 0 {
|
||||
t.Errorf("storage for unfetched account should not be written, got %x", val)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAccessListApplicationSameTxCreateDestroy tests the edge case where an
|
||||
// account is created and self-destructed in the same transaction during the
|
||||
// pivot gap. Per EIP-7928, such accounts appear in the BAL with a balance
|
||||
// change to zero but no nonce or code changes. Since the account didn't exist
|
||||
// at the old pivot and doesn't exist at the new pivot (destroyed),
|
||||
// applyAccessList should not leave a zero-balance account in the snapshot.
|
||||
// Per EIP-161, empty accounts (zero balance, zero nonce, no code) must not exist
|
||||
// in state.
|
||||
func TestAccessListApplicationSameTxCreateDestroy(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := rawdb.NewMemoryDatabase()
|
||||
syncer := NewSyncer(db, rawdb.HashScheme)
|
||||
addr := common.HexToAddress("0x04")
|
||||
accountHash := crypto.Keccak256Hash(addr[:])
|
||||
|
||||
// Verify account doesn't exist before apply
|
||||
if data := rawdb.ReadAccountSnapshot(db, accountHash); len(data) > 0 {
|
||||
t.Fatal("account should not exist yet")
|
||||
}
|
||||
|
||||
// Build a BAL mimicking same-tx create+destroy: the account appears
|
||||
// with a balance change to zero and nothing else.
|
||||
cb := bal.NewConstructionBlockAccessList()
|
||||
cb.BalanceChange(0, addr, uint256.NewInt(0))
|
||||
b := buildTestBAL(t, &cb)
|
||||
if err := syncer.applyAccessList(b); err != nil {
|
||||
t.Fatalf("applyAccessList failed: %v", err)
|
||||
}
|
||||
|
||||
// Check if applyAccessList created an account.
|
||||
data := rawdb.ReadAccountSnapshot(db, accountHash)
|
||||
if len(data) > 0 {
|
||||
// Account was created
|
||||
account, err := types.FullAccount(data)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to decode account: %v", err)
|
||||
}
|
||||
t.Errorf("account created for same-tx create+destroy: "+
|
||||
"balance=%v, nonce=%d, codeHash=%x, root=%v",
|
||||
account.Balance, account.Nonce, account.CodeHash, account.Root)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAccessListApplicationDestroyExisting verifies that when a BAL reduces
|
||||
// an existing flat-state account to nonce=0, balance=0, empty code (the
|
||||
// pre-funded destruction pattern), applyAccessList deletes the entry rather
|
||||
// than leaving it zereod.
|
||||
func TestAccessListApplicationDestroyExisting(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := rawdb.NewMemoryDatabase()
|
||||
syncer := NewSyncer(db, rawdb.HashScheme)
|
||||
addr := common.HexToAddress("0x05")
|
||||
accountHash := crypto.Keccak256Hash(addr[:])
|
||||
|
||||
// Pre-funded account: has balance, no nonce, no code.
|
||||
original := types.StateAccount{
|
||||
Nonce: 0,
|
||||
Balance: uint256.NewInt(1000),
|
||||
Root: types.EmptyRootHash,
|
||||
CodeHash: types.EmptyCodeHash[:],
|
||||
}
|
||||
rawdb.WriteAccountSnapshot(db, accountHash, types.SlimAccountRLP(original))
|
||||
|
||||
// The BAL zeros the balance. Nonce and code were already empty, so
|
||||
// the account ends up fully empty after applying.
|
||||
cb := bal.NewConstructionBlockAccessList()
|
||||
cb.BalanceChange(0, addr, uint256.NewInt(0))
|
||||
b := buildTestBAL(t, &cb)
|
||||
if err := syncer.applyAccessList(b); err != nil {
|
||||
t.Fatalf("applyAccessList failed: %v", err)
|
||||
}
|
||||
|
||||
if data := rawdb.ReadAccountSnapshot(db, accountHash); len(data) != 0 {
|
||||
account, _ := types.FullAccount(data)
|
||||
t.Errorf("destroyed account should have been deleted from flat state, "+
|
||||
"got balance=%v, nonce=%d, codeHash=%x",
|
||||
account.Balance, account.Nonce, account.CodeHash)
|
||||
}
|
||||
}
|
||||
|
|
@ -25,20 +25,6 @@ import (
|
|||
"github.com/ethereum/go-ethereum/trie"
|
||||
)
|
||||
|
||||
// genTrie interface is used by the snap syncer to generate merkle tree nodes
|
||||
// based on a received batch of states.
|
||||
type genTrie interface {
|
||||
// update inserts the state item into generator trie.
|
||||
update(key, value []byte) error
|
||||
|
||||
// delete removes the state item from the generator trie.
|
||||
delete(key []byte) error
|
||||
|
||||
// commit flushes the right boundary nodes if complete flag is true. This
|
||||
// function must be called before flushing the associated database batch.
|
||||
commit(complete bool) common.Hash
|
||||
}
|
||||
|
||||
// pathTrie is a wrapper over the stackTrie, incorporating numerous additional
|
||||
// logics to handle the semi-completed trie and potential leftover dangling
|
||||
// nodes in the database. It is utilized for constructing the merkle tree nodes
|
||||
|
|
@ -292,30 +278,3 @@ func (t *pathTrie) commit(complete bool) common.Hash {
|
|||
}
|
||||
|
||||
// hashTrie is a wrapper over the stackTrie for implementing genTrie interface.
|
||||
type hashTrie struct {
|
||||
tr *trie.StackTrie
|
||||
}
|
||||
|
||||
// newHashTrie initializes the hash trie.
|
||||
func newHashTrie(batch ethdb.Batch) *hashTrie {
|
||||
return &hashTrie{tr: trie.NewStackTrie(func(path []byte, hash common.Hash, blob []byte) {
|
||||
rawdb.WriteLegacyTrieNode(batch, hash, blob)
|
||||
})}
|
||||
}
|
||||
|
||||
// update implements genTrie interface, inserting a (key, value) pair into
|
||||
// the stack trie.
|
||||
func (t *hashTrie) update(key, value []byte) error {
|
||||
return t.tr.Update(key, value)
|
||||
}
|
||||
|
||||
// delete implements genTrie interface, ignoring the state item for deleting.
|
||||
func (t *hashTrie) delete(key []byte) error { return nil }
|
||||
|
||||
// commit implements genTrie interface, committing the nodes on right boundary.
|
||||
func (t *hashTrie) commit(complete bool) common.Hash {
|
||||
if !complete {
|
||||
return common.Hash{} // the hash is meaningless for incomplete commit
|
||||
}
|
||||
return t.tr.Hash() // return hash only if it's claimed as complete
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ var snap2 = map[uint64]msgHandler{
|
|||
GetByteCodesMsg: handleGetByteCodes,
|
||||
ByteCodesMsg: handleByteCodes,
|
||||
GetAccessListsMsg: handleGetAccessLists,
|
||||
// AccessListsMsg: TODO
|
||||
AccessListsMsg: handleAccessLists,
|
||||
}
|
||||
|
||||
// HandleMessage is invoked whenever an inbound message is received from a
|
||||
|
|
|
|||
|
|
@ -598,3 +598,16 @@ func ServiceGetAccessListsQuery(chain *core.BlockChain, req *GetAccessListsPacke
|
|||
}
|
||||
return response
|
||||
}
|
||||
|
||||
// nolint:unused
|
||||
func handleAccessLists(backend Backend, msg Decoder, peer *Peer) error {
|
||||
res := new(AccessListsPacket)
|
||||
if err := msg.Decode(res); err != nil {
|
||||
return fmt.Errorf("%w: message %v: %v", errDecode, msg, err)
|
||||
}
|
||||
tresp := tracker.Response{ID: res.ID, MsgCode: AccessListsMsg, Size: res.AccessLists.Len()}
|
||||
if err := peer.tracker.Fulfil(tresp); err != nil {
|
||||
return fmt.Errorf("BALs: %w", err)
|
||||
}
|
||||
return backend.Handle(peer, res)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,15 +58,8 @@ var (
|
|||
// to retrieved concurrently.
|
||||
largeStorageGauge = metrics.NewRegisteredGauge("eth/protocols/snap/sync/storage/large", nil)
|
||||
|
||||
// skipStorageHealingGauge is the metric to track how many storages are retrieved
|
||||
// in multiple requests but healing is not necessary.
|
||||
skipStorageHealingGauge = metrics.NewRegisteredGauge("eth/protocols/snap/sync/storage/noheal", nil)
|
||||
|
||||
// largeStorageDiscardGauge is the metric to track how many chunked storages are
|
||||
// discarded during the snap sync.
|
||||
largeStorageDiscardGauge = metrics.NewRegisteredGauge("eth/protocols/snap/sync/storage/chunk/discard", nil)
|
||||
largeStorageResumedGauge = metrics.NewRegisteredGauge("eth/protocols/snap/sync/storage/chunk/resume", nil)
|
||||
|
||||
stateSyncTimeGauge = metrics.NewRegisteredGauge("eth/protocols/snap/sync/time/statesync", nil)
|
||||
stateHealTimeGauge = metrics.NewRegisteredGauge("eth/protocols/snap/sync/time/stateheal", nil)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import (
|
|||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/p2p/tracker"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
)
|
||||
|
||||
// Peer is a collection of relevant information we have about a `snap` peer.
|
||||
|
|
@ -154,25 +153,21 @@ func (p *Peer) RequestByteCodes(id uint64, hashes []common.Hash, bytes int) erro
|
|||
})
|
||||
}
|
||||
|
||||
// RequestTrieNodes fetches a batch of account or storage trie nodes rooted in
|
||||
// a specific state trie. The `count` is the total count of paths being requested.
|
||||
func (p *Peer) RequestTrieNodes(id uint64, root common.Hash, count int, paths []TrieNodePathSet, bytes int) error {
|
||||
p.logger.Trace("Fetching set of trie nodes", "reqid", id, "root", root, "pathsets", len(paths), "bytes", common.StorageSize(bytes))
|
||||
|
||||
// RequestAccessLists fetches a batch of BALs by block hash.
|
||||
func (p *Peer) RequestAccessLists(id uint64, hashes []common.Hash, bytes int) error {
|
||||
p.logger.Trace("Fetching set of BALs", "reqid", id, "hashes", len(hashes), "bytes", common.StorageSize(bytes))
|
||||
err := p.tracker.Track(tracker.Request{
|
||||
ReqCode: GetTrieNodesMsg,
|
||||
RespCode: TrieNodesMsg,
|
||||
ReqCode: GetAccessListsMsg,
|
||||
RespCode: AccessListsMsg,
|
||||
ID: id,
|
||||
Size: count, // TrieNodes is limited by number of items.
|
||||
Size: len(hashes),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
encPaths, _ := rlp.EncodeToRawList(paths)
|
||||
return p2p.Send(p.rw, GetTrieNodesMsg, &GetTrieNodesPacket{
|
||||
ID: id,
|
||||
Root: root,
|
||||
Paths: encPaths,
|
||||
Bytes: uint64(bytes),
|
||||
return p2p.Send(p.rw, GetAccessListsMsg, &GetAccessListsPacket{
|
||||
ID: id,
|
||||
Hashes: hashes,
|
||||
Bytes: uint64(bytes),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,137 +18,123 @@ package snap
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
)
|
||||
|
||||
// Legacy sync progress definitions
|
||||
type legacyStorageTask struct {
|
||||
Next common.Hash // Next account to sync in this interval
|
||||
Last common.Hash // Last account to sync in this interval
|
||||
}
|
||||
// TestSyncProgressV1Discarded verifies that a persisted blob written in the
|
||||
// old unversioned format (raw JSON, no version prefix) is detected and
|
||||
// discarded on load, that the syncer falls through to a fresh start, and
|
||||
// that any orphan flat-state entries from the prior format are wiped.
|
||||
func TestSyncProgressV1Discarded(t *testing.T) {
|
||||
db := rawdb.NewMemoryDatabase()
|
||||
|
||||
type legacyAccountTask struct {
|
||||
Next common.Hash // Next account to sync in this interval
|
||||
Last common.Hash // Last account to sync in this interval
|
||||
SubTasks map[common.Hash][]*legacyStorageTask // Storage intervals needing fetching for large contracts
|
||||
}
|
||||
|
||||
type legacyProgress struct {
|
||||
Tasks []*legacyAccountTask // The suspended account tasks (contract tasks within)
|
||||
}
|
||||
|
||||
func compareProgress(a legacyProgress, b SyncProgress) bool {
|
||||
if len(a.Tasks) != len(b.Tasks) {
|
||||
return false
|
||||
// Write a raw JSON blob (no version byte) to simulate progress persisted
|
||||
// by a prior geth binary (snap/1 format).
|
||||
legacy := map[string]any{
|
||||
"Root": common.HexToHash("0xaaaa"),
|
||||
"BlockNumber": uint64(42),
|
||||
"Tasks": []any{},
|
||||
}
|
||||
for i := 0; i < len(a.Tasks); i++ {
|
||||
if a.Tasks[i].Next != b.Tasks[i].Next {
|
||||
return false
|
||||
}
|
||||
if a.Tasks[i].Last != b.Tasks[i].Last {
|
||||
return false
|
||||
}
|
||||
// new fields are not checked here
|
||||
|
||||
if len(a.Tasks[i].SubTasks) != len(b.Tasks[i].SubTasks) {
|
||||
return false
|
||||
}
|
||||
for addrHash, subTasksA := range a.Tasks[i].SubTasks {
|
||||
subTasksB, ok := b.Tasks[i].SubTasks[addrHash]
|
||||
if !ok || len(subTasksB) != len(subTasksA) {
|
||||
return false
|
||||
}
|
||||
for j := 0; j < len(subTasksA); j++ {
|
||||
if subTasksA[j].Next != subTasksB[j].Next {
|
||||
return false
|
||||
}
|
||||
if subTasksA[j].Last != subTasksB[j].Last {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func makeLegacyProgress() legacyProgress {
|
||||
return legacyProgress{
|
||||
Tasks: []*legacyAccountTask{
|
||||
{
|
||||
Next: common.Hash{},
|
||||
Last: common.Hash{0x77},
|
||||
SubTasks: map[common.Hash][]*legacyStorageTask{
|
||||
{0x1}: {
|
||||
{
|
||||
Next: common.Hash{},
|
||||
Last: common.Hash{0xff},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Next: common.Hash{0x88},
|
||||
Last: common.Hash{0xff},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func convertLegacy(legacy legacyProgress) SyncProgress {
|
||||
var progress SyncProgress
|
||||
for i, task := range legacy.Tasks {
|
||||
subTasks := make(map[common.Hash][]*storageTask)
|
||||
for owner, list := range task.SubTasks {
|
||||
var cpy []*storageTask
|
||||
for i := 0; i < len(list); i++ {
|
||||
cpy = append(cpy, &storageTask{
|
||||
Next: list[i].Next,
|
||||
Last: list[i].Last,
|
||||
})
|
||||
}
|
||||
subTasks[owner] = cpy
|
||||
}
|
||||
accountTask := &accountTask{
|
||||
Next: task.Next,
|
||||
Last: task.Last,
|
||||
SubTasks: subTasks,
|
||||
}
|
||||
if i == 0 {
|
||||
accountTask.StorageCompleted = []common.Hash{{0xaa}, {0xbb}} // fulfill new fields
|
||||
}
|
||||
progress.Tasks = append(progress.Tasks, accountTask)
|
||||
}
|
||||
return progress
|
||||
}
|
||||
|
||||
func TestSyncProgressCompatibility(t *testing.T) {
|
||||
// Decode serialized bytes of legacy progress, backward compatibility
|
||||
legacy := makeLegacyProgress()
|
||||
blob, err := json.Marshal(legacy)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal progress %v", err)
|
||||
}
|
||||
var dec SyncProgress
|
||||
if err := json.Unmarshal(blob, &dec); err != nil {
|
||||
t.Fatalf("Failed to unmarshal progress %v", err)
|
||||
}
|
||||
if !compareProgress(legacy, dec) {
|
||||
t.Fatal("sync progress is not backward compatible")
|
||||
t.Fatalf("marshal legacy: %v", err)
|
||||
}
|
||||
rawdb.WriteSnapshotSyncStatus(db, blob)
|
||||
|
||||
// Decode serialized bytes of new format progress
|
||||
progress := convertLegacy(legacy)
|
||||
blob, err = json.Marshal(progress)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal progress %v", err)
|
||||
// Pre-write orphan flat-state entries that should be wiped on fresh start.
|
||||
orphanAccountHash := common.HexToHash("0xdeadbeef")
|
||||
rawdb.WriteAccountSnapshot(db, orphanAccountHash, []byte{0xde, 0xad})
|
||||
orphanStorageAccount := common.HexToHash("0xfeedface")
|
||||
orphanStorageSlot := common.HexToHash("0xabcd")
|
||||
rawdb.WriteStorageSnapshot(db, orphanStorageAccount, orphanStorageSlot, []byte{0xff, 0xff})
|
||||
|
||||
syncer := NewSyncer(db, rawdb.HashScheme)
|
||||
syncer.loadSyncStatus()
|
||||
|
||||
if syncer.previousPivot != nil {
|
||||
t.Fatalf("expected previousPivot nil after discarding old format, got %+v", syncer.previousPivot)
|
||||
}
|
||||
var legacyDec legacyProgress
|
||||
if err := json.Unmarshal(blob, &legacyDec); err != nil {
|
||||
t.Fatalf("Failed to unmarshal progress %v", err)
|
||||
if len(syncer.tasks) != accountConcurrency {
|
||||
t.Fatalf("expected fresh task split of %d, got %d", accountConcurrency, len(syncer.tasks))
|
||||
}
|
||||
if !compareProgress(legacyDec, progress) {
|
||||
t.Fatal("sync progress is not forward compatible")
|
||||
if data := rawdb.ReadAccountSnapshot(db, orphanAccountHash); len(data) != 0 {
|
||||
t.Errorf("orphan account snapshot should be wiped, got %x", data)
|
||||
}
|
||||
if val := rawdb.ReadStorageSnapshot(db, orphanStorageAccount, orphanStorageSlot); len(val) != 0 {
|
||||
t.Errorf("orphan storage snapshot should be wiped, got %x", val)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSyncProgressV2RoundTrip verifies that the persisted blob is framed
|
||||
// with the expected version byte at offset 0, and that all six status
|
||||
// counters survive the round-trip.
|
||||
func TestSyncProgressV2RoundTrip(t *testing.T) {
|
||||
db := rawdb.NewMemoryDatabase()
|
||||
|
||||
saver := NewSyncer(db, rawdb.HashScheme)
|
||||
saver.pivot = &types.Header{Number: new(big.Int).SetUint64(123), Difficulty: common.Big0}
|
||||
saver.accountSynced = 1
|
||||
saver.accountBytes = 2
|
||||
saver.bytecodeSynced = 3
|
||||
saver.bytecodeBytes = 4
|
||||
saver.storageSynced = 5
|
||||
saver.storageBytes = 6
|
||||
saver.saveSyncStatus()
|
||||
|
||||
raw := rawdb.ReadSnapshotSyncStatus(db)
|
||||
if len(raw) == 0 || raw[0] != syncProgressVersion {
|
||||
t.Fatalf("expected version byte %d at offset 0, got blob %x", syncProgressVersion, raw)
|
||||
}
|
||||
|
||||
loader := NewSyncer(db, rawdb.HashScheme)
|
||||
loader.loadSyncStatus()
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
got uint64
|
||||
want uint64
|
||||
}{
|
||||
{"accountSynced", loader.accountSynced, 1},
|
||||
{"accountBytes", uint64(loader.accountBytes), 2},
|
||||
{"bytecodeSynced", loader.bytecodeSynced, 3},
|
||||
{"bytecodeBytes", uint64(loader.bytecodeBytes), 4},
|
||||
{"storageSynced", loader.storageSynced, 5},
|
||||
{"storageBytes", uint64(loader.storageBytes), 6},
|
||||
} {
|
||||
if c.got != c.want {
|
||||
t.Errorf("%s mismatch: got %d, want %d", c.name, c.got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSyncProgressCorruptPayload verifies that a persisted blob with the
|
||||
// correct version byte but unparseable JSON body is discarded, triggers a
|
||||
// fresh-start fall-through (not a panic or a stale-state load), and the
|
||||
// orphan flat state is wiped along with the corrupt status.
|
||||
func TestSyncProgressCorruptPayload(t *testing.T) {
|
||||
db := rawdb.NewMemoryDatabase()
|
||||
|
||||
// Version byte followed by garbage that isn't valid JSON.
|
||||
rawdb.WriteSnapshotSyncStatus(db, []byte{syncProgressVersion, 0x7b, 0x7b, 0x7b})
|
||||
|
||||
// Pre-write orphan flat-state entries that should be wiped on fresh start.
|
||||
orphanAccountHash := common.HexToHash("0xdeadbeef")
|
||||
rawdb.WriteAccountSnapshot(db, orphanAccountHash, []byte{0xde, 0xad})
|
||||
|
||||
syncer := NewSyncer(db, rawdb.HashScheme)
|
||||
syncer.loadSyncStatus()
|
||||
|
||||
if syncer.previousPivot != nil {
|
||||
t.Fatalf("expected previousPivot nil after corrupt payload, got %+v", syncer.previousPivot)
|
||||
}
|
||||
if len(syncer.tasks) != accountConcurrency {
|
||||
t.Fatalf("expected fresh task split of %d, got %d", accountConcurrency, len(syncer.tasks))
|
||||
}
|
||||
if data := rawdb.ReadAccountSnapshot(db, orphanAccountHash); len(data) != 0 {
|
||||
t.Errorf("orphan account snapshot should be wiped, got %x", data)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue