eth/downloader, eth/protocols/snap: BAL req wiring

eth/downloader, eth/protocols/snap: remove healing and genTrie, restructure sync loop for snap/2

eth/protocols/snap: Implement BAL fetching

eth/protocols/snap: create functions for bal verification and apply

eth/downloader,eth/protocols/snap: implement catch-up on pivot

eth/protocols/snap: add tests and fix peer registration for access lists

eth/protocols/snap: add pivot movement integration tests

core, core/state/snapshot: skip snapshot generation after sync completion

eth/protocols/snap: skip new empty accounts in applyAccessList and test
This commit is contained in:
jonny rhea 2026-03-31 11:34:14 -05:00
parent e1e3eaa381
commit 6b830ce8fb
13 changed files with 1932 additions and 1471 deletions

View file

@ -1179,10 +1179,10 @@ func (bc *BlockChain) SnapSyncComplete(hash common.Hash) error {
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.
// Set up the snapshot tree from the synced flat state. Snap/2 downloads
// flat state directly as the snapshot.
if bc.snaps != nil {
bc.snaps.Rebuild(root)
bc.snaps.RebuildFromSyncedState(root)
}
// If all checks out, manually set the head block.

View file

@ -23,6 +23,7 @@ import (
"fmt"
"sync"
"github.com/VictoriaMetrics/fastcache"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
@ -726,6 +727,27 @@ func (t *Tree) Rebuild(root common.Hash) {
}
}
// RebuildFromSyncedState sets up the snapshot tree to use flat state that was
// already downloaded by snap sync. Unlike Rebuild, it does NOT regenerate the
// snapshot from the trie.
func (t *Tree) RebuildFromSyncedState(root common.Hash) {
t.lock.Lock()
defer t.lock.Unlock()
rawdb.DeleteSnapshotRecoveryNumber(t.diskdb)
rawdb.DeleteSnapshotDisabled(t.diskdb)
rawdb.WriteSnapshotRoot(t.diskdb, root)
journalProgress(t.diskdb, nil, nil)
log.Info("Setting up snapshot from synced state", "root", root)
t.layers = map[common.Hash]snapshot{
root: &diskLayer{
diskdb: t.diskdb,
triedb: t.triedb,
cache: fastcache.New(t.config.CacheSize * 1024 * 1024),
root: root,
},
}
}
// AccountIterator creates a new account iterator for the specified root hash and
// seeks to a starting account hash.
func (t *Tree) AccountIterator(root common.Hash, seek common.Hash) (AccountIterator, error) {

View file

@ -276,7 +276,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,
@ -288,12 +288,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,
}
}
@ -873,13 +867,48 @@ func (d *Downloader) importBlockResults(results []*fetchResult) error {
return nil
}
// checkDeepReorg checks if the old pivot block was reorged by comparing its
// state root against the current canonical chain. If the canonical header at
// the old pivot's block number has a different state root, the syncer's flat
// state is from the old fork and must be wiped. Returns true if a deep reorg
// was detected.
//
// Returns false (no reorg) when the canonical hash or header is missing. This
// avoids false positives from pruned or not-yet-downloaded data. If the chain
// really did shorten past the old pivot, sync.catchUp's from > to guard will
// catch this.
func checkDeepReorg(db ethdb.Database, oldNumber uint64, oldRoot common.Hash) bool {
oldHash := rawdb.ReadCanonicalHash(db, oldNumber)
if oldHash == (common.Hash{}) {
return false
}
oldHeader := rawdb.ReadHeader(db, oldHash, oldNumber)
if oldHeader == nil {
return false
}
return oldHeader.Root != oldRoot
}
// restartSnapSync cancels the current state sync and starts a new one with the
// given root. Before restarting, it checks for deep reorgs and wipes sync
// progress if the old pivot was reorged.
func (d *Downloader) restartSnapSync(oldSync *stateSync, newRoot common.Hash, newNumber uint64) *stateSync {
if checkDeepReorg(d.stateDB, oldSync.number, oldSync.root) {
log.Warn("Deep reorg detected, restarting snap sync from scratch",
"number", oldSync.number, "oldRoot", oldSync.root)
rawdb.WriteSnapshotSyncStatus(d.stateDB, nil)
}
oldSync.Cancel()
return d.syncState(newRoot, newNumber)
}
// processSnapSyncContent takes fetch results from the queue and writes them to the
// database. It also controls the synchronisation of state nodes of the pivot block.
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.Root, d.pivotHeader.Number.Uint64())
d.pivotLock.RUnlock()
defer func() {
@ -950,9 +979,7 @@ 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
sync.Cancel()
sync = d.syncState(pivot.Root)
sync = d.restartSnapSync(sync, pivot.Root, pivot.Number.Uint64())
go closeOnErr(sync)
}
}
@ -966,9 +993,7 @@ func (d *Downloader) processSnapSyncContent() error {
if P != nil {
// If new pivot block found, cancel old state retrieval and restart
if oldPivot != P {
sync.Cancel()
sync = d.syncState(P.Header.Root)
sync = d.restartSnapSync(sync, P.Header.Root, P.Header.Number.Uint64())
go closeOnErr(sync)
oldPivot = P
}
@ -1086,7 +1111,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)

View file

@ -373,20 +373,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
}

View file

@ -23,10 +23,10 @@ import (
"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 root hash and block number.
func (d *Downloader) syncState(root common.Hash, number uint64) *stateSync {
// Create the state sync
s := newStateSync(d, root)
s := newStateSync(d, root, number)
select {
case d.stateSyncStart <- s:
// If we tell the statesync to restart with a new root, we also need
@ -77,8 +77,9 @@ func (d *Downloader) runStateSync(s *stateSync) *stateSync {
// stateSync schedules requests for downloading a particular state trie defined
// by a given state root.
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
root common.Hash // State root currently being synced
number uint64 // Block number of the pivot
started chan struct{} // Started is signalled once the sync loop starts
cancel chan struct{} // Channel to signal a termination request
@ -89,10 +90,11 @@ 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, root common.Hash, number uint64) *stateSync {
return &stateSync{
d: d,
root: root,
number: number,
cancel: make(chan struct{}),
done: make(chan struct{}),
started: make(chan struct{}),
@ -104,7 +106,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.root, s.number, s.cancel)
close(s.done)
}

View file

@ -0,0 +1,120 @@
// 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
}
// 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()
for _, access := range b.Accesses {
addr := common.Address(access.Address)
accountHash := crypto.Keccak256Hash(addr[:])
// 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 {
raw := access.BalanceChanges[n-1].Balance
account.Balance = new(uint256.Int).SetBytes(raw[:])
}
// 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
storageHash := crypto.Keccak256Hash(slotWrites.Slot[:])
rawdb.WriteStorageSnapshot(batch, accountHash, storageHash, value[:])
}
}
// Don't create empty accounts in flat state (EIP-161).
// 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.
if isNew && account.Balance.IsZero() && account.Nonce == 0 &&
bytes.Equal(account.CodeHash, types.EmptyCodeHash[:]) {
continue
}
// Write the updated account (storageRoot intentionally left stale)
rawdb.WriteAccountSnapshot(batch, accountHash, types.SlimAccountRLP(account))
}
return batch.Write()
}

View file

@ -0,0 +1,299 @@
// 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)
}
}
// 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())
}
}
// 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)
}
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
)

View file

@ -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),
})
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff