mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-05-24 08:49:29 +00:00
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:
parent
e1e3eaa381
commit
6b830ce8fb
13 changed files with 1932 additions and 1471 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
120
eth/protocols/snap/bal_apply.go
Normal file
120
eth/protocols/snap/bal_apply.go
Normal 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()
|
||||
}
|
||||
299
eth/protocols/snap/bal_apply_test.go
Normal file
299
eth/protocols/snap/bal_apply_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
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