mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-17 01:13:45 +00:00
eth/protocols/snap: add tests and comments (#35477)
This PR updates some descriptions of snap sync v2, attaching two unit tests.
This commit is contained in:
parent
255842b750
commit
42c5059b58
3 changed files with 391 additions and 35 deletions
|
|
@ -94,6 +94,12 @@ func (s *syncerV2) isStorageFetched(accountHash, storageHash common.Hash) bool {
|
||||||
// in the database. For each account, it applies the post-block values (highest
|
// in the database. For each account, it applies the post-block values (highest
|
||||||
// TxIdx entry) for balance, nonce, code, and storage. The storageRoot field is
|
// TxIdx entry) for balance, nonce, code, and storage. The storageRoot field is
|
||||||
// intentionally left stale. It will be recomputed during the trie generation.
|
// intentionally left stale. It will be recomputed during the trie generation.
|
||||||
|
//
|
||||||
|
// Correctness rests on the access list enumerating every storage change as an
|
||||||
|
// individual slot write. This holds post EIP-6780: pre-existing contracts can
|
||||||
|
// no longer be destructed, so storage only changes via SSTOREs (all recorded),
|
||||||
|
// Networks with the legacy SELFDESTRUCT break this premise: wholesale storage
|
||||||
|
// wipes carry no per-slot writes, leaving already-downloaded slots stale.
|
||||||
func (s *syncerV2) applyAccessList(b *bal.BlockAccessList, batch ethdb.Batch) error {
|
func (s *syncerV2) applyAccessList(b *bal.BlockAccessList, batch ethdb.Batch) error {
|
||||||
// Iterate over all accounts in the access list
|
// Iterate over all accounts in the access list
|
||||||
for _, access := range *b {
|
for _, access := range *b {
|
||||||
|
|
@ -166,8 +172,7 @@ func (s *syncerV2) applyAccessList(b *bal.BlockAccessList, batch ethdb.Batch) er
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't create empty accounts in flat state (EIP-161).
|
// Don't create empty accounts in flat state (EIP-161).
|
||||||
isEmpty := account.Balance.IsZero() && account.Nonce == 0 &&
|
isEmpty := account.Balance.IsZero() && account.Nonce == 0 && bytes.Equal(account.CodeHash, types.EmptyCodeHash[:])
|
||||||
bytes.Equal(account.CodeHash, types.EmptyCodeHash[:])
|
|
||||||
switch {
|
switch {
|
||||||
case isEmpty && isNew:
|
case isEmpty && isNew:
|
||||||
// This covers cases where an account is created and destroyed within the
|
// This covers cases where an account is created and destroyed within the
|
||||||
|
|
|
||||||
|
|
@ -368,6 +368,14 @@ type SyncPeerV2 interface {
|
||||||
// - The peer remains connected, but does not deliver a response in time
|
// - The peer remains connected, but does not deliver a response in time
|
||||||
// - The peer delivers a stale response after a previous timeout
|
// - The peer delivers a stale response after a previous timeout
|
||||||
// - The peer delivers a refusal to serve the requested state
|
// - The peer delivers a refusal to serve the requested state
|
||||||
|
//
|
||||||
|
// The whole design is premised on EIP-6780 (self-destruct disabled): already
|
||||||
|
// downloaded state is never refetched, it is only rolled forward by applying
|
||||||
|
// BAL diffs, so every state change must surface in the access lists. A legacy
|
||||||
|
// SELFDESTRUCT wipes an entire storage trie without per-slot writes, which the
|
||||||
|
// BALs cannot express; the wiped slots would survive in the flat state and
|
||||||
|
// break the sync's core assumption. Networks with self-destruct enabled are
|
||||||
|
// therefore not supported.
|
||||||
type syncerV2 struct {
|
type syncerV2 struct {
|
||||||
db ethdb.Database // Database to store the trie nodes into (and dedup)
|
db ethdb.Database // Database to store the trie nodes into (and dedup)
|
||||||
scheme string // Node scheme used in node database
|
scheme string // Node scheme used in node database
|
||||||
|
|
@ -376,20 +384,22 @@ type syncerV2 struct {
|
||||||
tasks []*accountTaskV2 // Current account task set being synced
|
tasks []*accountTaskV2 // Current account task set being synced
|
||||||
update chan struct{} // Notification channel for possible sync progression
|
update chan struct{} // Notification channel for possible sync progression
|
||||||
|
|
||||||
peers map[string]SyncPeerV2 // Currently active peers to download from
|
peerJoin *event.Feed // Event feed to react to peers joining
|
||||||
peerJoin *event.Feed // Event feed to react to peers joining
|
peerDrop *event.Feed // Event feed to react to peers dropping
|
||||||
peerDrop *event.Feed // Event feed to react to peers dropping
|
rates *msgrate.Trackers // Message throughput rates for peers
|
||||||
rates *msgrate.Trackers // Message throughput rates for peers
|
|
||||||
|
// Peer tracking during syncing phase.
|
||||||
|
//
|
||||||
|
// These fields should be protected by lock.
|
||||||
|
peers map[string]SyncPeerV2 // Currently active peers to download from
|
||||||
|
statelessPeers map[string]struct{} // Peers that failed to deliver state data
|
||||||
|
accountIdlers map[string]struct{} // Peers that aren't serving account requests
|
||||||
|
bytecodeIdlers map[string]struct{} // Peers that aren't serving bytecode requests
|
||||||
|
storageIdlers map[string]struct{} // Peers that aren't serving storage requests
|
||||||
|
accessListIdlers map[string]struct{} // Peers that aren't serving BAL requests
|
||||||
|
|
||||||
// Request tracking during syncing phase.
|
// Request tracking during syncing phase.
|
||||||
//
|
//
|
||||||
// These fields should be protected by lock.
|
|
||||||
statelessPeers map[string]struct{} // Peers that failed to deliver state data
|
|
||||||
accountIdlers map[string]struct{} // Peers that aren't serving account requests
|
|
||||||
bytecodeIdlers map[string]struct{} // Peers that aren't serving bytecode requests
|
|
||||||
storageIdlers map[string]struct{} // Peers that aren't serving storage requests
|
|
||||||
accessListIdlers map[string]struct{} // Peers that aren't serving BAL requests
|
|
||||||
|
|
||||||
// These fields should be protected by lock.
|
// These fields should be protected by lock.
|
||||||
accountReqs map[uint64]*accountRequestV2 // Account requests currently running
|
accountReqs map[uint64]*accountRequestV2 // Account requests currently running
|
||||||
bytecodeReqs map[uint64]*bytecodeRequestV2 // Bytecode requests currently running
|
bytecodeReqs map[uint64]*bytecodeRequestV2 // Bytecode requests currently running
|
||||||
|
|
@ -507,7 +517,6 @@ func (s *syncerV2) Unregister(id string) error {
|
||||||
|
|
||||||
// Remove status markers, even if no sync is running
|
// Remove status markers, even if no sync is running
|
||||||
delete(s.statelessPeers, id)
|
delete(s.statelessPeers, id)
|
||||||
|
|
||||||
delete(s.accountIdlers, id)
|
delete(s.accountIdlers, id)
|
||||||
delete(s.storageIdlers, id)
|
delete(s.storageIdlers, id)
|
||||||
delete(s.bytecodeIdlers, id)
|
delete(s.bytecodeIdlers, id)
|
||||||
|
|
@ -529,6 +538,8 @@ func (s *syncerV2) Sync(target *types.Header, cancel chan struct{}) error {
|
||||||
s.lock.Lock()
|
s.lock.Lock()
|
||||||
s.statelessPeers = make(map[string]struct{})
|
s.statelessPeers = make(map[string]struct{})
|
||||||
s.lock.Unlock()
|
s.lock.Unlock()
|
||||||
|
|
||||||
|
// Track the time when snap sync actually starts
|
||||||
if s.startTime.IsZero() {
|
if s.startTime.IsZero() {
|
||||||
s.startTime = time.Now()
|
s.startTime = time.Now()
|
||||||
}
|
}
|
||||||
|
|
@ -798,7 +809,6 @@ func isPivotReorged(db ethdb.Database, prev, curr *types.Header) bool {
|
||||||
if canonical == (common.Hash{}) {
|
if canonical == (common.Hash{}) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// If canonical at the old pivot's height has a different hash, the
|
// If canonical at the old pivot's height has a different hash, the
|
||||||
// old pivot was reorged out.
|
// old pivot was reorged out.
|
||||||
return canonical != prev.Hash()
|
return canonical != prev.Hash()
|
||||||
|
|
@ -1325,26 +1335,26 @@ func (s *syncerV2) pruneStaleState() error {
|
||||||
deleteKeyRange(batch, accountKey(task.Next), keyRangeLimit(bytes.Clone(rawdb.SnapshotAccountPrefix), task.Last))
|
deleteKeyRange(batch, accountKey(task.Next), keyRangeLimit(bytes.Clone(rawdb.SnapshotAccountPrefix), task.Last))
|
||||||
|
|
||||||
protected := make([]common.Hash, 0, len(task.stateCompleted)+len(task.SubTasks))
|
protected := make([]common.Hash, 0, len(task.stateCompleted)+len(task.SubTasks))
|
||||||
for hash := range task.stateCompleted {
|
for accountHash := range task.stateCompleted {
|
||||||
if bytes.Compare(hash[:], task.Next[:]) < 0 {
|
if bytes.Compare(accountHash[:], task.Next[:]) < 0 {
|
||||||
return errors.New("unexpected storage marker before the range")
|
return errors.New("unexpected storage marker before the range")
|
||||||
}
|
}
|
||||||
if bytes.Compare(hash[:], task.Last[:]) > 0 {
|
if bytes.Compare(accountHash[:], task.Last[:]) > 0 {
|
||||||
return errors.New("unexpected storage marker after the range")
|
return errors.New("unexpected storage marker after the range")
|
||||||
}
|
}
|
||||||
protected = append(protected, hash)
|
protected = append(protected, accountHash)
|
||||||
}
|
}
|
||||||
for hash := range task.SubTasks {
|
for accountHash := range task.SubTasks {
|
||||||
if _, ok := task.stateCompleted[hash]; ok {
|
if _, ok := task.stateCompleted[accountHash]; ok {
|
||||||
return errors.New("unexpected duplicated storage marker")
|
return errors.New("unexpected duplicated storage marker")
|
||||||
}
|
}
|
||||||
if bytes.Compare(hash[:], task.Next[:]) < 0 {
|
if bytes.Compare(accountHash[:], task.Next[:]) < 0 {
|
||||||
return errors.New("unexpected storage marker before the range")
|
return errors.New("unexpected storage marker before the range")
|
||||||
}
|
}
|
||||||
if bytes.Compare(hash[:], task.Last[:]) > 0 {
|
if bytes.Compare(accountHash[:], task.Last[:]) > 0 {
|
||||||
return errors.New("unexpected storage marker after the range")
|
return errors.New("unexpected storage marker after the range")
|
||||||
}
|
}
|
||||||
protected = append(protected, hash)
|
protected = append(protected, accountHash)
|
||||||
}
|
}
|
||||||
sort.Slice(protected, func(i, j int) bool {
|
sort.Slice(protected, func(i, j int) bool {
|
||||||
return bytes.Compare(protected[i][:], protected[j][:]) < 0
|
return bytes.Compare(protected[i][:], protected[j][:]) < 0
|
||||||
|
|
@ -1524,8 +1534,7 @@ func (s *syncerV2) cleanStorageTasks() {
|
||||||
delete(task.SubTasks, account)
|
delete(task.SubTasks, account)
|
||||||
task.pend--
|
task.pend--
|
||||||
|
|
||||||
// Mark the state as complete to prevent resyncing, regardless
|
// Mark the state as complete to prevent resyncing
|
||||||
// if state healing is necessary.
|
|
||||||
task.stateCompleted[account] = struct{}{}
|
task.stateCompleted[account] = struct{}{}
|
||||||
|
|
||||||
// If this was the last pending task, forward the account task
|
// If this was the last pending task, forward the account task
|
||||||
|
|
@ -2137,8 +2146,9 @@ func (s *syncerV2) processAccountResponse(res *accountResponseV2) {
|
||||||
// Check if the account is a contract with an unknown storage trie
|
// Check if the account is a contract with an unknown storage trie
|
||||||
if account.Root != types.EmptyRootHash {
|
if account.Root != types.EmptyRootHash {
|
||||||
// If the storage was already retrieved in the last cycle, there's no need
|
// If the storage was already retrieved in the last cycle, there's no need
|
||||||
// to resync it again, regardless of whether the storage root is consistent
|
// to resync it again, the state difference has already been fixed in the
|
||||||
// or not.
|
// bal catchup stage. This relies on access lists covering every storage
|
||||||
|
// change, guaranteed post EIP-6780.
|
||||||
if _, exist := res.task.stateCompleted[res.hashes[i]]; exist {
|
if _, exist := res.task.stateCompleted[res.hashes[i]]; exist {
|
||||||
// The leftover storage tasks are not expected, unless system is
|
// The leftover storage tasks are not expected, unless system is
|
||||||
// very wrong.
|
// very wrong.
|
||||||
|
|
@ -2158,10 +2168,6 @@ func (s *syncerV2) processAccountResponse(res *accountResponseV2) {
|
||||||
resumed[res.hashes[i]] = struct{}{}
|
resumed[res.hashes[i]] = struct{}{}
|
||||||
largeStorageResumedGauge.Inc(1)
|
largeStorageResumedGauge.Inc(1)
|
||||||
} else {
|
} else {
|
||||||
// It's possible that in the hash scheme, the storage, along
|
|
||||||
// with the trie nodes of the given root, is already present
|
|
||||||
// in the database. Schedule the storage task anyway to simplify
|
|
||||||
// the logic here.
|
|
||||||
res.task.stateTasks[res.hashes[i]] = account.Root
|
res.task.stateTasks[res.hashes[i]] = account.Root
|
||||||
}
|
}
|
||||||
res.task.needState[i] = true
|
res.task.needState[i] = true
|
||||||
|
|
@ -2185,7 +2191,7 @@ func (s *syncerV2) processAccountResponse(res *accountResponseV2) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, ok := resumed[hash]; !ok {
|
if _, ok := resumed[hash]; !ok {
|
||||||
log.Warn("Aborting suspended storage retrieval", "account", hash)
|
log.Error("Aborting suspended storage retrieval", "account", hash)
|
||||||
delete(res.task.SubTasks, hash)
|
delete(res.task.SubTasks, hash)
|
||||||
largeStorageDiscardGauge.Inc(1)
|
largeStorageDiscardGauge.Inc(1)
|
||||||
}
|
}
|
||||||
|
|
@ -2270,8 +2276,7 @@ func (s *syncerV2) processStorageResponse(res *storageResponseV2) {
|
||||||
res.mainTask.stateTasks[account] = res.roots[i]
|
res.mainTask.stateTasks[account] = res.roots[i]
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// State was delivered, if complete mark as not needed any more, otherwise
|
// State was delivered, if complete mark as not needed any more
|
||||||
// mark the account as needing healing
|
|
||||||
for j, hash := range res.mainTask.res.hashes {
|
for j, hash := range res.mainTask.res.hashes {
|
||||||
if account != hash {
|
if account != hash {
|
||||||
continue
|
continue
|
||||||
|
|
@ -2485,6 +2490,7 @@ func (s *syncerV2) OnAccounts(peer SyncPeerV2, id uint64, hashes []common.Hash,
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
s.lock.Lock()
|
s.lock.Lock()
|
||||||
|
|
||||||
// Ensure the response is for a valid request
|
// Ensure the response is for a valid request
|
||||||
req, ok := s.accountReqs[id]
|
req, ok := s.accountReqs[id]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
@ -2531,6 +2537,7 @@ func (s *syncerV2) OnAccounts(peer SyncPeerV2, id uint64, hashes []common.Hash,
|
||||||
cont, err := trie.VerifyRangeProof(root, req.origin[:], keys, accounts, nodes.Set())
|
cont, err := trie.VerifyRangeProof(root, req.origin[:], keys, accounts, nodes.Set())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn("Account range failed proof", "err", err)
|
logger.Warn("Account range failed proof", "err", err)
|
||||||
|
|
||||||
// Signal this request as failed, and ready for rescheduling
|
// Signal this request as failed, and ready for rescheduling
|
||||||
s.scheduleRevertAccountRequest(req)
|
s.scheduleRevertAccountRequest(req)
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -3535,3 +3535,347 @@ func testCatchUpAppliesStorageBALs(t *testing.T, scheme string) {
|
||||||
checkSlot(slotNew, vNew, true)
|
checkSlot(slotNew, vNew, true)
|
||||||
checkSlot(slotMultiTx, vMultiFinal, true)
|
checkSlot(slotMultiTx, vMultiFinal, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// suspendChunkedContractSync runs an interrupted first download cycle against
|
||||||
|
// the given state. Storage responses are byte-capped so the contract switches
|
||||||
|
// into chunked (large-contract) mode, and the cycle is cancelled on the first
|
||||||
|
// subtask continuation request, leaving suspended SubTasks in the journal and
|
||||||
|
// a partially downloaded storage prefix on disk. Both are asserted before
|
||||||
|
// returning.
|
||||||
|
func suspendChunkedContractSync(t *testing.T, db ethdb.Database, scheme string, pivot *types.Header, accTrie *trie.Trie, accElems []*kv, stTrie *trie.Trie, stElems []*kv, contractHash common.Hash) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var (
|
||||||
|
once sync.Once
|
||||||
|
cancel = make(chan struct{})
|
||||||
|
term = func() { once.Do(func() { close(cancel) }) }
|
||||||
|
)
|
||||||
|
syncer := newSyncerV2(db, scheme)
|
||||||
|
src := newTestPeerV2("suspend-seed", t, term)
|
||||||
|
src.accountTrie = accTrie.Copy()
|
||||||
|
src.accountValues = accElems
|
||||||
|
src.setStorageTries(map[common.Hash]*trie.Trie{contractHash: stTrie})
|
||||||
|
src.storageValues = map[common.Hash][]*kv{
|
||||||
|
contractHash: stElems,
|
||||||
|
}
|
||||||
|
src.storageRequestV2Handler = func(tp *testPeerV2, id uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max int) error {
|
||||||
|
// A continuation request proves the first chunked response was fully
|
||||||
|
// processed and the subtasks exist; cut the cycle right there.
|
||||||
|
if len(origin) > 0 {
|
||||||
|
term()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Byte-cap the initial response so the contract gets chunked.
|
||||||
|
return defaultStorageRequestHandlerV2(tp, id, root, accounts, origin, limit, 2000)
|
||||||
|
}
|
||||||
|
syncer.Register(src)
|
||||||
|
src.remote = syncer
|
||||||
|
|
||||||
|
syncer.loadSyncStatus()
|
||||||
|
syncer.pivot = pivot // Sync pins this before downloadState
|
||||||
|
syncer.downloadState(cancel)
|
||||||
|
syncer.saveSyncStatus()
|
||||||
|
|
||||||
|
// The journal must carry the suspended subtasks and a partial storage
|
||||||
|
// prefix must be on disk, otherwise the fixture proves nothing.
|
||||||
|
loaded := newSyncerV2(db, scheme)
|
||||||
|
loaded.loadSyncStatus()
|
||||||
|
suspended := false
|
||||||
|
for _, task := range loaded.tasks {
|
||||||
|
if len(task.SubTasks[contractHash]) > 0 {
|
||||||
|
suspended = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !suspended {
|
||||||
|
t.Fatal("fixture: no suspended storage subtasks journaled")
|
||||||
|
}
|
||||||
|
downloaded := 0
|
||||||
|
for _, entry := range stElems {
|
||||||
|
if len(rawdb.ReadStorageSnapshot(db, contractHash, common.BytesToHash(entry.k))) > 0 {
|
||||||
|
downloaded++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if downloaded == 0 || downloaded == len(stElems) {
|
||||||
|
t.Fatalf("fixture: want partially downloaded storage, got %d/%d slots", downloaded, len(stElems))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPivotMoveAbortsEmptiedStorage covers the suspended-storage abort path:
|
||||||
|
// a chunked contract download is interrupted, then the pivot moves to a block
|
||||||
|
// whose BAL zeroes every slot of the contract (the only way a storage root can
|
||||||
|
// become empty post EIP-6780). The follow-up Sync must delete the downloaded
|
||||||
|
// prefix during catch-up, abort the suspended subtasks when the account
|
||||||
|
// returns with an empty root, and complete against the exact new state root.
|
||||||
|
func TestPivotMoveAbortsEmptiedStorage(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
testPivotMoveAbortsEmptiedStorage(t, rawdb.HashScheme)
|
||||||
|
testPivotMoveAbortsEmptiedStorage(t, rawdb.PathScheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPivotMoveAbortsEmptiedStorage(t *testing.T, scheme string) {
|
||||||
|
contractAddr := common.HexToAddress("0x00000000000000000000000000000000c0ffee02")
|
||||||
|
contractHash := crypto.Keccak256Hash(contractAddr[:])
|
||||||
|
|
||||||
|
// Enough slots that a byte-capped response leaves most of them undelivered.
|
||||||
|
slotsA := make(map[common.Hash]common.Hash, 500)
|
||||||
|
for i := 0; i < 500; i++ {
|
||||||
|
slotsA[common.BigToHash(big.NewInt(int64(i+1)))] = common.BigToHash(big.NewInt(int64(0x10000 + i)))
|
||||||
|
}
|
||||||
|
contractTmpl := types.StateAccount{
|
||||||
|
Nonce: 7,
|
||||||
|
Balance: uint256.NewInt(123456),
|
||||||
|
CodeHash: types.EmptyCodeHash[:],
|
||||||
|
}
|
||||||
|
// Storage-less filler accounts, identical at A and A+1.
|
||||||
|
_, _, plain, _ := makeAccountTrieWithAddresses(20, scheme)
|
||||||
|
|
||||||
|
// State at pivot A (contract populated) and at A+1 (contract emptied).
|
||||||
|
accTrieA, accElemsA, stTrieA, stElemsA, rootA := makeStateWithStorageContract(scheme, plain, contractAddr, contractTmpl, slotsA)
|
||||||
|
accTrieB, accElemsB, _, _, rootB := makeStateWithStorageContract(scheme, plain, contractAddr, contractTmpl, nil)
|
||||||
|
|
||||||
|
// The A+1 BAL zeroes every slot: post EIP-6780 an emptied storage is
|
||||||
|
// always visible as per-slot writes.
|
||||||
|
cb := bal.NewConstructionBlockAccessList()
|
||||||
|
for raw := range slotsA {
|
||||||
|
cb.StorageWrite(0, contractAddr, raw, common.Hash{})
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
|
||||||
|
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{
|
||||||
|
ParentHash: hdrA.Hash(),
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Cycle 1: interrupted download at pivot A with suspended contract subtasks.
|
||||||
|
suspendChunkedContractSync(t, db, scheme, hdrA, accTrieA, accElemsA, stTrieA, stElemsA, contractHash)
|
||||||
|
|
||||||
|
// Cycle 2: the pivot moves to A+1. Catch-up must delete the downloaded
|
||||||
|
// slots, the account response (empty root) must abort the suspended
|
||||||
|
// subtasks, and the sync must complete against rootB.
|
||||||
|
var (
|
||||||
|
once sync.Once
|
||||||
|
cancel = make(chan struct{})
|
||||||
|
term = func() { once.Do(func() { close(cancel) }) }
|
||||||
|
)
|
||||||
|
syncer := newSyncerV2(db, scheme)
|
||||||
|
src := newTestPeerV2("emptied-serve", t, term)
|
||||||
|
src.accountTrie = accTrieB.Copy()
|
||||||
|
src.accountValues = accElemsB
|
||||||
|
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 move sync failed: %v", err)
|
||||||
|
}
|
||||||
|
close(done)
|
||||||
|
|
||||||
|
// The emptied contract must not have triggered any storage retrieval.
|
||||||
|
if n := src.nStorageRequests.Load(); n != 0 {
|
||||||
|
t.Errorf("unexpected storage requests for emptied contract: %d", n)
|
||||||
|
}
|
||||||
|
// The generation inside Sync already verified rootB; re-walk independently.
|
||||||
|
verifyTrie(scheme, db, rootB, t)
|
||||||
|
|
||||||
|
// Not a single downloaded slot may survive.
|
||||||
|
for _, entry := range stElemsA {
|
||||||
|
if v := rawdb.ReadStorageSnapshot(db, contractHash, common.BytesToHash(entry.k)); len(v) != 0 {
|
||||||
|
t.Errorf("stale slot %x survived the emptied contract: %x", entry.k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestShortAccountResponseKeepsSuspendedStorage covers the keep branch of the
|
||||||
|
// suspended-subtask sweep: a resumed cycle whose first account response ends
|
||||||
|
// short of the contract must keep the suspended subtasks (they lie beyond the
|
||||||
|
// response), then resume them — not restart from scratch — once a later wave
|
||||||
|
// covers the contract.
|
||||||
|
func TestShortAccountResponseKeepsSuspendedStorage(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
testShortAccountResponseKeepsSuspendedStorage(t, rawdb.HashScheme)
|
||||||
|
testShortAccountResponseKeepsSuspendedStorage(t, rawdb.PathScheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testShortAccountResponseKeepsSuspendedStorage(t *testing.T, scheme string) {
|
||||||
|
contractAddr := common.HexToAddress("0x00000000000000000000000000000000c0ffee03")
|
||||||
|
contractHash := crypto.Keccak256Hash(contractAddr[:])
|
||||||
|
|
||||||
|
// Craft neighbour accounts right around the contract hash. They must share
|
||||||
|
// its leading nibble so all of them land in the contract's account task:
|
||||||
|
// two below give the shortened response some content, one sits above.
|
||||||
|
mkPlain := func(key common.Hash, nonce uint64) *kv {
|
||||||
|
val, _ := rlp.EncodeToBytes(&types.StateAccount{
|
||||||
|
Nonce: nonce,
|
||||||
|
Balance: uint256.NewInt(1000 + nonce),
|
||||||
|
Root: types.EmptyRootHash,
|
||||||
|
CodeHash: types.EmptyCodeHash[:],
|
||||||
|
})
|
||||||
|
return &kv{key.Bytes(), val}
|
||||||
|
}
|
||||||
|
shift := func(h common.Hash, delta int64) common.Hash {
|
||||||
|
return common.BigToHash(new(big.Int).Add(h.Big(), big.NewInt(delta)))
|
||||||
|
}
|
||||||
|
below1, below2, above := shift(contractHash, -2), shift(contractHash, -1), shift(contractHash, 1)
|
||||||
|
if below1[0]>>4 != contractHash[0]>>4 || above[0]>>4 != contractHash[0]>>4 {
|
||||||
|
t.Fatal("fixture: neighbour accounts left the contract's task range")
|
||||||
|
}
|
||||||
|
plain := []*kv{
|
||||||
|
mkPlain(below1, 1),
|
||||||
|
mkPlain(below2, 2),
|
||||||
|
mkPlain(above, 3),
|
||||||
|
}
|
||||||
|
slots := make(map[common.Hash]common.Hash, 500)
|
||||||
|
for i := 0; i < 500; i++ {
|
||||||
|
slots[common.BigToHash(big.NewInt(int64(i+1)))] = common.BigToHash(big.NewInt(int64(0x20000 + i)))
|
||||||
|
}
|
||||||
|
contractTmpl := types.StateAccount{
|
||||||
|
Nonce: 7,
|
||||||
|
Balance: uint256.NewInt(123456),
|
||||||
|
CodeHash: types.EmptyCodeHash[:],
|
||||||
|
}
|
||||||
|
accTrie, accElems, stTrie, stElems, root := makeStateWithStorageContract(scheme, plain, contractAddr, contractTmpl, slots)
|
||||||
|
|
||||||
|
db := rawdb.NewMemoryDatabase()
|
||||||
|
pivot := mkPivot(0, root)
|
||||||
|
|
||||||
|
// Cycle 1: interrupted download with suspended contract subtasks.
|
||||||
|
suspendChunkedContractSync(t, db, scheme, pivot, accTrie, accElems, stTrie, stElems, contractHash)
|
||||||
|
|
||||||
|
// Cycle 2: resume at the same pivot. The first response of the contract's
|
||||||
|
// task is truncated below the contract; the follow-up wave serves it in
|
||||||
|
// full. Watch the storage requests to tell resumption from a restart.
|
||||||
|
var (
|
||||||
|
once sync.Once
|
||||||
|
cancel = make(chan struct{})
|
||||||
|
term = func() { once.Do(func() { close(cancel) }) }
|
||||||
|
|
||||||
|
truncated atomic.Bool
|
||||||
|
freshFetch atomic.Bool
|
||||||
|
resumed atomic.Bool
|
||||||
|
)
|
||||||
|
syncer := newSyncerV2(db, scheme)
|
||||||
|
src := newTestPeerV2("resume-serve", t, term)
|
||||||
|
src.accountTrie = accTrie.Copy()
|
||||||
|
src.accountValues = accElems
|
||||||
|
src.setStorageTries(map[common.Hash]*trie.Trie{contractHash: stTrie})
|
||||||
|
src.storageValues = map[common.Hash][]*kv{
|
||||||
|
contractHash: stElems,
|
||||||
|
}
|
||||||
|
src.accountRequestV2Handler = func(tp *testPeerV2, id uint64, reqRoot common.Hash, origin common.Hash, limit common.Hash, cap int) error {
|
||||||
|
if !truncated.Load() && bytes.Compare(origin[:], contractHash[:]) <= 0 && bytes.Compare(contractHash[:], limit[:]) <= 0 {
|
||||||
|
truncated.Store(true)
|
||||||
|
|
||||||
|
// Serve only the accounts below the contract: a legitimate
|
||||||
|
// shortened response whose proof marks a continuation.
|
||||||
|
var (
|
||||||
|
keys []common.Hash
|
||||||
|
vals [][]byte
|
||||||
|
)
|
||||||
|
for _, entry := range tp.accountValues {
|
||||||
|
if bytes.Compare(entry.k, origin[:]) < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Explicitly truncate the response at the contract, leaving the
|
||||||
|
// subtasks as suspended.
|
||||||
|
if bytes.Compare(entry.k, contractHash[:]) >= 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
keys = append(keys, common.BytesToHash(entry.k))
|
||||||
|
vals = append(vals, entry.v)
|
||||||
|
}
|
||||||
|
if len(keys) == 0 {
|
||||||
|
t.Errorf("fixture: shortened response empty, origin %x", origin)
|
||||||
|
}
|
||||||
|
proof := trienode.NewProofSet()
|
||||||
|
if err := tp.accountTrie.Prove(origin[:], proof); err != nil {
|
||||||
|
t.Errorf("could not prove origin: %v", err)
|
||||||
|
}
|
||||||
|
if len(keys) > 0 {
|
||||||
|
if err := tp.accountTrie.Prove(keys[len(keys)-1].Bytes(), proof); err != nil {
|
||||||
|
t.Errorf("could not prove last item: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := tp.remote.OnAccounts(tp, id, keys, vals, proof.List()); err != nil {
|
||||||
|
t.Errorf("remote side rejected shortened delivery: %v", err)
|
||||||
|
tp.term()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return defaultAccountRequestHandlerV2(tp, id, reqRoot, origin, limit, cap)
|
||||||
|
}
|
||||||
|
src.storageRequestV2Handler = func(tp *testPeerV2, id uint64, reqRoot common.Hash, accounts []common.Hash, origin, limit []byte, max int) error {
|
||||||
|
for _, account := range accounts {
|
||||||
|
if account == contractHash {
|
||||||
|
if len(origin) > 0 {
|
||||||
|
resumed.Store(true)
|
||||||
|
} else {
|
||||||
|
freshFetch.Store(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultStorageRequestHandlerV2(tp, id, reqRoot, accounts, origin, limit, max)
|
||||||
|
}
|
||||||
|
syncer.Register(src)
|
||||||
|
src.remote = syncer
|
||||||
|
done := checkStall(t, term)
|
||||||
|
if err := syncer.Sync(pivot, cancel); err != nil {
|
||||||
|
t.Fatalf("resumed sync failed: %v", err)
|
||||||
|
}
|
||||||
|
close(done)
|
||||||
|
|
||||||
|
if !truncated.Load() {
|
||||||
|
t.Fatal("shortened account response never served")
|
||||||
|
}
|
||||||
|
if freshFetch.Load() {
|
||||||
|
t.Error("suspended subtasks were aborted: contract storage rescheduled from scratch")
|
||||||
|
}
|
||||||
|
if !resumed.Load() {
|
||||||
|
t.Error("suspended subtasks never resumed")
|
||||||
|
}
|
||||||
|
verifyTrie(scheme, db, root, t)
|
||||||
|
|
||||||
|
for _, entry := range stElems {
|
||||||
|
if len(rawdb.ReadStorageSnapshot(db, contractHash, common.BytesToHash(entry.k))) == 0 {
|
||||||
|
t.Errorf("missing slot %x after resumed download", entry.k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue