mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-21 14:14:30 +00:00
This PR fixes an issue where flat states are continuously persisted during downloadState, while the sync journal is only persisted at the end of Sync. As a result, an unclean shutdown can leave the on-disk flat state ahead of the journal markers. Some persisted entries may be stale (storage slots that should have been deleted), and these dangling entries are not detected or fixed by subsequent state downloads. To address this, this PR introduces a cleanup step before state downloading begins. It removes all state entries that are not covered by the persisted journal markers.
3101 lines
108 KiB
Go
3101 lines
108 KiB
Go
// 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"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"slices"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"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/ethdb"
|
|
"github.com/ethereum/go-ethereum/log"
|
|
"github.com/ethereum/go-ethereum/rlp"
|
|
"github.com/ethereum/go-ethereum/trie"
|
|
"github.com/ethereum/go-ethereum/trie/trienode"
|
|
"github.com/ethereum/go-ethereum/triedb"
|
|
"github.com/holiman/uint256"
|
|
)
|
|
|
|
type (
|
|
accountHandlerFuncV2 func(t *testPeerV2, requestId uint64, root common.Hash, origin common.Hash, limit common.Hash, cap int) error
|
|
storageHandlerFuncV2 func(t *testPeerV2, requestId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max int) error
|
|
codeHandlerFuncV2 func(t *testPeerV2, id uint64, hashes []common.Hash, max int) error
|
|
accessListHandlerFunc func(t *testPeerV2, id uint64, hashes []common.Hash, max int) error
|
|
)
|
|
|
|
type testPeerV2 struct {
|
|
id string
|
|
test *testing.T
|
|
remote *syncerV2
|
|
logger log.Logger
|
|
accountTrie *trie.Trie
|
|
accountValues []*kv
|
|
storageTries map[common.Hash]*trie.Trie
|
|
storageValues map[common.Hash][]*kv
|
|
accessLists map[common.Hash]rlp.RawValue // block hash -> RLP-encoded BAL
|
|
|
|
accountRequestV2Handler accountHandlerFuncV2
|
|
storageRequestV2Handler storageHandlerFuncV2
|
|
codeRequestHandler codeHandlerFuncV2
|
|
accessListRequestHandler accessListHandlerFunc
|
|
term func()
|
|
|
|
// counters
|
|
nAccountRequests atomic.Int64
|
|
nStorageRequests atomic.Int64
|
|
nBytecodeRequests atomic.Int64
|
|
nAccessListRequests atomic.Int64
|
|
}
|
|
|
|
func newTestPeerV2(id string, t *testing.T, term func()) *testPeerV2 {
|
|
peer := &testPeerV2{
|
|
id: id,
|
|
test: t,
|
|
logger: log.New("id", id),
|
|
accountRequestV2Handler: defaultAccountRequestHandlerV2,
|
|
storageRequestV2Handler: defaultStorageRequestHandlerV2,
|
|
codeRequestHandler: defaultCodeRequestHandlerV2,
|
|
accessListRequestHandler: defaultAccessListRequestHandler,
|
|
term: term,
|
|
}
|
|
return peer
|
|
}
|
|
|
|
func (t *testPeerV2) setStorageTries(tries map[common.Hash]*trie.Trie) {
|
|
t.storageTries = make(map[common.Hash]*trie.Trie)
|
|
for root, trie := range tries {
|
|
t.storageTries[root] = trie.Copy()
|
|
}
|
|
}
|
|
|
|
func (t *testPeerV2) ID() string { return t.id }
|
|
func (t *testPeerV2) Log() log.Logger { return t.logger }
|
|
|
|
func (t *testPeerV2) Stats() string {
|
|
return fmt.Sprintf(`Account requests: %d Storage requests: %d Bytecode requests: %d`, t.nAccountRequests.Load(), t.nStorageRequests.Load(), t.nBytecodeRequests.Load())
|
|
}
|
|
|
|
func (t *testPeerV2) RequestAccountRange(id uint64, root, origin, limit common.Hash, bytes int) error {
|
|
t.logger.Trace("Fetching range of accounts", "reqid", id, "root", root, "origin", origin, "limit", limit, "bytes", common.StorageSize(bytes))
|
|
t.nAccountRequests.Add(1)
|
|
go t.accountRequestV2Handler(t, id, root, origin, limit, bytes)
|
|
return nil
|
|
}
|
|
|
|
func (t *testPeerV2) RequestStorageRanges(id uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, bytes int) error {
|
|
t.nStorageRequests.Add(1)
|
|
if len(accounts) == 1 && origin != nil {
|
|
t.logger.Trace("Fetching range of large storage slots", "reqid", id, "root", root, "account", accounts[0], "origin", common.BytesToHash(origin), "limit", common.BytesToHash(limit), "bytes", common.StorageSize(bytes))
|
|
} else {
|
|
t.logger.Trace("Fetching ranges of small storage slots", "reqid", id, "root", root, "accounts", len(accounts), "first", accounts[0], "bytes", common.StorageSize(bytes))
|
|
}
|
|
go t.storageRequestV2Handler(t, id, root, accounts, origin, limit, bytes)
|
|
return nil
|
|
}
|
|
|
|
func (t *testPeerV2) RequestByteCodes(id uint64, hashes []common.Hash, bytes int) error {
|
|
t.nBytecodeRequests.Add(1)
|
|
t.logger.Trace("Fetching set of byte codes", "reqid", id, "hashes", len(hashes), "bytes", common.StorageSize(bytes))
|
|
go t.codeRequestHandler(t, id, hashes, bytes)
|
|
return nil
|
|
}
|
|
|
|
func (t *testPeerV2) RequestTrieNodes(id uint64, root common.Hash, count int, paths []TrieNodePathSet, bytes int) error {
|
|
// snap/2 never requests trie nodes.
|
|
return nil
|
|
}
|
|
|
|
func (t *testPeerV2) RequestAccessLists(id uint64, hashes []common.Hash, bytes int) error {
|
|
t.nAccessListRequests.Add(1)
|
|
t.logger.Trace("Fetching set of BALs", "reqid", id, "hashes", len(hashes), "bytes", common.StorageSize(bytes))
|
|
go t.accessListRequestHandler(t, id, hashes, bytes)
|
|
return nil
|
|
}
|
|
|
|
func createAccountRequestResponseV2(t *testPeerV2, root common.Hash, origin common.Hash, limit common.Hash, cap int) (keys []common.Hash, vals [][]byte, proofs [][]byte) {
|
|
var size int
|
|
if limit == (common.Hash{}) {
|
|
limit = common.MaxHash
|
|
}
|
|
for _, entry := range t.accountValues {
|
|
if size > cap {
|
|
break
|
|
}
|
|
if bytes.Compare(origin[:], entry.k) <= 0 {
|
|
keys = append(keys, common.BytesToHash(entry.k))
|
|
vals = append(vals, entry.v)
|
|
size += 32 + len(entry.v)
|
|
}
|
|
if bytes.Compare(entry.k, limit[:]) >= 0 {
|
|
break
|
|
}
|
|
}
|
|
proof := trienode.NewProofSet()
|
|
if err := t.accountTrie.Prove(origin[:], proof); err != nil {
|
|
t.logger.Error("Could not prove inexistence of origin", "origin", origin, "error", err)
|
|
}
|
|
if len(keys) > 0 {
|
|
lastK := (keys[len(keys)-1])[:]
|
|
if err := t.accountTrie.Prove(lastK, proof); err != nil {
|
|
t.logger.Error("Could not prove last item", "error", err)
|
|
}
|
|
}
|
|
return keys, vals, proof.List()
|
|
}
|
|
|
|
func createStorageRequestResponseV2(t *testPeerV2, root common.Hash, accounts []common.Hash, origin, limit []byte, max int) (hashes [][]common.Hash, slots [][][]byte, proofs [][]byte) {
|
|
var size int
|
|
for _, account := range accounts {
|
|
var originHash common.Hash
|
|
if len(origin) > 0 {
|
|
originHash = common.BytesToHash(origin)
|
|
}
|
|
var limitHash = common.MaxHash
|
|
if len(limit) > 0 {
|
|
limitHash = common.BytesToHash(limit)
|
|
}
|
|
var (
|
|
keys []common.Hash
|
|
vals [][]byte
|
|
abort bool
|
|
)
|
|
for _, entry := range t.storageValues[account] {
|
|
if size >= max {
|
|
abort = true
|
|
break
|
|
}
|
|
if bytes.Compare(entry.k, originHash[:]) < 0 {
|
|
continue
|
|
}
|
|
keys = append(keys, common.BytesToHash(entry.k))
|
|
vals = append(vals, entry.v)
|
|
size += 32 + len(entry.v)
|
|
if bytes.Compare(entry.k, limitHash[:]) >= 0 {
|
|
break
|
|
}
|
|
}
|
|
if len(keys) > 0 {
|
|
hashes = append(hashes, keys)
|
|
slots = append(slots, vals)
|
|
}
|
|
if originHash != (common.Hash{}) || (abort && len(keys) > 0) {
|
|
proof := trienode.NewProofSet()
|
|
stTrie := t.storageTries[account]
|
|
|
|
if err := stTrie.Prove(originHash[:], proof); err != nil {
|
|
t.logger.Error("Could not prove inexistence of origin", "origin", originHash, "error", err)
|
|
}
|
|
if len(keys) > 0 {
|
|
lastK := (keys[len(keys)-1])[:]
|
|
if err := stTrie.Prove(lastK, proof); err != nil {
|
|
t.logger.Error("Could not prove last item", "error", err)
|
|
}
|
|
}
|
|
proofs = append(proofs, proof.List()...)
|
|
break
|
|
}
|
|
}
|
|
return hashes, slots, proofs
|
|
}
|
|
|
|
func createStorageRequestResponseAlwaysProveV2(t *testPeerV2, root common.Hash, accounts []common.Hash, bOrigin, bLimit []byte, max int) (hashes [][]common.Hash, slots [][][]byte, proofs [][]byte) {
|
|
var size int
|
|
max = max * 3 / 4
|
|
|
|
var origin common.Hash
|
|
if len(bOrigin) > 0 {
|
|
origin = common.BytesToHash(bOrigin)
|
|
}
|
|
var exit bool
|
|
for i, account := range accounts {
|
|
var keys []common.Hash
|
|
var vals [][]byte
|
|
for _, entry := range t.storageValues[account] {
|
|
if bytes.Compare(entry.k, origin[:]) < 0 {
|
|
exit = true
|
|
}
|
|
keys = append(keys, common.BytesToHash(entry.k))
|
|
vals = append(vals, entry.v)
|
|
size += 32 + len(entry.v)
|
|
if size > max {
|
|
exit = true
|
|
}
|
|
}
|
|
if i == len(accounts)-1 {
|
|
exit = true
|
|
}
|
|
hashes = append(hashes, keys)
|
|
slots = append(slots, vals)
|
|
|
|
if exit {
|
|
proof := trienode.NewProofSet()
|
|
stTrie := t.storageTries[account]
|
|
|
|
if err := stTrie.Prove(origin[:], proof); err != nil {
|
|
t.logger.Error("Could not prove inexistence of origin", "origin", origin, "error", err)
|
|
}
|
|
if len(keys) > 0 {
|
|
lastK := (keys[len(keys)-1])[:]
|
|
if err := stTrie.Prove(lastK, proof); err != nil {
|
|
t.logger.Error("Could not prove last item", "error", err)
|
|
}
|
|
}
|
|
proofs = append(proofs, proof.List()...)
|
|
break
|
|
}
|
|
}
|
|
return hashes, slots, proofs
|
|
}
|
|
|
|
// defaultAccountRequestHandlerV2 is a well-behaving handler for AccountRangeRequests.
|
|
func defaultAccountRequestHandlerV2(t *testPeerV2, id uint64, root common.Hash, origin common.Hash, limit common.Hash, cap int) error {
|
|
keys, vals, proofs := createAccountRequestResponseV2(t, root, origin, limit, cap)
|
|
if err := t.remote.OnAccounts(t, id, keys, vals, proofs); err != nil {
|
|
t.test.Errorf("Remote side rejected our delivery: %v", err)
|
|
t.term()
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func defaultStorageRequestHandlerV2(t *testPeerV2, requestId uint64, root common.Hash, accounts []common.Hash, bOrigin, bLimit []byte, max int) error {
|
|
hashes, slots, proofs := createStorageRequestResponseV2(t, root, accounts, bOrigin, bLimit, max)
|
|
if err := t.remote.OnStorage(t, requestId, hashes, slots, proofs); err != nil {
|
|
t.test.Errorf("Remote side rejected our delivery: %v", err)
|
|
t.term()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func defaultCodeRequestHandlerV2(t *testPeerV2, id uint64, hashes []common.Hash, max int) error {
|
|
var bytecodes [][]byte
|
|
for _, h := range hashes {
|
|
bytecodes = append(bytecodes, getCodeByHash(h))
|
|
}
|
|
if err := t.remote.OnByteCodes(t, id, bytecodes); err != nil {
|
|
t.test.Errorf("Remote side rejected our delivery: %v", err)
|
|
t.term()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// defaultAccessListRequestHandler serves BALs from the peer's accessLists map.
|
|
// If the peer has no BAL data, it returns empty (peer rejection).
|
|
func defaultAccessListRequestHandler(t *testPeerV2, id uint64, hashes []common.Hash, max int) error {
|
|
var results []rlp.RawValue
|
|
if t.accessLists != nil {
|
|
for _, h := range hashes {
|
|
if raw, ok := t.accessLists[h]; ok {
|
|
results = append(results, raw)
|
|
}
|
|
}
|
|
}
|
|
rawList, _ := rlp.EncodeToRawList(results)
|
|
if err := t.remote.OnAccessLists(t, id, rawList); err != nil {
|
|
t.test.Errorf("Remote side rejected our delivery: %v", err)
|
|
t.term()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// emptyRequestAccountRangeFnV2 is a rejects AccountRangeRequests
|
|
func emptyRequestAccountRangeFnV2(t *testPeerV2, requestId uint64, root common.Hash, origin common.Hash, limit common.Hash, cap int) error {
|
|
t.remote.OnAccounts(t, requestId, nil, nil, nil)
|
|
return nil
|
|
}
|
|
|
|
func nonResponsiveRequestAccountRangeFnV2(t *testPeerV2, requestId uint64, root common.Hash, origin common.Hash, limit common.Hash, cap int) error {
|
|
return nil
|
|
}
|
|
|
|
func emptyStorageRequestHandlerV2(t *testPeerV2, requestId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max int) error {
|
|
t.remote.OnStorage(t, requestId, nil, nil, nil)
|
|
return nil
|
|
}
|
|
|
|
func nonResponsiveStorageRequestHandlerV2(t *testPeerV2, requestId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max int) error {
|
|
return nil
|
|
}
|
|
|
|
func proofHappyStorageRequestHandlerV2(t *testPeerV2, requestId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max int) error {
|
|
hashes, slots, proofs := createStorageRequestResponseAlwaysProveV2(t, root, accounts, origin, limit, max)
|
|
if err := t.remote.OnStorage(t, requestId, hashes, slots, proofs); err != nil {
|
|
t.test.Errorf("Remote side rejected our delivery: %v", err)
|
|
t.term()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func corruptCodeRequestHandlerV2(t *testPeerV2, id uint64, hashes []common.Hash, max int) error {
|
|
var bytecodes [][]byte
|
|
for _, h := range hashes {
|
|
bytecodes = append(bytecodes, h[:])
|
|
}
|
|
if err := t.remote.OnByteCodes(t, id, bytecodes); err != nil {
|
|
t.logger.Info("remote error on delivery (as expected)", "error", err)
|
|
t.remote.Unregister(t.id)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func cappedCodeRequestHandlerV2(t *testPeerV2, id uint64, hashes []common.Hash, max int) error {
|
|
var bytecodes [][]byte
|
|
for _, h := range hashes[:1] {
|
|
bytecodes = append(bytecodes, getCodeByHash(h))
|
|
}
|
|
if err := t.remote.OnByteCodes(t, id, bytecodes); err != nil {
|
|
t.test.Errorf("Remote side rejected our delivery: %v", err)
|
|
t.term()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func starvingStorageRequestHandlerV2(t *testPeerV2, requestId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max int) error {
|
|
return defaultStorageRequestHandlerV2(t, requestId, root, accounts, origin, limit, 500)
|
|
}
|
|
|
|
func starvingAccountRequestHandlerV2(t *testPeerV2, requestId uint64, root common.Hash, origin common.Hash, limit common.Hash, cap int) error {
|
|
return defaultAccountRequestHandlerV2(t, requestId, root, origin, limit, 500)
|
|
}
|
|
|
|
func corruptAccountRequestHandlerV2(t *testPeerV2, requestId uint64, root common.Hash, origin common.Hash, limit common.Hash, cap int) error {
|
|
hashes, accounts, proofs := createAccountRequestResponseV2(t, root, origin, limit, cap)
|
|
if len(proofs) > 0 {
|
|
proofs = proofs[1:]
|
|
}
|
|
if err := t.remote.OnAccounts(t, requestId, hashes, accounts, proofs); err != nil {
|
|
t.logger.Info("remote error on delivery (as expected)", "error", err)
|
|
t.remote.Unregister(t.id)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func corruptStorageRequestHandlerV2(t *testPeerV2, requestId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max int) error {
|
|
hashes, slots, proofs := createStorageRequestResponseV2(t, root, accounts, origin, limit, max)
|
|
if len(proofs) > 0 {
|
|
proofs = proofs[1:]
|
|
}
|
|
if err := t.remote.OnStorage(t, requestId, hashes, slots, proofs); err != nil {
|
|
t.logger.Info("remote error on delivery (as expected)", "error", err)
|
|
t.remote.Unregister(t.id)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func noProofStorageRequestHandlerV2(t *testPeerV2, requestId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max int) error {
|
|
hashes, slots, _ := createStorageRequestResponseV2(t, root, accounts, origin, limit, max)
|
|
if err := t.remote.OnStorage(t, requestId, hashes, slots, nil); err != nil {
|
|
t.logger.Info("remote error on delivery (as expected)", "error", err)
|
|
t.remote.Unregister(t.id)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TestSyncBloatedProofV2 tests a scenario where we provide only _one_ value, but
|
|
// also ship the entire trie inside the proof. If the attack is successful,
|
|
// the remote side does not do any follow-up requests
|
|
func TestSyncBloatedProofV2(t *testing.T) {
|
|
t.Parallel()
|
|
testSyncBloatedProofV2(t, rawdb.HashScheme)
|
|
testSyncBloatedProofV2(t, rawdb.PathScheme)
|
|
}
|
|
|
|
func testSyncBloatedProofV2(t *testing.T, scheme string) {
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, scheme)
|
|
source := newTestPeerV2("source", t, term)
|
|
source.accountTrie = sourceAccountTrie.Copy()
|
|
source.accountValues = elems
|
|
|
|
source.accountRequestV2Handler = func(t *testPeerV2, requestId uint64, root common.Hash, origin common.Hash, limit common.Hash, cap int) error {
|
|
var (
|
|
keys []common.Hash
|
|
vals [][]byte
|
|
)
|
|
// The values
|
|
for _, entry := range t.accountValues {
|
|
if bytes.Compare(entry.k, origin[:]) < 0 {
|
|
continue
|
|
}
|
|
if bytes.Compare(entry.k, limit[:]) > 0 {
|
|
continue
|
|
}
|
|
keys = append(keys, common.BytesToHash(entry.k))
|
|
vals = append(vals, entry.v)
|
|
}
|
|
// The proofs
|
|
proof := trienode.NewProofSet()
|
|
if err := t.accountTrie.Prove(origin[:], proof); err != nil {
|
|
t.logger.Error("Could not prove origin", "origin", origin, "error", err)
|
|
}
|
|
// The bloat: add proof of every single element
|
|
for _, entry := range t.accountValues {
|
|
if err := t.accountTrie.Prove(entry.k, proof); err != nil {
|
|
t.logger.Error("Could not prove item", "error", err)
|
|
}
|
|
}
|
|
// And remove one item from the elements
|
|
if len(keys) > 2 {
|
|
keys = append(keys[:1], keys[2:]...)
|
|
vals = append(vals[:1], vals[2:]...)
|
|
}
|
|
if err := t.remote.OnAccounts(t, requestId, keys, vals, proof.List()); err != nil {
|
|
t.logger.Info("remote error on delivery (as expected)", "error", err)
|
|
t.term()
|
|
// This is actually correct, signal to exit the test successfully
|
|
}
|
|
return nil
|
|
}
|
|
syncer := setupSyncerV2(nodeScheme, source)
|
|
if err := syncer.Sync(mkPivot(0, sourceAccountTrie.Hash()), cancel); err == nil {
|
|
t.Fatal("No error returned from incomplete/cancelled sync")
|
|
}
|
|
}
|
|
|
|
func setupSyncerV2(scheme string, peers ...*testPeerV2) *syncerV2 {
|
|
stateDb := rawdb.NewMemoryDatabase()
|
|
syncer := newSyncerV2(stateDb, scheme)
|
|
for _, peer := range peers {
|
|
syncer.Register(peer)
|
|
peer.remote = syncer
|
|
}
|
|
return syncer
|
|
}
|
|
|
|
// mkPivot builds a minimal pivot header with the given block number and state
|
|
// root, suitable for test calls into syncerV2.Sync.
|
|
func mkPivot(num uint64, root common.Hash) *types.Header {
|
|
return &types.Header{
|
|
Number: new(big.Int).SetUint64(num),
|
|
Root: root,
|
|
Difficulty: common.Big0,
|
|
}
|
|
}
|
|
|
|
// makeAccessListHeaders builds a header map keyed by block hash where each
|
|
// header's BlockAccessListHash matches the BAL it points to. fetchAccessLists
|
|
// uses these headers to verify peer responses, so tests need to provide them
|
|
// alongside any BALs they expect to be accepted.
|
|
func makeAccessListHeaders(bals map[common.Hash]rlp.RawValue) map[common.Hash]*types.Header {
|
|
headers := make(map[common.Hash]*types.Header, len(bals))
|
|
for h, raw := range bals {
|
|
var b bal.BlockAccessList
|
|
if err := rlp.DecodeBytes(raw, &b); err != nil {
|
|
continue
|
|
}
|
|
bh := b.Hash()
|
|
headers[h] = &types.Header{BlockAccessListHash: &bh}
|
|
}
|
|
return headers
|
|
}
|
|
|
|
// TestSyncV2 tests a basic sync with one peer
|
|
func TestSyncV2(t *testing.T) {
|
|
t.Parallel()
|
|
testSyncV2(t, rawdb.HashScheme)
|
|
testSyncV2(t, rawdb.PathScheme)
|
|
}
|
|
|
|
func testSyncV2(t *testing.T, scheme string) {
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, scheme)
|
|
|
|
mkSource := func(name string) *testPeerV2 {
|
|
source := newTestPeerV2(name, t, term)
|
|
source.accountTrie = sourceAccountTrie.Copy()
|
|
source.accountValues = elems
|
|
return source
|
|
}
|
|
syncer := setupSyncerV2(nodeScheme, mkSource("source"))
|
|
if err := syncer.Sync(mkPivot(0, sourceAccountTrie.Hash()), cancel); err != nil {
|
|
t.Fatalf("sync failed: %v", err)
|
|
}
|
|
verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
|
|
verifyAdoptedSyncedState(scheme, syncer.db, sourceAccountTrie.Hash(), elems, t)
|
|
}
|
|
|
|
// TestSyncV2FrozenPivot checks the pivot freeze signal around the sync
|
|
// lifecycle. The pivot is unfrozen while flat state is downloading, frozen
|
|
// once the download completes, stays frozen after the sync returns so the
|
|
// downloader resumes against it until the pivot block is committed, and
|
|
// unfreezes again after a state reset.
|
|
func TestSyncV2FrozenPivot(t *testing.T) {
|
|
t.Parallel()
|
|
testSyncV2FrozenPivot(t, rawdb.HashScheme)
|
|
testSyncV2FrozenPivot(t, rawdb.PathScheme)
|
|
}
|
|
|
|
func testSyncV2FrozenPivot(t *testing.T, scheme string) {
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, scheme)
|
|
|
|
source := newTestPeerV2("source", t, term)
|
|
source.accountTrie = sourceAccountTrie.Copy()
|
|
source.accountValues = elems
|
|
|
|
syncer := setupSyncerV2(nodeScheme, source)
|
|
pivot := mkPivot(0, sourceAccountTrie.Hash())
|
|
|
|
// The handler runs while account ranges are still being served, so it
|
|
// can observe the signal mid download.
|
|
source.accountRequestV2Handler = func(p *testPeerV2, requestId uint64, root common.Hash, origin common.Hash, limit common.Hash, cap int) error {
|
|
if syncer.FrozenPivot() != nil {
|
|
t.Error("pivot frozen during flat state download")
|
|
}
|
|
return defaultAccountRequestHandlerV2(p, requestId, root, origin, limit, cap)
|
|
}
|
|
if syncer.FrozenPivot() != nil {
|
|
t.Fatal("pivot frozen before sync started")
|
|
}
|
|
if err := syncer.Sync(pivot, cancel); err != nil {
|
|
t.Fatalf("sync failed: %v", err)
|
|
}
|
|
if frozen := syncer.FrozenPivot(); frozen == nil || frozen.Hash() != pivot.Hash() {
|
|
t.Fatal("pivot not frozen at the synced header after download completed")
|
|
}
|
|
// A restart must not lose the freeze: a fresh syncer instance on the same
|
|
// database derives it from the persisted journal, before any Sync call.
|
|
restarted := newSyncerV2(syncer.db, nodeScheme)
|
|
if frozen := restarted.FrozenPivot(); frozen == nil || frozen.Hash() != pivot.Hash() {
|
|
t.Fatal("pivot freeze lost after restart")
|
|
}
|
|
syncer.resetSyncState()
|
|
if syncer.FrozenPivot() != nil {
|
|
t.Fatal("pivot still frozen after state reset")
|
|
}
|
|
// The reset deletes the journal, so a restarted instance is unfrozen too.
|
|
if restarted := newSyncerV2(syncer.db, nodeScheme); restarted.FrozenPivot() != nil {
|
|
t.Fatal("pivot still frozen after restart following a state reset")
|
|
}
|
|
}
|
|
|
|
// verifyAdoptedSyncedState exercises the snap/2 completion contract end-to-end:
|
|
// after a real sync, opening a fresh triedb and calling AdoptSyncedState must
|
|
// (a) succeed and (b) leave flat-state reads serving immediately, with no
|
|
// background regeneration gating them.
|
|
func verifyAdoptedSyncedState(scheme string, db ethdb.KeyValueStore, root common.Hash, elems []*kv, t *testing.T) {
|
|
t.Helper()
|
|
if scheme != rawdb.PathScheme {
|
|
return
|
|
}
|
|
tdb := triedb.NewDatabase(rawdb.NewDatabase(db), newDbConfig(scheme))
|
|
defer tdb.Close()
|
|
|
|
if err := tdb.AdoptSyncedState(root); err != nil {
|
|
t.Fatalf("AdoptSyncedState failed: %v", err)
|
|
}
|
|
// Read one of the synced accounts via the public flat-state API. If this
|
|
// returned errNotCoveredYet we'd know AdoptSyncedState left a generator
|
|
// gating reads, exactly the bug we're trying to prevent.
|
|
sr, err := tdb.StateReader(root)
|
|
if err != nil {
|
|
t.Fatalf("StateReader: %v", err)
|
|
}
|
|
if len(elems) == 0 {
|
|
return
|
|
}
|
|
acc, err := sr.Account(common.BytesToHash(elems[0].k))
|
|
if err != nil {
|
|
t.Fatalf("flat-state read failed after AdoptSyncedState: %v", err)
|
|
}
|
|
if acc == nil {
|
|
t.Fatal("flat-state read returned nil account; sync did not populate the snapshot namespace")
|
|
}
|
|
}
|
|
|
|
// TestSyncTinyTriePanicV2 tests a basic sync with one peer, and a tiny trie. This caused a
|
|
// panic within the prover
|
|
func TestSyncTinyTriePanicV2(t *testing.T) {
|
|
t.Parallel()
|
|
testSyncTinyTriePanicV2(t, rawdb.HashScheme)
|
|
testSyncTinyTriePanicV2(t, rawdb.PathScheme)
|
|
}
|
|
|
|
func testSyncTinyTriePanicV2(t *testing.T, scheme string) {
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(1, scheme)
|
|
|
|
mkSource := func(name string) *testPeerV2 {
|
|
source := newTestPeerV2(name, t, term)
|
|
source.accountTrie = sourceAccountTrie.Copy()
|
|
source.accountValues = elems
|
|
return source
|
|
}
|
|
syncer := setupSyncerV2(nodeScheme, mkSource("source"))
|
|
done := checkStall(t, term)
|
|
if err := syncer.Sync(mkPivot(0, sourceAccountTrie.Hash()), cancel); err != nil {
|
|
t.Fatalf("sync failed: %v", err)
|
|
}
|
|
close(done)
|
|
verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
|
|
}
|
|
|
|
// TestMultiSyncV2 tests a basic sync with multiple peers
|
|
func TestMultiSyncV2(t *testing.T) {
|
|
t.Parallel()
|
|
testMultiSyncV2(t, rawdb.HashScheme)
|
|
testMultiSyncV2(t, rawdb.PathScheme)
|
|
}
|
|
|
|
func testMultiSyncV2(t *testing.T, scheme string) {
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, scheme)
|
|
|
|
mkSource := func(name string) *testPeerV2 {
|
|
source := newTestPeerV2(name, t, term)
|
|
source.accountTrie = sourceAccountTrie.Copy()
|
|
source.accountValues = elems
|
|
return source
|
|
}
|
|
syncer := setupSyncerV2(nodeScheme, mkSource("sourceA"), mkSource("sourceB"))
|
|
done := checkStall(t, term)
|
|
if err := syncer.Sync(mkPivot(0, sourceAccountTrie.Hash()), cancel); err != nil {
|
|
t.Fatalf("sync failed: %v", err)
|
|
}
|
|
close(done)
|
|
verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
|
|
}
|
|
|
|
// TestSyncWithStorageV2 tests basic sync using accounts + storage + code
|
|
func TestSyncWithStorageV2(t *testing.T) {
|
|
t.Parallel()
|
|
testSyncWithStorageV2(t, rawdb.HashScheme)
|
|
testSyncWithStorageV2(t, rawdb.PathScheme)
|
|
}
|
|
|
|
func testSyncWithStorageV2(t *testing.T, scheme string) {
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 3, 3000, true, false, false)
|
|
|
|
mkSource := func(name string) *testPeerV2 {
|
|
source := newTestPeerV2(name, t, term)
|
|
source.accountTrie = sourceAccountTrie.Copy()
|
|
source.accountValues = elems
|
|
source.setStorageTries(storageTries)
|
|
source.storageValues = storageElems
|
|
return source
|
|
}
|
|
syncer := setupSyncerV2(scheme, mkSource("sourceA"))
|
|
done := checkStall(t, term)
|
|
if err := syncer.Sync(mkPivot(0, sourceAccountTrie.Hash()), cancel); err != nil {
|
|
t.Fatalf("sync failed: %v", err)
|
|
}
|
|
close(done)
|
|
verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
|
|
}
|
|
|
|
// TestMultiSyncManyUselessV2 contains one good peer, and many which doesn't return anything valuable at all
|
|
func TestMultiSyncManyUselessV2(t *testing.T) {
|
|
t.Parallel()
|
|
testMultiSyncManyUselessV2(t, rawdb.HashScheme)
|
|
testMultiSyncManyUselessV2(t, rawdb.PathScheme)
|
|
}
|
|
|
|
func testMultiSyncManyUselessV2(t *testing.T, scheme string) {
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 300, true, false, false)
|
|
|
|
mkSource := func(name string, noAccount, noStorage bool) *testPeerV2 {
|
|
source := newTestPeerV2(name, t, term)
|
|
source.accountTrie = sourceAccountTrie.Copy()
|
|
source.accountValues = elems
|
|
source.setStorageTries(storageTries)
|
|
source.storageValues = storageElems
|
|
if !noAccount {
|
|
source.accountRequestV2Handler = emptyRequestAccountRangeFnV2
|
|
}
|
|
if !noStorage {
|
|
source.storageRequestV2Handler = emptyStorageRequestHandlerV2
|
|
}
|
|
return source
|
|
}
|
|
syncer := setupSyncerV2(
|
|
scheme,
|
|
mkSource("full", true, true),
|
|
mkSource("noAccounts", false, true),
|
|
mkSource("noStorage", true, false),
|
|
)
|
|
done := checkStall(t, term)
|
|
if err := syncer.Sync(mkPivot(0, sourceAccountTrie.Hash()), cancel); err != nil {
|
|
t.Fatalf("sync failed: %v", err)
|
|
}
|
|
close(done)
|
|
verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
|
|
}
|
|
|
|
// TestMultiSyncManyUselessWithLowTimeoutV2 contains one good peer, and many which doesn't return anything valuable at all
|
|
func TestMultiSyncManyUselessWithLowTimeoutV2(t *testing.T) {
|
|
t.Parallel()
|
|
testMultiSyncManyUselessWithLowTimeoutV2(t, rawdb.HashScheme)
|
|
testMultiSyncManyUselessWithLowTimeoutV2(t, rawdb.PathScheme)
|
|
}
|
|
|
|
func testMultiSyncManyUselessWithLowTimeoutV2(t *testing.T, scheme string) {
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 300, true, false, false)
|
|
|
|
mkSource := func(name string, noAccount, noStorage bool) *testPeerV2 {
|
|
source := newTestPeerV2(name, t, term)
|
|
source.accountTrie = sourceAccountTrie.Copy()
|
|
source.accountValues = elems
|
|
source.setStorageTries(storageTries)
|
|
source.storageValues = storageElems
|
|
if !noAccount {
|
|
source.accountRequestV2Handler = emptyRequestAccountRangeFnV2
|
|
}
|
|
if !noStorage {
|
|
source.storageRequestV2Handler = emptyStorageRequestHandlerV2
|
|
}
|
|
return source
|
|
}
|
|
syncer := setupSyncerV2(
|
|
scheme,
|
|
mkSource("full", true, true),
|
|
mkSource("noAccounts", false, true),
|
|
mkSource("noStorage", true, false),
|
|
)
|
|
syncer.rates.OverrideTTLLimit = time.Millisecond
|
|
|
|
done := checkStall(t, term)
|
|
if err := syncer.Sync(mkPivot(0, sourceAccountTrie.Hash()), cancel); err != nil {
|
|
t.Fatalf("sync failed: %v", err)
|
|
}
|
|
close(done)
|
|
verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
|
|
}
|
|
|
|
// TestMultiSyncManyUnresponsiveV2 contains one good peer, and many which doesn't respond at all
|
|
func TestMultiSyncManyUnresponsiveV2(t *testing.T) {
|
|
t.Parallel()
|
|
testMultiSyncManyUnresponsiveV2(t, rawdb.HashScheme)
|
|
testMultiSyncManyUnresponsiveV2(t, rawdb.PathScheme)
|
|
}
|
|
|
|
func testMultiSyncManyUnresponsiveV2(t *testing.T, scheme string) {
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 300, true, false, false)
|
|
|
|
mkSource := func(name string, noAccount, noStorage bool) *testPeerV2 {
|
|
source := newTestPeerV2(name, t, term)
|
|
source.accountTrie = sourceAccountTrie.Copy()
|
|
source.accountValues = elems
|
|
source.setStorageTries(storageTries)
|
|
source.storageValues = storageElems
|
|
if !noAccount {
|
|
source.accountRequestV2Handler = nonResponsiveRequestAccountRangeFnV2
|
|
}
|
|
if !noStorage {
|
|
source.storageRequestV2Handler = nonResponsiveStorageRequestHandlerV2
|
|
}
|
|
return source
|
|
}
|
|
syncer := setupSyncerV2(
|
|
scheme,
|
|
mkSource("full", true, true),
|
|
mkSource("noAccounts", false, true),
|
|
mkSource("noStorage", true, false),
|
|
)
|
|
syncer.rates.OverrideTTLLimit = time.Millisecond
|
|
|
|
done := checkStall(t, term)
|
|
if err := syncer.Sync(mkPivot(0, sourceAccountTrie.Hash()), cancel); err != nil {
|
|
t.Fatalf("sync failed: %v", err)
|
|
}
|
|
close(done)
|
|
verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
|
|
}
|
|
|
|
// TestSyncBoundaryAccountTrieV2 tests sync against a few normal peers, but the
|
|
// account trie has a few boundary elements.
|
|
func TestSyncBoundaryAccountTrieV2(t *testing.T) {
|
|
t.Parallel()
|
|
testSyncBoundaryAccountTrieV2(t, rawdb.HashScheme)
|
|
testSyncBoundaryAccountTrieV2(t, rawdb.PathScheme)
|
|
}
|
|
|
|
func testSyncBoundaryAccountTrieV2(t *testing.T, scheme string) {
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
nodeScheme, sourceAccountTrie, elems := makeBoundaryAccountTrie(scheme, 3000)
|
|
|
|
mkSource := func(name string) *testPeerV2 {
|
|
source := newTestPeerV2(name, t, term)
|
|
source.accountTrie = sourceAccountTrie.Copy()
|
|
source.accountValues = elems
|
|
return source
|
|
}
|
|
syncer := setupSyncerV2(
|
|
nodeScheme,
|
|
mkSource("peer-a"),
|
|
mkSource("peer-b"),
|
|
)
|
|
done := checkStall(t, term)
|
|
if err := syncer.Sync(mkPivot(0, sourceAccountTrie.Hash()), cancel); err != nil {
|
|
t.Fatalf("sync failed: %v", err)
|
|
}
|
|
close(done)
|
|
verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
|
|
}
|
|
|
|
// TestSyncNoStorageAndOneCappedPeerV2 tests sync using accounts and no storage, where one peer is
|
|
// consistently returning very small results
|
|
func TestSyncNoStorageAndOneCappedPeerV2(t *testing.T) {
|
|
t.Parallel()
|
|
testSyncNoStorageAndOneCappedPeerV2(t, rawdb.HashScheme)
|
|
testSyncNoStorageAndOneCappedPeerV2(t, rawdb.PathScheme)
|
|
}
|
|
|
|
func testSyncNoStorageAndOneCappedPeerV2(t *testing.T, scheme string) {
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(3000, scheme)
|
|
|
|
mkSource := func(name string, slow bool) *testPeerV2 {
|
|
source := newTestPeerV2(name, t, term)
|
|
source.accountTrie = sourceAccountTrie.Copy()
|
|
source.accountValues = elems
|
|
if slow {
|
|
source.accountRequestV2Handler = starvingAccountRequestHandlerV2
|
|
}
|
|
return source
|
|
}
|
|
|
|
syncer := setupSyncerV2(
|
|
nodeScheme,
|
|
mkSource("nice-a", false),
|
|
mkSource("nice-b", false),
|
|
mkSource("nice-c", false),
|
|
mkSource("capped", true),
|
|
)
|
|
done := checkStall(t, term)
|
|
if err := syncer.Sync(mkPivot(0, sourceAccountTrie.Hash()), cancel); err != nil {
|
|
t.Fatalf("sync failed: %v", err)
|
|
}
|
|
close(done)
|
|
verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
|
|
}
|
|
|
|
// TestSyncNoStorageAndOneCodeCorruptPeerV2 has one peer which doesn't deliver
|
|
// code requests properly.
|
|
func TestSyncNoStorageAndOneCodeCorruptPeerV2(t *testing.T) {
|
|
t.Parallel()
|
|
testSyncNoStorageAndOneCodeCorruptPeerV2(t, rawdb.HashScheme)
|
|
testSyncNoStorageAndOneCodeCorruptPeerV2(t, rawdb.PathScheme)
|
|
}
|
|
|
|
func testSyncNoStorageAndOneCodeCorruptPeerV2(t *testing.T, scheme string) {
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(3000, scheme)
|
|
|
|
mkSource := func(name string, codeFn codeHandlerFuncV2) *testPeerV2 {
|
|
source := newTestPeerV2(name, t, term)
|
|
source.accountTrie = sourceAccountTrie.Copy()
|
|
source.accountValues = elems
|
|
source.codeRequestHandler = codeFn
|
|
return source
|
|
}
|
|
syncer := setupSyncerV2(
|
|
nodeScheme,
|
|
mkSource("capped", cappedCodeRequestHandlerV2),
|
|
mkSource("corrupt", corruptCodeRequestHandlerV2),
|
|
)
|
|
done := checkStall(t, term)
|
|
if err := syncer.Sync(mkPivot(0, sourceAccountTrie.Hash()), cancel); err != nil {
|
|
t.Fatalf("sync failed: %v", err)
|
|
}
|
|
close(done)
|
|
verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
|
|
}
|
|
|
|
func TestSyncNoStorageAndOneAccountCorruptPeerV2(t *testing.T) {
|
|
t.Parallel()
|
|
testSyncNoStorageAndOneAccountCorruptPeerV2(t, rawdb.HashScheme)
|
|
testSyncNoStorageAndOneAccountCorruptPeerV2(t, rawdb.PathScheme)
|
|
}
|
|
|
|
func testSyncNoStorageAndOneAccountCorruptPeerV2(t *testing.T, scheme string) {
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(3000, scheme)
|
|
|
|
mkSource := func(name string, accFn accountHandlerFuncV2) *testPeerV2 {
|
|
source := newTestPeerV2(name, t, term)
|
|
source.accountTrie = sourceAccountTrie.Copy()
|
|
source.accountValues = elems
|
|
source.accountRequestV2Handler = accFn
|
|
return source
|
|
}
|
|
syncer := setupSyncerV2(
|
|
nodeScheme,
|
|
mkSource("capped", defaultAccountRequestHandlerV2),
|
|
mkSource("corrupt", corruptAccountRequestHandlerV2),
|
|
)
|
|
done := checkStall(t, term)
|
|
if err := syncer.Sync(mkPivot(0, sourceAccountTrie.Hash()), cancel); err != nil {
|
|
t.Fatalf("sync failed: %v", err)
|
|
}
|
|
close(done)
|
|
verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
|
|
}
|
|
|
|
// TestSyncNoStorageAndOneCodeCappedPeerV2 has one peer which delivers code hashes
|
|
// one by one
|
|
func TestSyncNoStorageAndOneCodeCappedPeerV2(t *testing.T) {
|
|
t.Parallel()
|
|
testSyncNoStorageAndOneCodeCappedPeerV2(t, rawdb.HashScheme)
|
|
testSyncNoStorageAndOneCodeCappedPeerV2(t, rawdb.PathScheme)
|
|
}
|
|
|
|
func testSyncNoStorageAndOneCodeCappedPeerV2(t *testing.T, scheme string) {
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(3000, scheme)
|
|
|
|
mkSource := func(name string, codeFn codeHandlerFuncV2) *testPeerV2 {
|
|
source := newTestPeerV2(name, t, term)
|
|
source.accountTrie = sourceAccountTrie.Copy()
|
|
source.accountValues = elems
|
|
source.codeRequestHandler = codeFn
|
|
return source
|
|
}
|
|
var counter int
|
|
syncer := setupSyncerV2(
|
|
nodeScheme,
|
|
mkSource("capped", func(t *testPeerV2, id uint64, hashes []common.Hash, max int) error {
|
|
counter++
|
|
return cappedCodeRequestHandlerV2(t, id, hashes, max)
|
|
}),
|
|
)
|
|
done := checkStall(t, term)
|
|
if err := syncer.Sync(mkPivot(0, sourceAccountTrie.Hash()), cancel); err != nil {
|
|
t.Fatalf("sync failed: %v", err)
|
|
}
|
|
close(done)
|
|
|
|
if threshold := 100; counter > threshold {
|
|
t.Logf("Error, expected < %d invocations, got %d", threshold, counter)
|
|
}
|
|
verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
|
|
}
|
|
|
|
// TestSyncBoundaryStorageTrieV2 tests sync against a few normal peers, but the
|
|
// storage trie has a few boundary elements.
|
|
func TestSyncBoundaryStorageTrieV2(t *testing.T) {
|
|
t.Parallel()
|
|
testSyncBoundaryStorageTrieV2(t, rawdb.HashScheme)
|
|
testSyncBoundaryStorageTrieV2(t, rawdb.PathScheme)
|
|
}
|
|
|
|
func testSyncBoundaryStorageTrieV2(t *testing.T, scheme string) {
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 10, 1000, false, true, false)
|
|
|
|
mkSource := func(name string) *testPeerV2 {
|
|
source := newTestPeerV2(name, t, term)
|
|
source.accountTrie = sourceAccountTrie.Copy()
|
|
source.accountValues = elems
|
|
source.setStorageTries(storageTries)
|
|
source.storageValues = storageElems
|
|
return source
|
|
}
|
|
syncer := setupSyncerV2(
|
|
scheme,
|
|
mkSource("peer-a"),
|
|
mkSource("peer-b"),
|
|
)
|
|
done := checkStall(t, term)
|
|
if err := syncer.Sync(mkPivot(0, sourceAccountTrie.Hash()), cancel); err != nil {
|
|
t.Fatalf("sync failed: %v", err)
|
|
}
|
|
close(done)
|
|
verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
|
|
}
|
|
|
|
// TestSyncWithStorageAndOneCappedPeerV2 tests sync using accounts + storage, where one peer is
|
|
// consistently returning very small results
|
|
func TestSyncWithStorageAndOneCappedPeerV2(t *testing.T) {
|
|
t.Parallel()
|
|
testSyncWithStorageAndOneCappedPeerV2(t, rawdb.HashScheme)
|
|
testSyncWithStorageAndOneCappedPeerV2(t, rawdb.PathScheme)
|
|
}
|
|
|
|
func testSyncWithStorageAndOneCappedPeerV2(t *testing.T, scheme string) {
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 300, 100, false, false, false)
|
|
|
|
mkSource := func(name string, slow bool) *testPeerV2 {
|
|
source := newTestPeerV2(name, t, term)
|
|
source.accountTrie = sourceAccountTrie.Copy()
|
|
source.accountValues = elems
|
|
source.setStorageTries(storageTries)
|
|
source.storageValues = storageElems
|
|
if slow {
|
|
source.storageRequestV2Handler = starvingStorageRequestHandlerV2
|
|
}
|
|
return source
|
|
}
|
|
syncer := setupSyncerV2(
|
|
scheme,
|
|
mkSource("nice-a", false),
|
|
mkSource("slow", true),
|
|
)
|
|
done := checkStall(t, term)
|
|
if err := syncer.Sync(mkPivot(0, sourceAccountTrie.Hash()), cancel); err != nil {
|
|
t.Fatalf("sync failed: %v", err)
|
|
}
|
|
close(done)
|
|
verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
|
|
}
|
|
|
|
// TestSyncWithStorageAndCorruptPeerV2 tests sync using accounts + storage, where one peer is
|
|
// sometimes sending bad proofs
|
|
func TestSyncWithStorageAndCorruptPeerV2(t *testing.T) {
|
|
t.Parallel()
|
|
testSyncWithStorageAndCorruptPeerV2(t, rawdb.HashScheme)
|
|
testSyncWithStorageAndCorruptPeerV2(t, rawdb.PathScheme)
|
|
}
|
|
|
|
func testSyncWithStorageAndCorruptPeerV2(t *testing.T, scheme string) {
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 300, true, false, false)
|
|
|
|
mkSource := func(name string, handler storageHandlerFuncV2) *testPeerV2 {
|
|
source := newTestPeerV2(name, t, term)
|
|
source.accountTrie = sourceAccountTrie.Copy()
|
|
source.accountValues = elems
|
|
source.setStorageTries(storageTries)
|
|
source.storageValues = storageElems
|
|
source.storageRequestV2Handler = handler
|
|
return source
|
|
}
|
|
syncer := setupSyncerV2(
|
|
scheme,
|
|
mkSource("nice-a", defaultStorageRequestHandlerV2),
|
|
mkSource("nice-b", defaultStorageRequestHandlerV2),
|
|
mkSource("nice-c", defaultStorageRequestHandlerV2),
|
|
mkSource("corrupt", corruptStorageRequestHandlerV2),
|
|
)
|
|
done := checkStall(t, term)
|
|
if err := syncer.Sync(mkPivot(0, sourceAccountTrie.Hash()), cancel); err != nil {
|
|
t.Fatalf("sync failed: %v", err)
|
|
}
|
|
close(done)
|
|
verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
|
|
}
|
|
|
|
func TestSyncWithStorageAndNonProvingPeerV2(t *testing.T) {
|
|
t.Parallel()
|
|
testSyncWithStorageAndNonProvingPeerV2(t, rawdb.HashScheme)
|
|
testSyncWithStorageAndNonProvingPeerV2(t, rawdb.PathScheme)
|
|
}
|
|
|
|
func testSyncWithStorageAndNonProvingPeerV2(t *testing.T, scheme string) {
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 300, true, false, false)
|
|
|
|
mkSource := func(name string, handler storageHandlerFuncV2) *testPeerV2 {
|
|
source := newTestPeerV2(name, t, term)
|
|
source.accountTrie = sourceAccountTrie.Copy()
|
|
source.accountValues = elems
|
|
source.setStorageTries(storageTries)
|
|
source.storageValues = storageElems
|
|
source.storageRequestV2Handler = handler
|
|
return source
|
|
}
|
|
syncer := setupSyncerV2(
|
|
scheme,
|
|
mkSource("nice-a", defaultStorageRequestHandlerV2),
|
|
mkSource("nice-b", defaultStorageRequestHandlerV2),
|
|
mkSource("nice-c", defaultStorageRequestHandlerV2),
|
|
mkSource("corrupt", noProofStorageRequestHandlerV2),
|
|
)
|
|
done := checkStall(t, term)
|
|
if err := syncer.Sync(mkPivot(0, sourceAccountTrie.Hash()), cancel); err != nil {
|
|
t.Fatalf("sync failed: %v", err)
|
|
}
|
|
close(done)
|
|
verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
|
|
}
|
|
|
|
// TestSyncWithStorageMisbehavingProveV2 tests basic sync using accounts + storage + code, against
|
|
// a peer who insists on delivering full storage sets _and_ proofs. This triggered
|
|
// an error, where the recipient erroneously clipped the boundary nodes, but
|
|
// did not mark the account for healing.
|
|
func TestSyncWithStorageMisbehavingProveV2(t *testing.T) {
|
|
t.Parallel()
|
|
testSyncWithStorageMisbehavingProveV2(t, rawdb.HashScheme)
|
|
testSyncWithStorageMisbehavingProveV2(t, rawdb.PathScheme)
|
|
}
|
|
|
|
func testSyncWithStorageMisbehavingProveV2(t *testing.T, scheme string) {
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
nodeScheme, sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorageWithUniqueStorage(scheme, 10, 30, false)
|
|
|
|
mkSource := func(name string) *testPeerV2 {
|
|
source := newTestPeerV2(name, t, term)
|
|
source.accountTrie = sourceAccountTrie.Copy()
|
|
source.accountValues = elems
|
|
source.setStorageTries(storageTries)
|
|
source.storageValues = storageElems
|
|
source.storageRequestV2Handler = proofHappyStorageRequestHandlerV2
|
|
return source
|
|
}
|
|
syncer := setupSyncerV2(nodeScheme, mkSource("sourceA"))
|
|
if err := syncer.Sync(mkPivot(0, sourceAccountTrie.Hash()), cancel); err != nil {
|
|
t.Fatalf("sync failed: %v", err)
|
|
}
|
|
verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
|
|
}
|
|
|
|
// TestSyncWithUnevenStorageV2 tests sync where the storage trie is not even
|
|
// and with a few empty ranges.
|
|
func TestSyncWithUnevenStorageV2(t *testing.T) {
|
|
t.Parallel()
|
|
testSyncWithUnevenStorageV2(t, rawdb.HashScheme)
|
|
testSyncWithUnevenStorageV2(t, rawdb.PathScheme)
|
|
}
|
|
|
|
func testSyncWithUnevenStorageV2(t *testing.T, scheme string) {
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
accountTrie, accounts, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 3, 256, false, false, true)
|
|
|
|
mkSource := func(name string) *testPeerV2 {
|
|
source := newTestPeerV2(name, t, term)
|
|
source.accountTrie = accountTrie.Copy()
|
|
source.accountValues = accounts
|
|
source.setStorageTries(storageTries)
|
|
source.storageValues = storageElems
|
|
source.storageRequestV2Handler = func(t *testPeerV2, reqId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max int) error {
|
|
return defaultStorageRequestHandlerV2(t, reqId, root, accounts, origin, limit, 128) // retrieve storage in large mode
|
|
}
|
|
return source
|
|
}
|
|
syncer := setupSyncerV2(scheme, mkSource("source"))
|
|
if err := syncer.Sync(mkPivot(0, accountTrie.Hash()), cancel); err != nil {
|
|
t.Fatalf("sync failed: %v", err)
|
|
}
|
|
verifyTrie(scheme, syncer.db, accountTrie.Hash(), t)
|
|
}
|
|
|
|
// makeAccountTrieWithAddresses creates an account trie keyed by keccak(address),
|
|
// matching production behavior. Returns the trie, sorted entries, and the
|
|
// addresses used. This allows BAL-based tests to target specific addresses and
|
|
// have applyAccessList write to the same snapshot keys as the download.
|
|
func makeAccountTrieWithAddresses(n int, scheme string) (string, *trie.Trie, []*kv, []common.Address) {
|
|
var (
|
|
db = triedb.NewDatabase(rawdb.NewMemoryDatabase(), newDbConfig(scheme))
|
|
accTrie = trie.NewEmpty(db)
|
|
entries []*kv
|
|
addrs []common.Address
|
|
)
|
|
for i := uint64(1); i <= uint64(n); i++ {
|
|
// Deterministic address from index
|
|
addr := common.BigToAddress(new(big.Int).SetUint64(i))
|
|
addrs = append(addrs, addr)
|
|
|
|
value, _ := rlp.EncodeToBytes(&types.StateAccount{
|
|
Nonce: i,
|
|
Balance: uint256.NewInt(i),
|
|
Root: types.EmptyRootHash,
|
|
CodeHash: types.EmptyCodeHash[:],
|
|
})
|
|
key := crypto.Keccak256(addr[:])
|
|
elem := &kv{key, value}
|
|
accTrie.MustUpdate(elem.k, elem.v)
|
|
entries = append(entries, elem)
|
|
}
|
|
slices.SortFunc(entries, (*kv).cmp)
|
|
|
|
root, nodes := accTrie.Commit(false)
|
|
db.Update(root, types.EmptyRootHash, 0, trienode.NewWithNodeSet(nodes), triedb.NewStateSet())
|
|
|
|
accTrie, _ = trie.New(trie.StateTrieID(root), db)
|
|
return db.Scheme(), accTrie, entries, addrs
|
|
}
|
|
|
|
// TestIsPivotReorged verifies the four conditions isPivotReorged covers:
|
|
// reorged out, non-advancing pivot, missing canonical, and the happy path
|
|
// where the previous pivot is still canonical and the new pivot advances.
|
|
func TestIsPivotReorged(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Reorged: canonical hash at prev's height differs from prev. The
|
|
// previous pivot was reorged out by an alternate chain at the same
|
|
// (or higher) height.
|
|
t.Run("Reorged_DifferentHash", func(t *testing.T) {
|
|
db := rawdb.NewMemoryDatabase()
|
|
prev := mkPivot(100, common.HexToHash("0xaaaa"))
|
|
curr := mkPivot(105, common.HexToHash("0xcccc"))
|
|
canonical := mkPivot(100, common.HexToHash("0xbbbb"))
|
|
rawdb.WriteHeader(db, canonical)
|
|
rawdb.WriteCanonicalHash(db, canonical.Hash(), canonical.Number.Uint64())
|
|
|
|
if !isPivotReorged(db, prev, curr) {
|
|
t.Fatal("expected reorg detection when canonical hash differs")
|
|
}
|
|
})
|
|
|
|
// NonAdvancingPivot: new pivot is at or below the old one. There's
|
|
// nothing for catchUp to roll forward, regardless of canonical state.
|
|
t.Run("NonAdvancingPivot", func(t *testing.T) {
|
|
db := rawdb.NewMemoryDatabase()
|
|
prev := mkPivot(100, common.HexToHash("0xaaaa"))
|
|
curr := mkPivot(95, common.HexToHash("0xcccc"))
|
|
rawdb.WriteHeader(db, prev)
|
|
rawdb.WriteCanonicalHash(db, prev.Hash(), prev.Number.Uint64())
|
|
|
|
if !isPivotReorged(db, prev, curr) {
|
|
t.Fatal("expected reorg detection when new pivot is at or below the old one")
|
|
}
|
|
})
|
|
|
|
// MissingCanonical: canonical hash at prev's height is absent while
|
|
// curr advances past it. By the time Sync is called, headers up to
|
|
// curr should be indexed, so this implies broken chain state.
|
|
t.Run("MissingCanonical", func(t *testing.T) {
|
|
db := rawdb.NewMemoryDatabase()
|
|
prev := mkPivot(100, common.HexToHash("0xaaaa"))
|
|
curr := mkPivot(105, common.HexToHash("0xcccc"))
|
|
|
|
if !isPivotReorged(db, prev, curr) {
|
|
t.Fatal("expected reorg detection when canonical hash is missing at prev's height")
|
|
}
|
|
})
|
|
|
|
// NotReorged_SameHash: prev is still canonical and curr advances past
|
|
// it. Catch-up is feasible.
|
|
t.Run("NotReorged_SameHash", func(t *testing.T) {
|
|
db := rawdb.NewMemoryDatabase()
|
|
prev := mkPivot(100, common.HexToHash("0xaaaa"))
|
|
curr := mkPivot(105, common.HexToHash("0xcccc"))
|
|
rawdb.WriteHeader(db, prev)
|
|
rawdb.WriteCanonicalHash(db, prev.Hash(), prev.Number.Uint64())
|
|
|
|
if isPivotReorged(db, prev, curr) {
|
|
t.Fatal("should not detect reorg when prev is canonical and curr advances")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestSyncDetectsPivotReorged exercises the reorg-handling branch in Sync
|
|
// end-to-end.
|
|
//
|
|
// Setup: persisted progress points at an orphan pivot at block 100; the new
|
|
// canonical header at block 100 has a different hash. Sync is then called with
|
|
// a new pivot at the same height.
|
|
//
|
|
// If isPivotReorged works, loadSyncStatus restores the persisted pivot, the
|
|
// check flags it as reorged, resetSyncState clears it, catchUp is skipped,
|
|
// and the fresh download proceeds to completion.
|
|
//
|
|
// If detection doesn't fire, the pivot-move check would call catchUp with
|
|
// from = 101 and to = 100 — the inverted-range guard surfaces that as an
|
|
// error, failing the test. So Sync returning nil is the positive signal that
|
|
// reorg detection and the reset worked.
|
|
func TestSyncDetectsPivotReorged(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, rawdb.HashScheme)
|
|
root := sourceAccountTrie.Hash()
|
|
|
|
db := rawdb.NewMemoryDatabase()
|
|
|
|
// Persist progress against an orphan pivot — same height as the new
|
|
// canonical pivot we'll sync to, different hash. Populate a partial task
|
|
// and non-zero counter so the reset path has something to clean up.
|
|
orphanPivot := mkPivot(100, common.HexToHash("0xdead"))
|
|
seed := newSyncerV2(db, nodeScheme)
|
|
// pivot reflects where flat state matches and it is what saveSyncStatus
|
|
// persists. Set it to simulate a prior sync reaching orphanPivot.
|
|
seed.pivot = orphanPivot
|
|
seed.accountSynced = 42
|
|
seed.tasks = []*accountTaskV2{{
|
|
Next: common.HexToHash("0x80"),
|
|
Last: common.MaxHash,
|
|
SubTasks: make(map[common.Hash][]*storageTaskV2),
|
|
}}
|
|
seed.saveSyncStatus()
|
|
|
|
// Pre-write orphan flat-state entries at hashes the test peer won't
|
|
// re-serve. After resetSyncState wipes the snapshot ranges, these
|
|
// should be gone.
|
|
orphanAccountHash := common.HexToHash("0xdeadbeef")
|
|
rawdb.WriteAccountSnapshot(db, orphanAccountHash, []byte{0xde, 0xad})
|
|
orphanStorageAccount := common.HexToHash("0xfeedfacefeedfacefeedfacefeedfacefeedfacefeedfacefeedfacefeedface")
|
|
orphanStorageSlot := common.HexToHash("0xabcd")
|
|
rawdb.WriteStorageSnapshot(db, orphanStorageAccount, orphanStorageSlot, []byte{0xff, 0xff})
|
|
|
|
// Canonical header at block 100 is newPivot — different hash from the
|
|
// orphan pivot, which is what isPivotReorged will detect.
|
|
newPivot := mkPivot(100, root)
|
|
rawdb.WriteHeader(db, newPivot)
|
|
rawdb.WriteCanonicalHash(db, newPivot.Hash(), newPivot.Number.Uint64())
|
|
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
syncer := newSyncerV2(db, nodeScheme)
|
|
src := newTestPeerV2("source", t, term)
|
|
src.accountTrie = sourceAccountTrie.Copy()
|
|
src.accountValues = elems
|
|
syncer.Register(src)
|
|
src.remote = syncer
|
|
|
|
if err := syncer.Sync(newPivot, cancel); err != nil {
|
|
t.Fatalf("sync failed (reorg detection likely broken): %v", err)
|
|
}
|
|
// After successful completion, status should reach the complete phase
|
|
// against the new (canonical) pivot.
|
|
loader := newSyncerV2(db, nodeScheme)
|
|
loader.loadSyncStatus()
|
|
if loader.getPhase() != phaseComplete {
|
|
t.Fatal("sync status should reach the complete phase after successful completion")
|
|
}
|
|
if loader.pivot == nil || loader.pivot.Hash() != newPivot.Hash() {
|
|
t.Fatalf("expected persisted pivot to match new pivot")
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
|
|
// TestInterruptedDownloadRecovery verifies that partially completed download
|
|
// state is persisted and resumed on restart.
|
|
func TestInterruptedDownloadRecovery(t *testing.T) {
|
|
t.Parallel()
|
|
testInterruptedDownloadRecovery(t, rawdb.HashScheme)
|
|
testInterruptedDownloadRecovery(t, rawdb.PathScheme)
|
|
}
|
|
|
|
func testInterruptedDownloadRecovery(t *testing.T, scheme string) {
|
|
nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, scheme)
|
|
root := sourceAccountTrie.Hash()
|
|
|
|
// Cancel after exactly 2 account range responses, guaranteeing partial
|
|
// completion without any timing dependency.
|
|
var (
|
|
once1 sync.Once
|
|
cancel1 = make(chan struct{})
|
|
term1 = func() { once1.Do(func() { close(cancel1) }) }
|
|
responses atomic.Int32
|
|
)
|
|
cancelAfterHandler := func(tp *testPeerV2, id uint64, root common.Hash, origin common.Hash, limit common.Hash, cap int) error {
|
|
if responses.Add(1) > 2 {
|
|
term1()
|
|
return nil
|
|
}
|
|
return defaultAccountRequestHandlerV2(tp, id, root, origin, limit, cap)
|
|
}
|
|
db := rawdb.NewMemoryDatabase()
|
|
syncer1 := newSyncerV2(db, nodeScheme)
|
|
src1 := newTestPeerV2("source1", t, term1)
|
|
src1.accountTrie = sourceAccountTrie.Copy()
|
|
src1.accountValues = elems
|
|
src1.accountRequestV2Handler = cancelAfterHandler
|
|
syncer1.Register(src1)
|
|
src1.remote = syncer1
|
|
pivot := mkPivot(0, root)
|
|
syncer1.loadSyncStatus()
|
|
syncer1.pivot = pivot // Sync pins this before downloadState
|
|
syncer1.downloadState(cancel1)
|
|
|
|
// Save progress
|
|
for _, task := range syncer1.tasks {
|
|
syncer1.forwardAccountTask(task)
|
|
}
|
|
syncer1.cleanAccountTasks()
|
|
syncer1.saveSyncStatus()
|
|
|
|
// Count how many accounts were downloaded in the first run.
|
|
// Due to the async nature of response processing, the cancel may race
|
|
// with delivery so 0 accounts may be written.
|
|
firstRunCount := 0
|
|
for _, entry := range elems {
|
|
if data := rawdb.ReadAccountSnapshot(db, common.BytesToHash(entry.k)); len(data) > 0 {
|
|
firstRunCount++
|
|
}
|
|
}
|
|
if firstRunCount == len(elems) {
|
|
t.Fatal("first run should not have downloaded everything")
|
|
}
|
|
|
|
// Second run: resume with same root, should complete the download
|
|
var (
|
|
once2 sync.Once
|
|
cancel2 = make(chan struct{})
|
|
term2 = func() { once2.Do(func() { close(cancel2) }) }
|
|
)
|
|
syncer2 := newSyncerV2(db, nodeScheme)
|
|
src2 := newTestPeerV2("source2", t, term2)
|
|
src2.accountTrie = sourceAccountTrie.Copy()
|
|
src2.accountValues = elems
|
|
syncer2.Register(src2)
|
|
src2.remote = syncer2
|
|
pivot2 := mkPivot(0, root)
|
|
syncer2.loadSyncStatus()
|
|
syncer2.pivot = pivot2 // Sync pins this before downloadState
|
|
if err := syncer2.downloadState(cancel2); err != nil {
|
|
t.Fatalf("resumed download failed: %v", err)
|
|
}
|
|
|
|
// Verify all accounts are now present
|
|
for _, entry := range elems {
|
|
if data := rawdb.ReadAccountSnapshot(db, common.BytesToHash(entry.k)); len(data) == 0 {
|
|
t.Errorf("missing account after resumed download: %x", entry.k)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSyncPersistsPivotDuringDownload verifies that after a fresh Sync is
|
|
// interrupted mid-download, the persisted pivot equals the current pivot
|
|
// (not nil). Without this, a follow-up Sync at a different pivot would not
|
|
// see that the partial flat state belongs to the old pivot, and would mix
|
|
// old-pivot accounts with new-pivot data.
|
|
func TestSyncPersistsPivotDuringDownload(t *testing.T) {
|
|
t.Parallel()
|
|
nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, rawdb.HashScheme)
|
|
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
responses atomic.Int32
|
|
)
|
|
db := rawdb.NewMemoryDatabase()
|
|
syncer := newSyncerV2(db, nodeScheme)
|
|
src := newTestPeerV2("source", t, term)
|
|
src.accountTrie = sourceAccountTrie.Copy()
|
|
src.accountValues = elems
|
|
src.accountRequestV2Handler = func(tp *testPeerV2, id uint64, root common.Hash, origin common.Hash, limit common.Hash, cap int) error {
|
|
if responses.Add(1) > 2 {
|
|
term()
|
|
return nil
|
|
}
|
|
return defaultAccountRequestHandlerV2(tp, id, root, origin, limit, cap)
|
|
}
|
|
syncer.Register(src)
|
|
src.remote = syncer
|
|
|
|
pivot := mkPivot(0, sourceAccountTrie.Hash())
|
|
// Sync should be interrupted by the cancel after a couple of responses.
|
|
_ = syncer.Sync(pivot, cancel)
|
|
|
|
// Persisted pivot must equal the pivot, so a follow-up Sync at a different
|
|
// pivot can recognize the partial flat state belongs to this one.
|
|
loader := newSyncerV2(db, nodeScheme)
|
|
loader.loadSyncStatus()
|
|
if loader.pivot == nil {
|
|
t.Fatal("expected persisted pivot to be set after interrupted download, got nil")
|
|
}
|
|
if loader.pivot.Hash() != pivot.Hash() {
|
|
t.Errorf("persisted pivot mismatch: got %v, want %v", loader.pivot.Hash(), pivot.Hash())
|
|
}
|
|
}
|
|
|
|
// TestPivotMovement verifies the full pivot move flow: download with rootA,
|
|
// cancel+restart with rootB, catch-up applies BAL diffs, download resumes
|
|
// and completes against the new state.
|
|
func TestPivotMovement(t *testing.T) {
|
|
t.Parallel()
|
|
testPivotMovement(t, rawdb.HashScheme, 1)
|
|
testPivotMovement(t, rawdb.PathScheme, 1)
|
|
}
|
|
|
|
// TestPivotMovementRepeated verifies that multiple pivot moves work correctly.
|
|
func TestPivotMovementRepeated(t *testing.T) {
|
|
t.Parallel()
|
|
testPivotMovement(t, rawdb.HashScheme, 2)
|
|
testPivotMovement(t, rawdb.PathScheme, 2)
|
|
}
|
|
|
|
func testPivotMovement(t *testing.T, scheme string, pivotMoves int) {
|
|
// Use makeAccountTrieWithAddresses so trie keys are keccak(addr),
|
|
// matching what applyAccessList writes to the snapshot DB.
|
|
nodeScheme, sourceAccountTrie, elems, addrs := makeAccountTrieWithAddresses(100, scheme)
|
|
numA := uint64(100)
|
|
|
|
// Target account 50 for BAL changes
|
|
targetAddr := addrs[49]
|
|
targetHash := crypto.Keccak256Hash(targetAddr[:])
|
|
|
|
type pivotMove struct {
|
|
blockNum uint64
|
|
trie *trie.Trie
|
|
elems []*kv
|
|
root common.Hash
|
|
bals map[common.Hash]rlp.RawValue // header hash -> encoded BAL
|
|
balance *uint256.Int
|
|
}
|
|
|
|
// Build each pivot move: update account 50's balance in both the trie
|
|
// and a BAL, write the header, and record everything.
|
|
db := rawdb.NewMemoryDatabase()
|
|
currentElems := elems
|
|
moves := make([]pivotMove, pivotMoves)
|
|
emptyHash := common.Hash{}
|
|
zero := uint64(0)
|
|
for m := 0; m < pivotMoves; m++ {
|
|
blockNum := numA + uint64(m) + 1
|
|
balance := uint256.NewInt(uint64(1000 * (m + 1)))
|
|
|
|
// Build updated trie with new balance for account 50
|
|
trieDB := triedb.NewDatabase(rawdb.NewMemoryDatabase(), newDbConfig(scheme))
|
|
newTrie := trie.NewEmpty(trieDB)
|
|
newElems := make([]*kv, len(currentElems))
|
|
for i, entry := range currentElems {
|
|
if bytes.Equal(entry.k, targetHash[:]) {
|
|
val, _ := rlp.EncodeToBytes(&types.StateAccount{
|
|
Nonce: 50, Balance: balance,
|
|
Root: types.EmptyRootHash, CodeHash: types.EmptyCodeHash[:],
|
|
})
|
|
newElems[i] = &kv{entry.k, val}
|
|
} else {
|
|
newElems[i] = entry
|
|
}
|
|
newTrie.MustUpdate(newElems[i].k, newElems[i].v)
|
|
}
|
|
newRoot, nodes := newTrie.Commit(false)
|
|
trieDB.Update(newRoot, types.EmptyRootHash, 0, trienode.NewWithNodeSet(nodes), triedb.NewStateSet())
|
|
resultTrie, _ := trie.New(trie.StateTrieID(newRoot), trieDB)
|
|
|
|
// Build BAL matching the trie diff
|
|
cb := bal.NewConstructionBlockAccessList()
|
|
cb.BalanceChange(0, targetAddr, balance)
|
|
var buf bytes.Buffer
|
|
if err := cb.EncodeRLP(&buf); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Compute BAL hash, write header, store BAL keyed by header hash
|
|
var b bal.BlockAccessList
|
|
if err := rlp.DecodeBytes(buf.Bytes(), &b); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
balHash := b.Hash()
|
|
header := &types.Header{
|
|
Number: new(big.Int).SetUint64(blockNum), Difficulty: common.Big0,
|
|
BaseFee: common.Big0, WithdrawalsHash: &emptyHash,
|
|
BlobGasUsed: &zero, ExcessBlobGas: &zero,
|
|
ParentBeaconRoot: &emptyHash, RequestsHash: &emptyHash,
|
|
BlockAccessListHash: &balHash,
|
|
}
|
|
rawdb.WriteHeader(db, header)
|
|
headerHash := header.Hash()
|
|
rawdb.WriteCanonicalHash(db, headerHash, blockNum)
|
|
moves[m] = pivotMove{
|
|
blockNum: blockNum,
|
|
trie: resultTrie,
|
|
elems: newElems,
|
|
root: newRoot,
|
|
bals: map[common.Hash]rlp.RawValue{headerHash: buf.Bytes()},
|
|
balance: balance,
|
|
}
|
|
currentElems = newElems
|
|
}
|
|
|
|
// First run: download against rootA, cancel after 2 responses
|
|
rootA := sourceAccountTrie.Hash()
|
|
var (
|
|
once1 sync.Once
|
|
cancel1 = make(chan struct{})
|
|
term1 = func() { once1.Do(func() { close(cancel1) }) }
|
|
responses atomic.Int32
|
|
)
|
|
syncer1 := newSyncerV2(db, nodeScheme)
|
|
src1 := newTestPeerV2("source1", t, term1)
|
|
src1.accountTrie = sourceAccountTrie.Copy()
|
|
src1.accountValues = elems
|
|
src1.accountRequestV2Handler = func(tp *testPeerV2, id uint64, root common.Hash, origin common.Hash, limit common.Hash, cap int) error {
|
|
if responses.Add(1) > 2 {
|
|
term1()
|
|
return nil
|
|
}
|
|
return defaultAccountRequestHandlerV2(tp, id, root, origin, limit, cap)
|
|
}
|
|
syncer1.Register(src1)
|
|
src1.remote = syncer1
|
|
syncer1.Sync(mkPivot(numA, rootA), cancel1)
|
|
|
|
// Subsequent runs: each move triggers catch-up then resumes download
|
|
for i, move := range moves {
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
syncer := newSyncerV2(db, nodeScheme)
|
|
src := newTestPeerV2(fmt.Sprintf("source-%d", i+2), t, term)
|
|
src.accountTrie = move.trie.Copy()
|
|
src.accountValues = move.elems
|
|
src.accessLists = move.bals
|
|
syncer.Register(src)
|
|
src.remote = syncer
|
|
if err := syncer.Sync(mkPivot(move.blockNum, move.root), cancel); err != nil {
|
|
t.Fatalf("pivot move %d: sync failed: %v", i+1, err)
|
|
}
|
|
|
|
// Verify account 50's balance was updated by catch-up
|
|
data := rawdb.ReadAccountSnapshot(db, targetHash)
|
|
if len(data) == 0 {
|
|
t.Fatalf("pivot move %d: account 50 not found after sync", i+1)
|
|
}
|
|
account, aErr := types.FullAccount(data)
|
|
if aErr != nil {
|
|
t.Fatalf("pivot move %d: failed to decode account: %v", i+1, aErr)
|
|
}
|
|
if account.Balance.Cmp(move.balance) != 0 {
|
|
t.Errorf("pivot move %d: balance wrong: got %v, want %v", i+1, account.Balance, move.balance)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestCatchUpPersistsIncrementally verifies that catchUp updates and persists
|
|
// the pivot after each successfully applied BAL. If a later block in the
|
|
// gap fails to apply, the persisted state reflects the last successful block,
|
|
// so a follow-up Sync can resume from there rather than reapplying everything.
|
|
func TestCatchUpPersistsIncrementally(t *testing.T) {
|
|
t.Parallel()
|
|
testCatchUpPersistsIncrementally(t, rawdb.HashScheme)
|
|
testCatchUpPersistsIncrementally(t, rawdb.PathScheme)
|
|
}
|
|
|
|
func testCatchUpPersistsIncrementally(t *testing.T, scheme string) {
|
|
nodeScheme, sourceAccountTrie, elems, addrs := makeAccountTrieWithAddresses(100, scheme)
|
|
rootA := sourceAccountTrie.Hash()
|
|
numA := uint64(100)
|
|
|
|
goodAddr := addrs[0]
|
|
corruptAddr := addrs[1]
|
|
|
|
type balBlock struct {
|
|
header *types.Header
|
|
bal rlp.RawValue
|
|
}
|
|
|
|
db := rawdb.NewMemoryDatabase()
|
|
emptyHash := common.Hash{}
|
|
zero := uint64(0)
|
|
|
|
// Write the header and canonical hash for block A so the reorg-detection
|
|
// canonical-lookup in Sync passes (otherwise it'd treat A as reorged out
|
|
// and reset instead of running catchUp).
|
|
pivotAHeader := &types.Header{
|
|
Number: new(big.Int).SetUint64(numA), Root: rootA, Difficulty: common.Big0,
|
|
BaseFee: common.Big0, WithdrawalsHash: &emptyHash,
|
|
BlobGasUsed: &zero, ExcessBlobGas: &zero,
|
|
ParentBeaconRoot: &emptyHash, RequestsHash: &emptyHash,
|
|
}
|
|
rawdb.WriteHeader(db, pivotAHeader)
|
|
rawdb.WriteCanonicalHash(db, pivotAHeader.Hash(), numA)
|
|
pivotA := pivotAHeader
|
|
|
|
// Build three sequential BAL blocks (A+1, A+2, A+3). The first two touch
|
|
// goodAddr, the third touches corruptAddr so that block's apply fails
|
|
// once we've corrupted that account's snapshot.
|
|
blocks := make([]balBlock, 3)
|
|
for i := 0; i < 3; i++ {
|
|
blockNum := numA + uint64(i) + 1
|
|
target := goodAddr
|
|
if i == 2 {
|
|
target = corruptAddr
|
|
}
|
|
balance := uint256.NewInt(uint64(1000 * (i + 1)))
|
|
|
|
cb := bal.NewConstructionBlockAccessList()
|
|
cb.BalanceChange(0, target, balance)
|
|
var buf bytes.Buffer
|
|
if err := cb.EncodeRLP(&buf); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var b bal.BlockAccessList
|
|
if err := rlp.DecodeBytes(buf.Bytes(), &b); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
balHash := b.Hash()
|
|
header := &types.Header{
|
|
Number: new(big.Int).SetUint64(blockNum), Difficulty: common.Big0,
|
|
BaseFee: common.Big0, WithdrawalsHash: &emptyHash,
|
|
BlobGasUsed: &zero, ExcessBlobGas: &zero,
|
|
ParentBeaconRoot: &emptyHash, RequestsHash: &emptyHash,
|
|
BlockAccessListHash: &balHash,
|
|
}
|
|
rawdb.WriteHeader(db, header)
|
|
rawdb.WriteCanonicalHash(db, header.Hash(), blockNum)
|
|
blocks[i] = balBlock{header: header, bal: buf.Bytes()}
|
|
}
|
|
|
|
// First sync: complete sync to A so persisted state has pivot=A,
|
|
// flat state covers all accounts.
|
|
{
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
syncer := newSyncerV2(db, nodeScheme)
|
|
src := newTestPeerV2("seed", t, term)
|
|
src.accountTrie = sourceAccountTrie.Copy()
|
|
src.accountValues = elems
|
|
syncer.Register(src)
|
|
src.remote = syncer
|
|
if err := syncer.Sync(pivotA, cancel); err != nil {
|
|
t.Fatalf("seed sync failed: %v", err)
|
|
}
|
|
}
|
|
|
|
// Corrupt the flat-state snapshot for corruptAddr so applyAccessList will
|
|
// fail when block A+3's BAL touches it. types.FullAccount rejects this
|
|
// payload as undecodable.
|
|
rawdb.WriteAccountSnapshot(db, crypto.Keccak256Hash(corruptAddr[:]), []byte{0xff, 0xff, 0xff, 0xff})
|
|
|
|
// Second sync: target is A+3. catchUp should apply A+1 and A+2 (good
|
|
// account), persist after each, then fail on A+3 (corrupt account).
|
|
pivotB := blocks[2].header
|
|
balsByHash := map[common.Hash]rlp.RawValue{
|
|
blocks[0].header.Hash(): blocks[0].bal,
|
|
blocks[1].header.Hash(): blocks[1].bal,
|
|
blocks[2].header.Hash(): blocks[2].bal,
|
|
}
|
|
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
syncer := newSyncerV2(db, nodeScheme)
|
|
src := newTestPeerV2("catchup", t, term)
|
|
src.accountTrie = sourceAccountTrie.Copy()
|
|
src.accountValues = elems
|
|
src.accessLists = balsByHash
|
|
syncer.Register(src)
|
|
src.remote = syncer
|
|
|
|
if err := syncer.Sync(pivotB, cancel); err == nil {
|
|
t.Fatal("expected Sync to fail when applyAccessList hits corrupt flat state")
|
|
}
|
|
|
|
// Persisted pivot should now reflect the last successfully applied
|
|
// block (A+2). Without per-iteration saves, it would still be at A.
|
|
loader := newSyncerV2(db, nodeScheme)
|
|
loader.loadSyncStatus()
|
|
if loader.pivot == nil {
|
|
t.Fatal("expected persisted pivot to be set after partial catchUp")
|
|
}
|
|
wantHash := blocks[1].header.Hash()
|
|
if loader.pivot.Hash() != wantHash {
|
|
t.Errorf("persisted pivot mismatch after partial catchUp: got %v, want %v (block A+2)",
|
|
loader.pivot.Hash(), wantHash)
|
|
}
|
|
}
|
|
|
|
// TestCatchUpWindowed verifies that catch-up correctly rolls the flat state
|
|
// forward when the gap spans several windows. With catchUpWindow shrunk to 2,
|
|
// a 5-block gap is processed as three windows ([A+1,A+2], [A+3,A+4], [A+5]),
|
|
// exercising the window boundaries. Every block's BAL must be fetched and
|
|
// applied, and the final pivot must reach the target.
|
|
func TestCatchUpWindowed(t *testing.T) {
|
|
t.Parallel()
|
|
testCatchUpWindowed(t, rawdb.HashScheme)
|
|
testCatchUpWindowed(t, rawdb.PathScheme)
|
|
}
|
|
|
|
func testCatchUpWindowed(t *testing.T, scheme string) {
|
|
nodeScheme, sourceAccountTrie, elems, addrs := makeAccountTrieWithAddresses(100, scheme)
|
|
rootA := sourceAccountTrie.Hash()
|
|
numA := uint64(100)
|
|
|
|
targetAddr := addrs[0]
|
|
targetHash := crypto.Keccak256Hash(targetAddr[:])
|
|
|
|
db := rawdb.NewMemoryDatabase()
|
|
emptyHash := common.Hash{}
|
|
zero := uint64(0)
|
|
|
|
// Persist header + canonical hash for the base pivot A so reorg detection
|
|
// in Sync passes and catchUp (not reset) runs.
|
|
pivotA := &types.Header{
|
|
Number: new(big.Int).SetUint64(numA), Root: rootA, Difficulty: common.Big0,
|
|
BaseFee: common.Big0, WithdrawalsHash: &emptyHash,
|
|
BlobGasUsed: &zero, ExcessBlobGas: &zero,
|
|
ParentBeaconRoot: &emptyHash, RequestsHash: &emptyHash,
|
|
}
|
|
rawdb.WriteHeader(db, pivotA)
|
|
rawdb.WriteCanonicalHash(db, pivotA.Hash(), numA)
|
|
|
|
// Build a 5-block gap, each block bumping targetAddr's balance. The last
|
|
// block's balance is the expected final state.
|
|
const gap = 5
|
|
var (
|
|
lastHeader *types.Header
|
|
lastBalance *uint256.Int
|
|
balsByHash = make(map[common.Hash]rlp.RawValue, gap)
|
|
)
|
|
for i := 0; i < gap; i++ {
|
|
blockNum := numA + uint64(i) + 1
|
|
balance := uint256.NewInt(uint64(1000 * (i + 1)))
|
|
|
|
cb := bal.NewConstructionBlockAccessList()
|
|
cb.BalanceChange(0, targetAddr, balance)
|
|
var buf bytes.Buffer
|
|
if err := cb.EncodeRLP(&buf); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var b bal.BlockAccessList
|
|
if err := rlp.DecodeBytes(buf.Bytes(), &b); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
balHash := b.Hash()
|
|
header := &types.Header{
|
|
Number: new(big.Int).SetUint64(blockNum), Difficulty: common.Big0,
|
|
BaseFee: common.Big0, WithdrawalsHash: &emptyHash,
|
|
BlobGasUsed: &zero, ExcessBlobGas: &zero,
|
|
ParentBeaconRoot: &emptyHash, RequestsHash: &emptyHash,
|
|
BlockAccessListHash: &balHash,
|
|
}
|
|
rawdb.WriteHeader(db, header)
|
|
rawdb.WriteCanonicalHash(db, header.Hash(), blockNum)
|
|
balsByHash[header.Hash()] = buf.Bytes()
|
|
lastHeader, lastBalance = header, balance
|
|
}
|
|
|
|
// Seed sync to A: persisted state ends with pivot=A and full flat state.
|
|
{
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
syncer := newSyncerV2(db, nodeScheme)
|
|
src := newTestPeerV2("seed", t, term)
|
|
src.accountTrie = sourceAccountTrie.Copy()
|
|
src.accountValues = elems
|
|
syncer.Register(src)
|
|
src.remote = syncer
|
|
if err := syncer.Sync(pivotA, cancel); err != nil {
|
|
t.Fatalf("seed sync failed: %v", err)
|
|
}
|
|
}
|
|
|
|
// Run catch-up to A+5 directly with a window of 2, forcing three windows
|
|
// ([A+1,A+2], [A+3,A+4], [A+5]). Calling catchUp in isolation keeps the
|
|
// focus on the windowing logic without the surrounding download/trie-gen
|
|
// phases (which would need a real target state root).
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
syncer := newSyncerV2(db, nodeScheme)
|
|
syncer.loadSyncStatus() // restores pivot=A from the seed sync
|
|
if syncer.pivot == nil || syncer.pivot.Number.Uint64() != numA {
|
|
t.Fatalf("expected restored pivot at block %d, got %v", numA, syncer.pivot)
|
|
}
|
|
syncer.catchUpWindow = 2
|
|
src := newTestPeerV2("catchup", t, term)
|
|
src.accountTrie = sourceAccountTrie.Copy()
|
|
src.accountValues = elems
|
|
src.accessLists = balsByHash
|
|
syncer.Register(src)
|
|
src.remote = syncer
|
|
if err := syncer.catchUp(lastHeader, cancel); err != nil {
|
|
t.Fatalf("windowed catch-up failed: %v", err)
|
|
}
|
|
|
|
// The account must reflect the final block's balance, proving every window
|
|
// (including the trailing partial one) was fetched and applied in order.
|
|
data := rawdb.ReadAccountSnapshot(db, targetHash)
|
|
if len(data) == 0 {
|
|
t.Fatal("target account missing after windowed catch-up")
|
|
}
|
|
account, err := types.FullAccount(data)
|
|
if err != nil {
|
|
t.Fatalf("failed to decode account: %v", err)
|
|
}
|
|
if account.Balance.Cmp(lastBalance) != 0 {
|
|
t.Errorf("balance after windowed catch-up: got %v, want %v", account.Balance, lastBalance)
|
|
}
|
|
|
|
// The persisted pivot must have advanced all the way to the target.
|
|
loader := newSyncerV2(db, nodeScheme)
|
|
loader.loadSyncStatus()
|
|
if loader.pivot == nil || loader.pivot.Hash() != lastHeader.Hash() {
|
|
t.Errorf("persisted pivot did not reach target after windowed catch-up")
|
|
}
|
|
}
|
|
|
|
// TestSyncStatusMarkedCompleteAfterCompletion verifies that after a full sync
|
|
// completes, the persisted sync status reaches the complete phase. This lets a
|
|
// subsequent Sync call distinguish "already done" from "fresh node" and skip.
|
|
func TestSyncStatusMarkedCompleteAfterCompletion(t *testing.T) {
|
|
t.Parallel()
|
|
testSyncStatusMarkedCompleteAfterCompletion(t, rawdb.HashScheme)
|
|
testSyncStatusMarkedCompleteAfterCompletion(t, rawdb.PathScheme)
|
|
}
|
|
|
|
func testSyncStatusMarkedCompleteAfterCompletion(t *testing.T, scheme string) {
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, scheme)
|
|
|
|
mkSource := func(name string) *testPeerV2 {
|
|
source := newTestPeerV2(name, t, term)
|
|
source.accountTrie = sourceAccountTrie.Copy()
|
|
source.accountValues = elems
|
|
return source
|
|
}
|
|
syncer := setupSyncerV2(nodeScheme, mkSource("source"))
|
|
pivot := mkPivot(0, sourceAccountTrie.Hash())
|
|
if err := syncer.Sync(pivot, cancel); err != nil {
|
|
t.Fatalf("sync failed: %v", err)
|
|
}
|
|
|
|
// After successful sync, persisted status should be present with
|
|
// the complete phase and the pivot we synced to.
|
|
loader := newSyncerV2(syncer.db, nodeScheme)
|
|
loader.loadSyncStatus()
|
|
if loader.getPhase() != phaseComplete {
|
|
t.Fatal("expected persisted status to reach the complete phase after successful sync")
|
|
}
|
|
if loader.pivot == nil || loader.pivot.Hash() != pivot.Hash() {
|
|
t.Fatalf("expected persisted pivot to match synced pivot")
|
|
}
|
|
}
|
|
|
|
// TestSyncSkipsIfAlreadyComplete verifies that a follow-up Sync call for the
|
|
// same pivot returns immediately without doing any work, since the persisted
|
|
// status indicates the sync is already complete. To prove the skip path actually
|
|
// fires, we deliberately wipe the flat state between the two calls. If it skips,
|
|
// Sync returns nil without touching flat state. If it doesn't kip, GenerateTrie
|
|
// would run against an empty snapshot and fail with a root mismatch.
|
|
func TestSyncSkipsIfAlreadyComplete(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, rawdb.HashScheme)
|
|
pivot := mkPivot(0, sourceAccountTrie.Hash())
|
|
|
|
var (
|
|
once1 sync.Once
|
|
cancel1 = make(chan struct{})
|
|
term1 = func() { once1.Do(func() { close(cancel1) }) }
|
|
)
|
|
src1 := newTestPeerV2("source1", t, term1)
|
|
src1.accountTrie = sourceAccountTrie.Copy()
|
|
src1.accountValues = elems
|
|
syncer := setupSyncerV2(nodeScheme, src1)
|
|
if err := syncer.Sync(pivot, cancel1); err != nil {
|
|
t.Fatalf("first sync failed: %v", err)
|
|
}
|
|
|
|
// Wipe the flat state. The persisted status (in the complete phase) stays.
|
|
if err := syncer.db.DeleteRange(rawdb.SnapshotAccountPrefix, []byte{rawdb.SnapshotAccountPrefix[0] + 1}); err != nil {
|
|
t.Fatalf("failed to wipe account snapshot: %v", err)
|
|
}
|
|
if err := syncer.db.DeleteRange(rawdb.SnapshotStoragePrefix, []byte{rawdb.SnapshotStoragePrefix[0] + 1}); err != nil {
|
|
t.Fatalf("failed to wipe storage snapshot: %v", err)
|
|
}
|
|
|
|
// Second sync must take the skip path. If it didn't, the empty flat
|
|
// state would cause GenerateTrie to fail with a root mismatch.
|
|
cancel2 := make(chan struct{})
|
|
if err := syncer.Sync(pivot, cancel2); err != nil {
|
|
t.Fatalf("second sync should have skipped, got error: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestInterruptedGenerationRecovery verifies that if sync is interrupted after
|
|
// download completes but before trie generation finishes, the next Sync() call
|
|
// re-runs the download (which completes immediately) and generation.
|
|
func TestInterruptedGenerationRecovery(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, rawdb.HashScheme)
|
|
root := sourceAccountTrie.Hash()
|
|
|
|
// First run: complete download, save status, simulate interruption
|
|
// before generation by calling downloadState() directly
|
|
var (
|
|
once1 sync.Once
|
|
cancel1 = make(chan struct{})
|
|
term1 = func() { once1.Do(func() { close(cancel1) }) }
|
|
)
|
|
db := rawdb.NewMemoryDatabase()
|
|
syncer1 := newSyncerV2(db, nodeScheme)
|
|
src1 := newTestPeerV2("source1", t, term1)
|
|
src1.accountTrie = sourceAccountTrie.Copy()
|
|
src1.accountValues = elems
|
|
syncer1.Register(src1)
|
|
src1.remote = syncer1
|
|
pivot := mkPivot(0, root)
|
|
syncer1.loadSyncStatus()
|
|
syncer1.pivot = pivot // Sync pins this before downloadState
|
|
|
|
if err := syncer1.downloadState(cancel1); err != nil {
|
|
t.Fatalf("download failed: %v", err)
|
|
}
|
|
// Save status (simulating what Sync's defer does)
|
|
for _, task := range syncer1.tasks {
|
|
syncer1.forwardAccountTask(task)
|
|
}
|
|
syncer1.cleanAccountTasks()
|
|
syncer1.saveSyncStatus()
|
|
|
|
// Status should exist (generation hasn't run yet)
|
|
if rawdb.ReadSnapshotSyncStatus(db) == nil {
|
|
t.Fatal("sync status should exist after download")
|
|
}
|
|
// Second run: full Sync should detect tasks are done, run generation
|
|
var (
|
|
once2 sync.Once
|
|
cancel2 = make(chan struct{})
|
|
term2 = func() { once2.Do(func() { close(cancel2) }) }
|
|
)
|
|
syncer2 := newSyncerV2(db, nodeScheme)
|
|
src2 := newTestPeerV2("source2", t, term2)
|
|
src2.accountTrie = sourceAccountTrie.Copy()
|
|
src2.accountValues = elems
|
|
syncer2.Register(src2)
|
|
src2.remote = syncer2
|
|
|
|
if err := syncer2.Sync(mkPivot(0, root), cancel2); err != nil {
|
|
t.Fatalf("resumed sync failed: %v", err)
|
|
}
|
|
// The resumed run re-arms the pivot freeze once its no-op download
|
|
// completes, the downloader relies on it until the pivot block commits.
|
|
if syncer2.FrozenPivot() == nil {
|
|
t.Fatal("pivot not frozen after resumed sync")
|
|
}
|
|
// After generation completes, status should reach the complete phase.
|
|
loader := newSyncerV2(db, nodeScheme)
|
|
loader.loadSyncStatus()
|
|
if loader.getPhase() != phaseComplete {
|
|
t.Fatal("sync status should reach the complete phase after generation completes")
|
|
}
|
|
}
|
|
|
|
// TestPruneStaleState verifies that resuming a sync wipes exactly the flat
|
|
// state the journal does not cover: account and storage entries beyond the
|
|
// task cursors are removed, while everything below the cursors — and the
|
|
// journaled storage of completed-storage accounts and chunked large
|
|
// contracts inside the window — survives.
|
|
func TestPruneStaleState(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
db = rawdb.NewMemoryDatabase()
|
|
syncer = newSyncerV2(db, rawdb.HashScheme)
|
|
|
|
fetched = common.Hash{0x20} // below the cursor, journal-covered
|
|
next = common.Hash{0x40} // task cursor
|
|
stale = common.Hash{0x50} // beyond the cursor, not covered
|
|
completed = common.Hash{0x60} // beyond the cursor, storage journaled complete
|
|
chunked = common.Hash{0x80} // beyond the cursor, large contract, partially journaled
|
|
staleToo = common.Hash{0xa0} // beyond the cursor, not covered
|
|
|
|
subNext = common.Hash{0x50} // slot cursor of the chunked contract
|
|
slotLo = common.Hash{0x11} // below the slot cursor, journal-covered
|
|
slotHi = common.Hash{0x77} // beyond the slot cursor, not covered
|
|
|
|
val = []byte{0xde, 0xad}
|
|
)
|
|
syncer.tasks = []*accountTaskV2{{
|
|
Next: next,
|
|
Last: common.MaxHash,
|
|
SubTasks: map[common.Hash][]*storageTaskV2{
|
|
chunked: {{Next: subNext, Last: common.MaxHash}},
|
|
},
|
|
stateCompleted: map[common.Hash]struct{}{completed: {}},
|
|
}}
|
|
|
|
// Journal-covered state: account below the cursor with a storage slot,
|
|
// the completed account's storage, the chunked contract's low slots.
|
|
rawdb.WriteAccountSnapshot(db, fetched, val)
|
|
rawdb.WriteStorageSnapshot(db, fetched, slotLo, val)
|
|
rawdb.WriteStorageSnapshot(db, completed, slotLo, val)
|
|
rawdb.WriteStorageSnapshot(db, chunked, slotLo, val)
|
|
|
|
// Uncovered state, flushed after the last journal save: accounts beyond
|
|
// the cursor (including the completed-storage one, whose account entry
|
|
// is only legitimate below the cursor), their storage, and the chunked
|
|
// contract's slots beyond its slot cursor.
|
|
rawdb.WriteAccountSnapshot(db, stale, val)
|
|
rawdb.WriteAccountSnapshot(db, completed, val)
|
|
rawdb.WriteAccountSnapshot(db, staleToo, val)
|
|
rawdb.WriteStorageSnapshot(db, stale, slotLo, val)
|
|
rawdb.WriteStorageSnapshot(db, staleToo, slotHi, val)
|
|
rawdb.WriteStorageSnapshot(db, chunked, slotHi, val)
|
|
|
|
// Plant bare keys right at the start of the neighboring keyspaces. The
|
|
// task ranges end at the maximum hash, so a limit computed by bumping
|
|
// the full key would roll the carry over into these keyspaces and
|
|
// swallow such keys.
|
|
neighborOfAccounts := bytes.Clone(rawdb.SnapshotAccountPrefix)
|
|
neighborOfAccounts[len(neighborOfAccounts)-1]++
|
|
db.Put(neighborOfAccounts, val)
|
|
|
|
neighborOfStorage := bytes.Clone(rawdb.SnapshotStoragePrefix)
|
|
neighborOfStorage[len(neighborOfStorage)-1]++
|
|
db.Put(neighborOfStorage, val)
|
|
|
|
if err := syncer.pruneStaleState(); err != nil {
|
|
t.Fatalf("prune failed: %v", err)
|
|
}
|
|
for _, key := range [][]byte{neighborOfAccounts, neighborOfStorage} {
|
|
if has, _ := db.Has(key); !has {
|
|
t.Errorf("neighboring keyspace entry %x swallowed by the wipe", key)
|
|
}
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
account common.Hash
|
|
slot *common.Hash
|
|
want bool
|
|
desc string
|
|
}{
|
|
{fetched, nil, true, "account below cursor"},
|
|
{fetched, &slotLo, true, "storage below cursor"},
|
|
{completed, &slotLo, true, "storage of completed account"},
|
|
{chunked, &slotLo, true, "chunked contract slot below slot cursor"},
|
|
{stale, nil, false, "account beyond cursor"},
|
|
{completed, nil, false, "account entry of completed account"},
|
|
{staleToo, nil, false, "account beyond cursor (second)"},
|
|
{stale, &slotLo, false, "storage of unprotected account"},
|
|
{staleToo, &slotHi, false, "storage of unprotected account (second)"},
|
|
{chunked, &slotHi, false, "chunked contract slot beyond slot cursor"},
|
|
} {
|
|
var have bool
|
|
if tc.slot == nil {
|
|
have = len(rawdb.ReadAccountSnapshot(db, tc.account)) > 0
|
|
} else {
|
|
have = len(rawdb.ReadStorageSnapshot(db, tc.account, *tc.slot)) > 0
|
|
}
|
|
if have != tc.want {
|
|
t.Errorf("%s: present = %v, want %v", tc.desc, have, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestResumeWipesUncoveredState simulates an unclean shutdown mid-download:
|
|
// flat state flushed beyond the journaled cursor (which the journal therefore
|
|
// considers unfetched) must be wiped when the sync resumes — otherwise it
|
|
// would survive the re-download untouched and corrupt the generated trie.
|
|
func TestResumeWipesUncoveredState(t *testing.T) {
|
|
t.Parallel()
|
|
testResumeWipesUncoveredState(t, rawdb.HashScheme)
|
|
testResumeWipesUncoveredState(t, rawdb.PathScheme)
|
|
}
|
|
|
|
func testResumeWipesUncoveredState(t *testing.T, scheme string) {
|
|
nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, scheme)
|
|
root := sourceAccountTrie.Hash()
|
|
db := rawdb.NewMemoryDatabase()
|
|
pivot := mkPivot(0, root)
|
|
|
|
// First run: interrupt the download after a couple of responses, leaving
|
|
// a journal behind (saved on the graceful teardown).
|
|
var (
|
|
once1 sync.Once
|
|
cancel1 = make(chan struct{})
|
|
term1 = func() { once1.Do(func() { close(cancel1) }) }
|
|
responses atomic.Int32
|
|
)
|
|
syncer1 := newSyncerV2(db, nodeScheme)
|
|
src1 := newTestPeerV2("source1", t, term1)
|
|
src1.accountTrie = sourceAccountTrie.Copy()
|
|
src1.accountValues = elems
|
|
src1.accountRequestV2Handler = func(tp *testPeerV2, id uint64, root common.Hash, origin common.Hash, limit common.Hash, cap int) error {
|
|
if responses.Add(1) > 2 {
|
|
term1()
|
|
return nil
|
|
}
|
|
return defaultAccountRequestHandlerV2(tp, id, root, origin, limit, cap)
|
|
}
|
|
syncer1.Register(src1)
|
|
src1.remote = syncer1
|
|
syncer1.Sync(pivot, cancel1)
|
|
|
|
// Simulate the unclean shutdown: plant a bogus account that the journal
|
|
// does not cover (right beyond an unfinished task's cursor) — as if it
|
|
// was flushed after the journal was last saved. The account is not part
|
|
// of the source trie, so the re-download would never overwrite it and
|
|
// trie generation would fail the root check if it survived.
|
|
loader := newSyncerV2(db, nodeScheme)
|
|
loader.loadSyncStatus()
|
|
if len(loader.tasks) == 0 {
|
|
t.Fatal("expected unfinished tasks in the journal")
|
|
}
|
|
task := loader.tasks[len(loader.tasks)-1]
|
|
bogus := incHash(task.Next)
|
|
rawdb.WriteAccountSnapshot(db, bogus, types.SlimAccountRLP(types.StateAccount{
|
|
Nonce: 1, Balance: uint256.NewInt(1),
|
|
Root: types.EmptyRootHash, CodeHash: types.EmptyCodeHash[:],
|
|
}))
|
|
|
|
// Second run: the resume must prune the bogus entry and complete cleanly.
|
|
var (
|
|
once2 sync.Once
|
|
cancel2 = make(chan struct{})
|
|
term2 = func() { once2.Do(func() { close(cancel2) }) }
|
|
)
|
|
syncer2 := newSyncerV2(db, nodeScheme)
|
|
src2 := newTestPeerV2("source2", t, term2)
|
|
src2.accountTrie = sourceAccountTrie.Copy()
|
|
src2.accountValues = elems
|
|
syncer2.Register(src2)
|
|
src2.remote = syncer2
|
|
|
|
if err := syncer2.Sync(pivot, cancel2); err != nil {
|
|
t.Fatalf("resumed sync failed: %v", err)
|
|
}
|
|
if len(rawdb.ReadAccountSnapshot(db, bogus)) != 0 {
|
|
t.Fatal("uncovered account entry should have been pruned on resume")
|
|
}
|
|
verifyTrie(scheme, db, root, t)
|
|
}
|
|
|
|
// TestFetchAccessListsMultiplePeers verifies that fetch distributes work
|
|
// across multiple idle peers.
|
|
func TestFetchAccessListsMultiplePeers(t *testing.T) {
|
|
t.Parallel()
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
|
|
// Create enough BALs to potentially split across peers
|
|
var hashes []common.Hash
|
|
bals := make(map[common.Hash]rlp.RawValue)
|
|
for i := 0; i < 10; i++ {
|
|
h := common.HexToHash(fmt.Sprintf("0x%02x", i+1))
|
|
hashes = append(hashes, h)
|
|
cb := bal.NewConstructionBlockAccessList()
|
|
cb.BalanceChange(0, common.HexToAddress("0xaa"), uint256.NewInt(uint64(i)))
|
|
var buf bytes.Buffer
|
|
if err := cb.EncodeRLP(&buf); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bals[h] = buf.Bytes()
|
|
}
|
|
mkSource := func(name string) *testPeerV2 {
|
|
source := newTestPeerV2(name, t, term)
|
|
source.accessLists = bals
|
|
return source
|
|
}
|
|
syncer := setupSyncerV2(rawdb.HashScheme, mkSource("peer-a"), mkSource("peer-b"), mkSource("peer-c"))
|
|
results, err := syncer.fetchAccessLists(hashes, makeAccessListHeaders(bals), cancel)
|
|
if err != nil {
|
|
t.Fatalf("fetchAccessLists failed: %v", err)
|
|
}
|
|
if len(results) != len(hashes) {
|
|
t.Fatalf("result count mismatch: got %d, want %d", len(results), len(hashes))
|
|
}
|
|
// Verify results match expected content in request order
|
|
for i, h := range hashes {
|
|
if !bytes.Equal(results[i], bals[h]) {
|
|
t.Errorf("result %d content mismatch for hash %v", i, h)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestFetchAccessListsPeerTimeout verifies that timed-out requests are retried
|
|
// with a different peer.
|
|
func TestFetchAccessListsPeerTimeout(t *testing.T) {
|
|
t.Parallel()
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
hashes := []common.Hash{common.HexToHash("0x01")}
|
|
bals := make(map[common.Hash]rlp.RawValue)
|
|
cb := bal.NewConstructionBlockAccessList()
|
|
cb.BalanceChange(0, common.HexToAddress("0xaa"), uint256.NewInt(42))
|
|
var buf bytes.Buffer
|
|
if err := cb.EncodeRLP(&buf); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bals[hashes[0]] = buf.Bytes()
|
|
|
|
// First peer never responds
|
|
nonResponsive := newTestPeerV2("non-responsive", t, term)
|
|
nonResponsive.accessListRequestHandler = func(t *testPeerV2, id uint64, hashes []common.Hash, max int) error {
|
|
// Don't respond — let it time out
|
|
return nil
|
|
}
|
|
|
|
// Second peer serves correctly
|
|
good := newTestPeerV2("good", t, term)
|
|
good.accessLists = bals
|
|
syncer := setupSyncerV2(rawdb.HashScheme, nonResponsive, good)
|
|
syncer.rates.OverrideTTLLimit = time.Millisecond // Fast timeout
|
|
results, err := syncer.fetchAccessLists(hashes, makeAccessListHeaders(bals), cancel)
|
|
if err != nil {
|
|
t.Fatalf("fetchAccessLists failed: %v", err)
|
|
}
|
|
if len(results) != 1 {
|
|
t.Fatalf("result count mismatch: got %d, want 1", len(results))
|
|
}
|
|
}
|
|
|
|
// TestFetchAccessListsPeerRejection verifies that peers returning empty
|
|
// responses are marked stateless and work is retried with another peer.
|
|
func TestFetchAccessListsPeerRejection(t *testing.T) {
|
|
t.Parallel()
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
hashes := []common.Hash{common.HexToHash("0x01")}
|
|
bals := make(map[common.Hash]rlp.RawValue)
|
|
cb := bal.NewConstructionBlockAccessList()
|
|
cb.BalanceChange(0, common.HexToAddress("0xaa"), uint256.NewInt(42))
|
|
var buf bytes.Buffer
|
|
if err := cb.EncodeRLP(&buf); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bals[hashes[0]] = buf.Bytes()
|
|
|
|
// First peer rejects (has no BAL data, returns empty)
|
|
// accessLists is nil, so defaultAccessListRequestHandler returns empty
|
|
rejector := newTestPeerV2("rejector", t, term)
|
|
|
|
// Second peer serves correctly
|
|
good := newTestPeerV2("good", t, term)
|
|
good.accessLists = bals
|
|
syncer := setupSyncerV2(rawdb.HashScheme, rejector, good)
|
|
results, err := syncer.fetchAccessLists(hashes, makeAccessListHeaders(bals), cancel)
|
|
if err != nil {
|
|
t.Fatalf("fetchAccessLists failed: %v", err)
|
|
}
|
|
if len(results) != 1 {
|
|
t.Fatalf("result count mismatch: got %d, want 1", len(results))
|
|
}
|
|
}
|
|
|
|
// TestFetchAccessListsCancel verifies that fetchAccessLists returns promptly
|
|
// when cancelled.
|
|
func TestFetchAccessListsCancel(t *testing.T) {
|
|
t.Parallel()
|
|
cancel := make(chan struct{})
|
|
|
|
// Peer that never responds
|
|
nonResponsive := newTestPeerV2("non-responsive", t, func() {})
|
|
nonResponsive.accessListRequestHandler = func(t *testPeerV2, id uint64, hashes []common.Hash, max int) error {
|
|
return nil // never deliver
|
|
}
|
|
syncer := setupSyncerV2(rawdb.HashScheme, nonResponsive)
|
|
hashes := []common.Hash{common.HexToHash("0x01")}
|
|
|
|
// Cancel after a short delay
|
|
go func() {
|
|
time.Sleep(50 * time.Millisecond)
|
|
close(cancel)
|
|
}()
|
|
_, err := syncer.fetchAccessLists(hashes, nil, cancel)
|
|
if err != ErrCancelled {
|
|
t.Fatalf("expected ErrCancelled, got %v", err)
|
|
}
|
|
}
|
|
|
|
// TestFetchAccessListsPeerDrop verifies that dropping a peer mid-request
|
|
// causes the request to be retried with a different peer.
|
|
func TestFetchAccessListsPeerDrop(t *testing.T) {
|
|
t.Parallel()
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
hashes := []common.Hash{common.HexToHash("0x01")}
|
|
bals := make(map[common.Hash]rlp.RawValue)
|
|
cb := bal.NewConstructionBlockAccessList()
|
|
cb.BalanceChange(0, common.HexToAddress("0xaa"), uint256.NewInt(42))
|
|
var buf bytes.Buffer
|
|
if err := cb.EncodeRLP(&buf); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bals[hashes[0]] = buf.Bytes()
|
|
|
|
// First peer will be dropped mid-request
|
|
dropped := newTestPeerV2("dropped", t, term)
|
|
dropped.accessListRequestHandler = func(tp *testPeerV2, id uint64, hashes []common.Hash, max int) error {
|
|
// Simulate peer dropping by unregistering
|
|
tp.remote.Unregister(tp.id)
|
|
return nil
|
|
}
|
|
|
|
// Second peer serves correctly
|
|
good := newTestPeerV2("good", t, term)
|
|
good.accessLists = bals
|
|
syncer := setupSyncerV2(rawdb.HashScheme, dropped, good)
|
|
results, err := syncer.fetchAccessLists(hashes, makeAccessListHeaders(bals), cancel)
|
|
if err != nil {
|
|
t.Fatalf("fetchAccessLists failed: %v", err)
|
|
}
|
|
if len(results) != 1 {
|
|
t.Fatalf("result count mismatch: got %d, want 1", len(results))
|
|
}
|
|
}
|
|
|
|
// TestFetchAccessListsShortResponse verifies that when a peer returns fewer
|
|
// BALs than requested (a short/partial response), the un-served hashes are
|
|
// retried and eventually all results are collected.
|
|
func TestFetchAccessListsShortResponse(t *testing.T) {
|
|
t.Parallel()
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
|
|
// Request 4 hashes but the peer only returns the first 2.
|
|
hashes := []common.Hash{
|
|
common.HexToHash("0x01"),
|
|
common.HexToHash("0x02"),
|
|
common.HexToHash("0x03"),
|
|
common.HexToHash("0x04"),
|
|
}
|
|
allBALs := make(map[common.Hash]rlp.RawValue)
|
|
for _, h := range hashes {
|
|
cb := bal.NewConstructionBlockAccessList()
|
|
cb.BalanceChange(0, common.HexToAddress("0xaa"), uint256.NewInt(uint64(h[31])))
|
|
var buf bytes.Buffer
|
|
if err := cb.EncodeRLP(&buf); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
allBALs[h] = buf.Bytes()
|
|
}
|
|
|
|
// shortPeer returns only the first 2 BALs regardless of how many are
|
|
// requested. This simulates a peer that truncates its response (e.g.,
|
|
// hitting the 2 MiB response soft limit).
|
|
shortPeer := newTestPeerV2("short", t, term)
|
|
shortPeer.accessListRequestHandler = func(tp *testPeerV2, id uint64, reqHashes []common.Hash, max int) error {
|
|
// Return only the first 2 of however many were requested.
|
|
limit := 2
|
|
if len(reqHashes) < limit {
|
|
limit = len(reqHashes)
|
|
}
|
|
var results []rlp.RawValue
|
|
for i := 0; i < limit; i++ {
|
|
results = append(results, allBALs[reqHashes[i]])
|
|
}
|
|
rawList, _ := rlp.EncodeToRawList(results)
|
|
if err := tp.remote.OnAccessLists(tp, id, rawList); err != nil {
|
|
tp.test.Errorf("delivery rejected: %v", err)
|
|
tp.term()
|
|
}
|
|
return nil
|
|
}
|
|
syncer := setupSyncerV2(rawdb.HashScheme, shortPeer)
|
|
|
|
// Pre-seed the rate tracker so the peer's capacity for AccessListsMsg is
|
|
// high enough to get all 4 hashes assigned in a single request. Without
|
|
// this, the default capacity is 1, so the peer would only get 1 hash per
|
|
// round and the short-response scenario never triggers.
|
|
syncer.rates.Update(shortPeer.id, AccessListsMsg, time.Millisecond, 100)
|
|
|
|
// If the bug exists, this will hang.
|
|
done := make(chan struct{})
|
|
var (
|
|
results []rlp.RawValue
|
|
fetchErr error
|
|
)
|
|
go func() {
|
|
results, fetchErr = syncer.fetchAccessLists(hashes, makeAccessListHeaders(allBALs), cancel)
|
|
close(done)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
// fetchAccessLists returned
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("fetchAccessLists has hung. This means unserved hashes were never re-added to pending.")
|
|
}
|
|
if fetchErr != nil {
|
|
t.Fatalf("fetchAccessLists failed: %v", fetchErr)
|
|
}
|
|
if len(results) != len(hashes) {
|
|
t.Fatalf("result count mismatch: got %d, want %d", len(results), len(hashes))
|
|
}
|
|
|
|
// Verify all results are non-nil and in correct order
|
|
for i, h := range hashes {
|
|
if results[i] == nil {
|
|
t.Errorf("result %d (hash %v) is nil", i, h)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestFetchAccessListsEmptyPlaceholder verifies that when a peer returns
|
|
// rlp.EmptyString placeholders for BALs it doesn't have, those placeholders
|
|
// are not silently accepted as valid results.
|
|
func TestFetchAccessListsEmptyPlaceholder(t *testing.T) {
|
|
t.Parallel()
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
hashes := []common.Hash{
|
|
common.HexToHash("0x01"),
|
|
common.HexToHash("0x02"),
|
|
common.HexToHash("0x03"),
|
|
}
|
|
|
|
// Build BALs for all 3 hashes
|
|
allBALs := make(map[common.Hash]rlp.RawValue)
|
|
for _, h := range hashes {
|
|
cb := bal.NewConstructionBlockAccessList()
|
|
cb.BalanceChange(0, common.HexToAddress("0xaa"), uint256.NewInt(uint64(h[31])))
|
|
var buf bytes.Buffer
|
|
if err := cb.EncodeRLP(&buf); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
allBALs[h] = buf.Bytes()
|
|
}
|
|
|
|
// partialPeer has BALs for hashes 0 and 2. The server
|
|
// handler returns rlp.EmptyString for the missing BAL.
|
|
partialPeer := newTestPeerV2("partial", t, term)
|
|
partialPeer.accessListRequestHandler = func(tp *testPeerV2, id uint64, reqHashes []common.Hash, max int) error {
|
|
var results []rlp.RawValue
|
|
for _, h := range reqHashes {
|
|
if raw, ok := allBALs[h]; ok && h != hashes[1] {
|
|
results = append(results, raw)
|
|
} else {
|
|
results = append(results, rlp.EmptyString)
|
|
}
|
|
}
|
|
rawList, _ := rlp.EncodeToRawList(results)
|
|
if err := tp.remote.OnAccessLists(tp, id, rawList); err != nil {
|
|
tp.test.Errorf("delivery rejected: %v", err)
|
|
tp.term()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// fullPeer has all BALs
|
|
fullPeer := newTestPeerV2("full", t, term)
|
|
fullPeer.accessLists = allBALs
|
|
syncer := setupSyncerV2(rawdb.HashScheme, partialPeer, fullPeer)
|
|
|
|
// Pre-seed capacity so partialPeer gets all 3 hashes
|
|
syncer.rates.Update(partialPeer.id, AccessListsMsg, time.Millisecond, 100)
|
|
done := make(chan struct{})
|
|
var (
|
|
results []rlp.RawValue
|
|
fetchErr error
|
|
)
|
|
go func() {
|
|
results, fetchErr = syncer.fetchAccessLists(hashes, makeAccessListHeaders(allBALs), cancel)
|
|
close(done)
|
|
}()
|
|
|
|
// Wait for fetch to complete
|
|
select {
|
|
case <-done:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("fetchAccessLists hung")
|
|
}
|
|
if fetchErr != nil {
|
|
t.Fatalf("fetchAccessLists failed: %v", fetchErr)
|
|
}
|
|
|
|
// Verify the results are valid.
|
|
for i, raw := range results {
|
|
var accessList bal.BlockAccessList
|
|
if err := rlp.DecodeBytes(raw, &accessList); err != nil {
|
|
t.Errorf("result %d (hash %v) is not a valid BAL: %v (got raw bytes %x)",
|
|
i, hashes[i], err, raw)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestFetchAccessListsRejectsBadBAL verifies that when a peer delivers a BAL
|
|
// whose hash doesn't match the canonical block header, fetchAccessLists marks
|
|
// the peer stateless, drops the response, and surfaces the exhaustion error
|
|
// once no other peers can serve the work.
|
|
func TestFetchAccessListsRejectsBadBAL(t *testing.T) {
|
|
t.Parallel()
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
hash := common.HexToHash("0x01")
|
|
hashes := []common.Hash{hash}
|
|
|
|
// Build a BAL we'll actually serve.
|
|
cb := bal.NewConstructionBlockAccessList()
|
|
cb.BalanceChange(0, common.HexToAddress("0xaa"), uint256.NewInt(42))
|
|
var buf bytes.Buffer
|
|
if err := cb.EncodeRLP(&buf); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
served := buf.Bytes()
|
|
|
|
// Build a header whose BlockAccessListHash points at something else, so
|
|
// the served BAL fails verification.
|
|
mismatch := common.HexToHash("0xdeadbeef")
|
|
headers := map[common.Hash]*types.Header{
|
|
hash: {BlockAccessListHash: &mismatch},
|
|
}
|
|
|
|
peer := newTestPeerV2("liar", t, term)
|
|
peer.accessLists = map[common.Hash]rlp.RawValue{hash: served}
|
|
syncer := setupSyncerV2(rawdb.HashScheme, peer)
|
|
|
|
results, err := syncer.fetchAccessLists(hashes, headers, cancel)
|
|
if !errors.Is(err, errAccessListPeersExhausted) {
|
|
t.Fatalf("expected errAccessListPeersExhausted, got %v", err)
|
|
}
|
|
if results != nil {
|
|
t.Errorf("expected nil results on error, got %v", results)
|
|
}
|
|
syncer.lock.RLock()
|
|
_, stateless := syncer.statelessPeers[peer.id]
|
|
syncer.lock.RUnlock()
|
|
if !stateless {
|
|
t.Error("expected liar peer to be marked stateless after bad BAL")
|
|
}
|
|
}
|
|
|
|
// TestCatchUpRetriesOnBadBAL verifies that when one peer serves a BAL that
|
|
// fails verification but another serves a valid one, fetchAccessLists routes
|
|
// the work around the bad peer and returns the verified BAL.
|
|
func TestCatchUpRetriesOnBadBAL(t *testing.T) {
|
|
t.Parallel()
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
hash := common.HexToHash("0x01")
|
|
hashes := []common.Hash{hash}
|
|
|
|
cb := bal.NewConstructionBlockAccessList()
|
|
cb.BalanceChange(0, common.HexToAddress("0xaa"), uint256.NewInt(42))
|
|
var buf bytes.Buffer
|
|
if err := cb.EncodeRLP(&buf); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
good := buf.Bytes()
|
|
|
|
// A second BAL with different content used as the "bad" payload. It
|
|
// decodes cleanly but its hash will not match the header.
|
|
other := bal.NewConstructionBlockAccessList()
|
|
other.BalanceChange(0, common.HexToAddress("0xbb"), uint256.NewInt(99))
|
|
var otherBuf bytes.Buffer
|
|
if err := other.EncodeRLP(&otherBuf); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bad := otherBuf.Bytes()
|
|
|
|
headers := makeAccessListHeaders(map[common.Hash]rlp.RawValue{hash: good})
|
|
|
|
liar := newTestPeerV2("liar", t, term)
|
|
liar.accessLists = map[common.Hash]rlp.RawValue{hash: bad}
|
|
honest := newTestPeerV2("honest", t, term)
|
|
honest.accessLists = map[common.Hash]rlp.RawValue{hash: good}
|
|
|
|
syncer := setupSyncerV2(rawdb.HashScheme, liar, honest)
|
|
// Bias the capacity sort so the liar is asked first, exercising the
|
|
// reject-and-retry path rather than getting lucky on assignment order.
|
|
syncer.rates.Update(liar.id, AccessListsMsg, time.Millisecond, 1000)
|
|
|
|
results, err := syncer.fetchAccessLists(hashes, headers, cancel)
|
|
if err != nil {
|
|
t.Fatalf("fetchAccessLists failed: %v", err)
|
|
}
|
|
if !bytes.Equal(results[0], good) {
|
|
t.Errorf("expected the honest BAL, got %x", results[0])
|
|
}
|
|
syncer.lock.RLock()
|
|
_, liarStateless := syncer.statelessPeers[liar.id]
|
|
_, honestStateless := syncer.statelessPeers[honest.id]
|
|
syncer.lock.RUnlock()
|
|
if !liarStateless {
|
|
t.Error("expected liar to be marked stateless")
|
|
}
|
|
if honestStateless {
|
|
t.Error("expected honest peer to remain in good standing")
|
|
}
|
|
}
|
|
|
|
// makeStorageTrieFromSlots builds a storage trie for owner from raw slot
|
|
// key->value pairs, using the exact on-disk encoding the flat snapshot and the
|
|
// trie generation expect: each leaf is keyed by keccak256(slotKey) and its value is
|
|
// rlp(TrimLeftZeroes(value)). Zero-valued slots are skipped (an unset slot has
|
|
// no leaf). It returns the storage root, the dirty node set, and the sorted
|
|
// snapshot leaves (which a test peer serves verbatim).
|
|
func makeStorageTrieFromSlots(db *triedb.Database, owner common.Hash, slots map[common.Hash]common.Hash) (common.Hash, *trienode.NodeSet, []*kv) {
|
|
st, _ := trie.New(trie.StorageTrieID(types.EmptyRootHash, owner, types.EmptyRootHash), db)
|
|
var entries []*kv
|
|
for rawKey, value := range slots {
|
|
if value == (common.Hash{}) {
|
|
continue // unset slot: no leaf
|
|
}
|
|
slotHash := crypto.Keccak256Hash(rawKey[:])
|
|
enc, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(value[:]))
|
|
st.MustUpdate(slotHash[:], enc)
|
|
entries = append(entries, &kv{slotHash[:], enc})
|
|
}
|
|
slices.SortFunc(entries, (*kv).cmp)
|
|
root, nodes := st.Commit(false)
|
|
return root, nodes, entries
|
|
}
|
|
|
|
// makeStateWithStorageContract builds an account trie holding the given
|
|
// storage-less accounts plus a single contract account whose storage trie is
|
|
// built from slots. Everything is committed into a fresh triedb so the tries
|
|
// can be served by a test peer. It returns the recreated account trie, the
|
|
// sorted account leaves, the recreated contract storage trie, the sorted
|
|
// storage leaves, and the resulting state root.
|
|
func makeStateWithStorageContract(scheme string, plain []*kv, contractAddr common.Address, contract types.StateAccount, slots map[common.Hash]common.Hash) (*trie.Trie, []*kv, *trie.Trie, []*kv, common.Hash) {
|
|
db := triedb.NewDatabase(rawdb.NewMemoryDatabase(), newDbConfig(scheme))
|
|
accTrie := trie.NewEmpty(db)
|
|
merged := trienode.NewMergedNodeSet()
|
|
|
|
// Contract storage trie.
|
|
contractHash := crypto.Keccak256Hash(contractAddr[:])
|
|
stRoot, stNodes, stEntries := makeStorageTrieFromSlots(db, contractHash, slots)
|
|
if stNodes != nil {
|
|
merged.Merge(stNodes)
|
|
}
|
|
|
|
// Contract account leaf carries the (live) storage root.
|
|
contract.Root = stRoot
|
|
cval, _ := rlp.EncodeToBytes(&contract)
|
|
accTrie.MustUpdate(contractHash[:], cval)
|
|
accEntries := []*kv{{contractHash[:], cval}}
|
|
|
|
// Storage-less filler accounts.
|
|
for _, e := range plain {
|
|
accTrie.MustUpdate(e.k, e.v)
|
|
accEntries = append(accEntries, &kv{e.k, e.v})
|
|
}
|
|
slices.SortFunc(accEntries, (*kv).cmp)
|
|
|
|
// Commit account + storage nodes together, then re-open for serving.
|
|
root, set := accTrie.Commit(true)
|
|
merged.Merge(set)
|
|
db.Update(root, types.EmptyRootHash, 0, merged, triedb.NewStateSet())
|
|
|
|
accTrie, _ = trie.New(trie.StateTrieID(root), db)
|
|
stTrie, _ := trie.New(trie.StorageTrieID(root, contractHash, stRoot), db)
|
|
return accTrie, accEntries, stTrie, stEntries, root
|
|
}
|
|
|
|
// TestCatchUpAppliesStorageBALs exercises the snap/2 catch-up path with a BAL
|
|
// that mutates storage slots (not just balances): a non-zero write to a fresh
|
|
// slot, an overwrite of an existing slot, a write of zero (deletion), and a
|
|
// multi-tx write where the post-block value wins.
|
|
//
|
|
// It fully syncs pivot A (flat-state download + trie generation), then moves the
|
|
// pivot to A+1. The move triggers catchUp, which fetches the A+1 BAL, applies
|
|
// the storage diffs to the flat state, and generates the trie. The generation
|
|
// verifies the recomputed root against the pivot's expected post-catch-up root,
|
|
// so a successful Sync proves the storage mutations were applied in the exact
|
|
// encoding the trie generation consumes. verifyTrie re-walks the result as an
|
|
// independent confirmation.
|
|
func TestCatchUpAppliesStorageBALs(t *testing.T) {
|
|
t.Parallel()
|
|
testCatchUpAppliesStorageBALs(t, rawdb.HashScheme)
|
|
testCatchUpAppliesStorageBALs(t, rawdb.PathScheme)
|
|
}
|
|
|
|
func testCatchUpAppliesStorageBALs(t *testing.T, scheme string) {
|
|
// The contract whose storage the A+1 BAL mutates.
|
|
contractAddr := common.HexToAddress("0x00000000000000000000000000000000c0ffee01")
|
|
contractHash := crypto.Keccak256Hash(contractAddr[:])
|
|
|
|
// Raw storage slot keys.
|
|
var (
|
|
slotKeep = common.HexToHash("0x01") // untouched by the BAL
|
|
slotOver = common.HexToHash("0x02") // overwritten with a new non-zero value
|
|
slotZero = common.HexToHash("0x03") // written to zero (deletion)
|
|
slotNew = common.HexToHash("0x04") // unset in A, written non-zero in A+1
|
|
slotMultiTx = common.HexToHash("0x05") // written several times within the block
|
|
)
|
|
// Slot values. Multi-byte values force RLP length prefixes, so the encoding
|
|
// differs sharply from the raw 32-byte form and a format mismatch surfaces.
|
|
var (
|
|
vKeep = common.HexToHash("0x1111")
|
|
vOver0 = common.HexToHash("0x2222")
|
|
vOver1 = common.HexToHash("0x22220000aaaa")
|
|
vZero0 = common.HexToHash("0x3333")
|
|
vNew = common.HexToHash("0x4444")
|
|
vMulti0 = common.HexToHash("0x5555")
|
|
vMultiMid = common.HexToHash("0x5556")
|
|
vMultiFinal = common.HexToHash("0x55570000bbbb")
|
|
)
|
|
// Storage at pivot A.
|
|
slotsA := map[common.Hash]common.Hash{
|
|
slotKeep: vKeep,
|
|
slotOver: vOver0,
|
|
slotZero: vZero0,
|
|
slotMultiTx: vMulti0,
|
|
}
|
|
// Expected storage at pivot A+1 after applying the BAL writes below.
|
|
slotsB := map[common.Hash]common.Hash{
|
|
slotKeep: vKeep, // unchanged
|
|
slotOver: vOver1, // overwritten
|
|
slotNew: vNew, // newly written
|
|
slotMultiTx: vMultiFinal, // post-block (highest-tx) value wins
|
|
// slotZero deleted
|
|
}
|
|
contractTmpl := types.StateAccount{
|
|
Nonce: 7,
|
|
Balance: uint256.NewInt(123456),
|
|
CodeHash: types.EmptyCodeHash[:],
|
|
}
|
|
|
|
// Storage-less filler accounts, identical in A and A+1.
|
|
_, _, plain, _ := makeAccountTrieWithAddresses(20, scheme)
|
|
|
|
// Build the state at pivot A (served by the seed peer) and the expected
|
|
// state at pivot A+1 (only its root is needed).
|
|
accTrieA, accElemsA, stTrieA, stElemsA, rootA := makeStateWithStorageContract(scheme, plain, contractAddr, contractTmpl, slotsA)
|
|
_, _, _, _, rootB := makeStateWithStorageContract(scheme, plain, contractAddr, contractTmpl, slotsB)
|
|
if rootA == rootB {
|
|
t.Fatal("test bug: pivot A and A+1 must have different state roots")
|
|
}
|
|
|
|
// Build the A+1 BAL describing the storage mutations.
|
|
cb := bal.NewConstructionBlockAccessList()
|
|
cb.StorageWrite(0, contractAddr, slotOver, vOver1) // overwrite
|
|
cb.StorageWrite(0, contractAddr, slotZero, common.Hash{}) // write zero -> delete
|
|
cb.StorageWrite(0, contractAddr, slotNew, vNew) // new non-zero
|
|
cb.StorageWrite(0, contractAddr, slotMultiTx, vMultiMid) // tx 0
|
|
cb.StorageWrite(2, contractAddr, slotMultiTx, vMultiFinal) // tx 2 (post-block)
|
|
var balBuf bytes.Buffer
|
|
if err := cb.EncodeRLP(&balBuf); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var decodedBAL bal.BlockAccessList
|
|
if err := rlp.DecodeBytes(balBuf.Bytes(), &decodedBAL); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
balHash := decodedBAL.Hash()
|
|
|
|
// Chain headers. The pivot-A header is the same object passed to the first
|
|
// Sync, so the follow-up Sync's reorg check sees A as still-canonical and
|
|
// runs catchUp instead of resetting. The A+1 header carries the BAL hash
|
|
// (verified during catch-up) and the expected post-catch-up state root
|
|
// (verified by the trie generation).
|
|
db := rawdb.NewMemoryDatabase()
|
|
numA := uint64(128)
|
|
emptyH := common.Hash{}
|
|
zero := uint64(0)
|
|
hdrA := &types.Header{
|
|
Number: new(big.Int).SetUint64(numA), Root: rootA, Difficulty: common.Big0,
|
|
BaseFee: common.Big0, WithdrawalsHash: &emptyH,
|
|
BlobGasUsed: &zero, ExcessBlobGas: &zero,
|
|
ParentBeaconRoot: &emptyH, RequestsHash: &emptyH,
|
|
}
|
|
rawdb.WriteHeader(db, hdrA)
|
|
rawdb.WriteCanonicalHash(db, hdrA.Hash(), numA)
|
|
|
|
hdrB := &types.Header{
|
|
Number: new(big.Int).SetUint64(numA + 1), Root: rootB, Difficulty: common.Big0,
|
|
BaseFee: common.Big0, WithdrawalsHash: &emptyH,
|
|
BlobGasUsed: &zero, ExcessBlobGas: &zero,
|
|
ParentBeaconRoot: &emptyH, RequestsHash: &emptyH,
|
|
BlockAccessListHash: &balHash,
|
|
}
|
|
rawdb.WriteHeader(db, hdrB)
|
|
rawdb.WriteCanonicalHash(db, hdrB.Hash(), numA+1)
|
|
|
|
// Sync 1: full flat-state download + trie generation against pivot A.
|
|
{
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
syncer := newSyncerV2(db, scheme)
|
|
src := newTestPeerV2("seed", t, term)
|
|
src.accountTrie = accTrieA.Copy()
|
|
src.accountValues = accElemsA
|
|
src.setStorageTries(map[common.Hash]*trie.Trie{contractHash: stTrieA})
|
|
src.storageValues = map[common.Hash][]*kv{contractHash: stElemsA}
|
|
syncer.Register(src)
|
|
src.remote = syncer
|
|
done := checkStall(t, term)
|
|
if err := syncer.Sync(hdrA, cancel); err != nil {
|
|
t.Fatalf("pivot A sync failed: %v", err)
|
|
}
|
|
close(done)
|
|
}
|
|
// Sanity: the generated trie for pivot A is complete and matches rootA. This
|
|
// also confirms the test fixture itself is internally consistent.
|
|
verifyTrie(scheme, db, rootA, t)
|
|
|
|
// Sync 2: the pivot moves to A+1, exercising the BAL catch-up path.
|
|
{
|
|
var (
|
|
once sync.Once
|
|
cancel = make(chan struct{})
|
|
term = func() { once.Do(func() { close(cancel) }) }
|
|
)
|
|
syncer := newSyncerV2(db, scheme)
|
|
src := newTestPeerV2("catchup", t, term)
|
|
// Pivot A is fully synced, so no download tasks remain; the peer only
|
|
// needs to serve the A+1 BAL. The trie data is provided defensively in
|
|
// case a stray account request is issued.
|
|
src.accountTrie = accTrieA.Copy()
|
|
src.accountValues = accElemsA
|
|
src.accessLists = map[common.Hash]rlp.RawValue{hdrB.Hash(): balBuf.Bytes()}
|
|
syncer.Register(src)
|
|
src.remote = syncer
|
|
done := checkStall(t, term)
|
|
if err := syncer.Sync(hdrB, cancel); err != nil {
|
|
t.Fatalf("pivot A+1 catch-up sync failed: %v", err)
|
|
}
|
|
// The freeze must re-arm on a pivot-moved cycle too, the downloader
|
|
// relies on it from download completion until commit, and it must
|
|
// point at the new pivot the catch-up rolled forward to.
|
|
if frozen := syncer.FrozenPivot(); frozen == nil || frozen.Hash() != hdrB.Hash() {
|
|
t.Fatal("pivot not frozen at the new header after catch-up sync")
|
|
}
|
|
close(done)
|
|
}
|
|
|
|
// A successful Sync already means GenerateTrie reproduced rootB from the
|
|
// BAL-updated flat state (it errors on root mismatch). Re-walk the trie as
|
|
// an independent confirmation that rootB is fully materialized.
|
|
verifyTrie(scheme, db, rootB, t)
|
|
|
|
// Spot-check each storage mutation landed in the flat snapshot in the
|
|
// canonical encoding.
|
|
checkSlot := func(raw common.Hash, want common.Hash, present bool) {
|
|
t.Helper()
|
|
got := rawdb.ReadStorageSnapshot(db, contractHash, crypto.Keccak256Hash(raw[:]))
|
|
if !present {
|
|
if len(got) != 0 {
|
|
t.Errorf("slot %x: expected deletion, got %x", raw, got)
|
|
}
|
|
return
|
|
}
|
|
wantEnc, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(want[:]))
|
|
if !bytes.Equal(got, wantEnc) {
|
|
t.Errorf("slot %x: got %x, want %x", raw, got, wantEnc)
|
|
}
|
|
}
|
|
checkSlot(slotKeep, vKeep, true)
|
|
checkSlot(slotOver, vOver1, true)
|
|
checkSlot(slotZero, common.Hash{}, false)
|
|
checkSlot(slotNew, vNew, true)
|
|
checkSlot(slotMultiTx, vMultiFinal, true)
|
|
}
|