From aefbdf5c1c30eaf52e6348c7ca17e7a6cfc648c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isabel=20Sch=C3=B6ps=20Thiel?= <155141998+IST-Github@users.noreply.github.com> Date: Mon, 1 Jan 2024 00:51:52 +0100 Subject: [PATCH] Delete eth directory --- eth/api.go | 52 - eth/api_backend.go | 417 ----- eth/api_debug_test.go | 222 --- eth/api_miner.go | 85 -- eth/backend.go | 552 ------- eth/catalyst/api.go | 837 ---------- eth/catalyst/api_test.go | 1644 -------------------- eth/catalyst/queue.go | 158 -- eth/catalyst/simulated_beacon.go | 272 ---- eth/catalyst/simulated_beacon_api.go | 36 - eth/catalyst/simulated_beacon_test.go | 141 -- eth/catalyst/tester.go | 97 -- eth/ethconfig/config.go | 179 --- eth/ethconfig/gen_config.go | 268 ---- eth/fetcher/block_fetcher.go | 939 ------------ eth/fetcher/block_fetcher_test.go | 948 ------------ eth/fetcher/tx_fetcher.go | 1003 ------------ eth/fetcher/tx_fetcher_test.go | 2030 ------------------------- eth/gasestimator/gasestimator.go | 235 --- eth/handler.go | 692 --------- eth/handler_eth_test.go | 608 -------- eth/handler_snap.go | 50 - eth/handler_test.go | 185 --- eth/peer.go | 59 - eth/peerset.go | 260 ---- eth/state_accessor.go | 259 ---- eth/sync.go | 269 ---- 27 files changed, 12497 deletions(-) delete mode 100644 eth/api.go delete mode 100644 eth/api_backend.go delete mode 100644 eth/api_debug_test.go delete mode 100644 eth/api_miner.go delete mode 100644 eth/backend.go delete mode 100644 eth/catalyst/api.go delete mode 100644 eth/catalyst/api_test.go delete mode 100644 eth/catalyst/queue.go delete mode 100644 eth/catalyst/simulated_beacon.go delete mode 100644 eth/catalyst/simulated_beacon_api.go delete mode 100644 eth/catalyst/simulated_beacon_test.go delete mode 100644 eth/catalyst/tester.go delete mode 100644 eth/ethconfig/config.go delete mode 100644 eth/ethconfig/gen_config.go delete mode 100644 eth/fetcher/block_fetcher.go delete mode 100644 eth/fetcher/block_fetcher_test.go delete mode 100644 eth/fetcher/tx_fetcher.go delete mode 100644 eth/fetcher/tx_fetcher_test.go delete mode 100644 eth/gasestimator/gasestimator.go delete mode 100644 eth/handler.go delete mode 100644 eth/handler_eth_test.go delete mode 100644 eth/handler_snap.go delete mode 100644 eth/handler_test.go delete mode 100644 eth/peer.go delete mode 100644 eth/peerset.go delete mode 100644 eth/state_accessor.go delete mode 100644 eth/sync.go diff --git a/eth/api.go b/eth/api.go deleted file mode 100644 index 44e934fd04..0000000000 --- a/eth/api.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2015 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 . - -package eth - -import ( - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" -) - -// EthereumAPI provides an API to access Ethereum full node-related information. -type EthereumAPI struct { - e *Ethereum -} - -// NewEthereumAPI creates a new Ethereum protocol API for full nodes. -func NewEthereumAPI(e *Ethereum) *EthereumAPI { - return &EthereumAPI{e} -} - -// Etherbase is the address that mining rewards will be sent to. -func (api *EthereumAPI) Etherbase() (common.Address, error) { - return api.e.Etherbase() -} - -// Coinbase is the address that mining rewards will be sent to (alias for Etherbase). -func (api *EthereumAPI) Coinbase() (common.Address, error) { - return api.Etherbase() -} - -// Hashrate returns the POW hashrate. -func (api *EthereumAPI) Hashrate() hexutil.Uint64 { - return hexutil.Uint64(api.e.Miner().Hashrate()) -} - -// Mining returns an indication if this node is currently mining. -func (api *EthereumAPI) Mining() bool { - return api.e.IsMining() -} diff --git a/eth/api_backend.go b/eth/api_backend.go deleted file mode 100644 index 84eb200095..0000000000 --- a/eth/api_backend.go +++ /dev/null @@ -1,417 +0,0 @@ -// Copyright 2015 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 . - -package eth - -import ( - "context" - "errors" - "math/big" - "time" - - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/accounts" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/consensus" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/bloombits" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/state" - "github.com/ethereum/go-ethereum/core/txpool" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/vm" - "github.com/ethereum/go-ethereum/eth/gasprice" - "github.com/ethereum/go-ethereum/eth/tracers" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/event" - "github.com/ethereum/go-ethereum/miner" - "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/rpc" -) - -// EthAPIBackend implements ethapi.Backend and tracers.Backend for full nodes -type EthAPIBackend struct { - extRPCEnabled bool - allowUnprotectedTxs bool - eth *Ethereum - gpo *gasprice.Oracle -} - -// ChainConfig returns the active chain configuration. -func (b *EthAPIBackend) ChainConfig() *params.ChainConfig { - return b.eth.blockchain.Config() -} - -func (b *EthAPIBackend) CurrentBlock() *types.Header { - return b.eth.blockchain.CurrentBlock() -} - -func (b *EthAPIBackend) SetHead(number uint64) { - b.eth.handler.downloader.Cancel() - b.eth.blockchain.SetHead(number) -} - -func (b *EthAPIBackend) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) { - // Pending block is only known by the miner - if number == rpc.PendingBlockNumber { - block := b.eth.miner.PendingBlock() - if block == nil { - return nil, errors.New("pending block is not available") - } - return block.Header(), nil - } - // Otherwise resolve and return the block - if number == rpc.LatestBlockNumber { - return b.eth.blockchain.CurrentBlock(), nil - } - if number == rpc.FinalizedBlockNumber { - block := b.eth.blockchain.CurrentFinalBlock() - if block == nil { - return nil, errors.New("finalized block not found") - } - return block, nil - } - if number == rpc.SafeBlockNumber { - block := b.eth.blockchain.CurrentSafeBlock() - if block == nil { - return nil, errors.New("safe block not found") - } - return block, nil - } - return b.eth.blockchain.GetHeaderByNumber(uint64(number)), nil -} - -func (b *EthAPIBackend) HeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.Header, error) { - if blockNr, ok := blockNrOrHash.Number(); ok { - return b.HeaderByNumber(ctx, blockNr) - } - if hash, ok := blockNrOrHash.Hash(); ok { - header := b.eth.blockchain.GetHeaderByHash(hash) - if header == nil { - return nil, errors.New("header for hash not found") - } - if blockNrOrHash.RequireCanonical && b.eth.blockchain.GetCanonicalHash(header.Number.Uint64()) != hash { - return nil, errors.New("hash is not currently canonical") - } - return header, nil - } - return nil, errors.New("invalid arguments; neither block nor hash specified") -} - -func (b *EthAPIBackend) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) { - return b.eth.blockchain.GetHeaderByHash(hash), nil -} - -func (b *EthAPIBackend) BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error) { - // Pending block is only known by the miner - if number == rpc.PendingBlockNumber { - block := b.eth.miner.PendingBlock() - if block == nil { - return nil, errors.New("pending block is not available") - } - return block, nil - } - // Otherwise resolve and return the block - if number == rpc.LatestBlockNumber { - header := b.eth.blockchain.CurrentBlock() - return b.eth.blockchain.GetBlock(header.Hash(), header.Number.Uint64()), nil - } - if number == rpc.FinalizedBlockNumber { - header := b.eth.blockchain.CurrentFinalBlock() - if header == nil { - return nil, errors.New("finalized block not found") - } - return b.eth.blockchain.GetBlock(header.Hash(), header.Number.Uint64()), nil - } - if number == rpc.SafeBlockNumber { - header := b.eth.blockchain.CurrentSafeBlock() - if header == nil { - return nil, errors.New("safe block not found") - } - return b.eth.blockchain.GetBlock(header.Hash(), header.Number.Uint64()), nil - } - return b.eth.blockchain.GetBlockByNumber(uint64(number)), nil -} - -func (b *EthAPIBackend) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { - return b.eth.blockchain.GetBlockByHash(hash), nil -} - -// GetBody returns body of a block. It does not resolve special block numbers. -func (b *EthAPIBackend) GetBody(ctx context.Context, hash common.Hash, number rpc.BlockNumber) (*types.Body, error) { - if number < 0 || hash == (common.Hash{}) { - return nil, errors.New("invalid arguments; expect hash and no special block numbers") - } - if body := b.eth.blockchain.GetBody(hash); body != nil { - return body, nil - } - return nil, errors.New("block body not found") -} - -func (b *EthAPIBackend) BlockByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.Block, error) { - if blockNr, ok := blockNrOrHash.Number(); ok { - return b.BlockByNumber(ctx, blockNr) - } - if hash, ok := blockNrOrHash.Hash(); ok { - header := b.eth.blockchain.GetHeaderByHash(hash) - if header == nil { - return nil, errors.New("header for hash not found") - } - if blockNrOrHash.RequireCanonical && b.eth.blockchain.GetCanonicalHash(header.Number.Uint64()) != hash { - return nil, errors.New("hash is not currently canonical") - } - block := b.eth.blockchain.GetBlock(hash, header.Number.Uint64()) - if block == nil { - return nil, errors.New("header found, but block body is missing") - } - return block, nil - } - return nil, errors.New("invalid arguments; neither block nor hash specified") -} - -func (b *EthAPIBackend) PendingBlockAndReceipts() (*types.Block, types.Receipts) { - return b.eth.miner.PendingBlockAndReceipts() -} - -func (b *EthAPIBackend) StateAndHeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*state.StateDB, *types.Header, error) { - // Pending state is only known by the miner - if number == rpc.PendingBlockNumber { - block, state := b.eth.miner.Pending() - if block == nil || state == nil { - return nil, nil, errors.New("pending state is not available") - } - return state, block.Header(), nil - } - // Otherwise resolve the block number and return its state - header, err := b.HeaderByNumber(ctx, number) - if err != nil { - return nil, nil, err - } - if header == nil { - return nil, nil, errors.New("header not found") - } - stateDb, err := b.eth.BlockChain().StateAt(header.Root) - if err != nil { - return nil, nil, err - } - return stateDb, header, nil -} - -func (b *EthAPIBackend) StateAndHeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*state.StateDB, *types.Header, error) { - if blockNr, ok := blockNrOrHash.Number(); ok { - return b.StateAndHeaderByNumber(ctx, blockNr) - } - if hash, ok := blockNrOrHash.Hash(); ok { - header, err := b.HeaderByHash(ctx, hash) - if err != nil { - return nil, nil, err - } - if header == nil { - return nil, nil, errors.New("header for hash not found") - } - if blockNrOrHash.RequireCanonical && b.eth.blockchain.GetCanonicalHash(header.Number.Uint64()) != hash { - return nil, nil, errors.New("hash is not currently canonical") - } - stateDb, err := b.eth.BlockChain().StateAt(header.Root) - if err != nil { - return nil, nil, err - } - return stateDb, header, nil - } - return nil, nil, errors.New("invalid arguments; neither block nor hash specified") -} - -func (b *EthAPIBackend) GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error) { - return b.eth.blockchain.GetReceiptsByHash(hash), nil -} - -func (b *EthAPIBackend) GetLogs(ctx context.Context, hash common.Hash, number uint64) ([][]*types.Log, error) { - return rawdb.ReadLogs(b.eth.chainDb, hash, number), nil -} - -func (b *EthAPIBackend) GetTd(ctx context.Context, hash common.Hash) *big.Int { - if header := b.eth.blockchain.GetHeaderByHash(hash); header != nil { - return b.eth.blockchain.GetTd(hash, header.Number.Uint64()) - } - return nil -} - -func (b *EthAPIBackend) GetEVM(ctx context.Context, msg *core.Message, state *state.StateDB, header *types.Header, vmConfig *vm.Config, blockCtx *vm.BlockContext) *vm.EVM { - if vmConfig == nil { - vmConfig = b.eth.blockchain.GetVMConfig() - } - txContext := core.NewEVMTxContext(msg) - var context vm.BlockContext - if blockCtx != nil { - context = *blockCtx - } else { - context = core.NewEVMBlockContext(header, b.eth.BlockChain(), nil) - } - return vm.NewEVM(context, txContext, state, b.eth.blockchain.Config(), *vmConfig) -} - -func (b *EthAPIBackend) SubscribeRemovedLogsEvent(ch chan<- core.RemovedLogsEvent) event.Subscription { - return b.eth.BlockChain().SubscribeRemovedLogsEvent(ch) -} - -func (b *EthAPIBackend) SubscribePendingLogsEvent(ch chan<- []*types.Log) event.Subscription { - return b.eth.miner.SubscribePendingLogs(ch) -} - -func (b *EthAPIBackend) SubscribeChainEvent(ch chan<- core.ChainEvent) event.Subscription { - return b.eth.BlockChain().SubscribeChainEvent(ch) -} - -func (b *EthAPIBackend) SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription { - return b.eth.BlockChain().SubscribeChainHeadEvent(ch) -} - -func (b *EthAPIBackend) SubscribeChainSideEvent(ch chan<- core.ChainSideEvent) event.Subscription { - return b.eth.BlockChain().SubscribeChainSideEvent(ch) -} - -func (b *EthAPIBackend) SubscribeLogsEvent(ch chan<- []*types.Log) event.Subscription { - return b.eth.BlockChain().SubscribeLogsEvent(ch) -} - -func (b *EthAPIBackend) SendTx(ctx context.Context, signedTx *types.Transaction) error { - return b.eth.txPool.Add([]*types.Transaction{signedTx}, true, false)[0] -} - -func (b *EthAPIBackend) GetPoolTransactions() (types.Transactions, error) { - pending := b.eth.txPool.Pending(false) - var txs types.Transactions - for _, batch := range pending { - for _, lazy := range batch { - if tx := lazy.Resolve(); tx != nil { - txs = append(txs, tx) - } - } - } - return txs, nil -} - -func (b *EthAPIBackend) GetPoolTransaction(hash common.Hash) *types.Transaction { - return b.eth.txPool.Get(hash) -} - -func (b *EthAPIBackend) GetTransaction(ctx context.Context, txHash common.Hash) (*types.Transaction, common.Hash, uint64, uint64, error) { - tx, blockHash, blockNumber, index := rawdb.ReadTransaction(b.eth.ChainDb(), txHash) - return tx, blockHash, blockNumber, index, nil -} - -func (b *EthAPIBackend) GetPoolNonce(ctx context.Context, addr common.Address) (uint64, error) { - return b.eth.txPool.Nonce(addr), nil -} - -func (b *EthAPIBackend) Stats() (runnable int, blocked int) { - return b.eth.txPool.Stats() -} - -func (b *EthAPIBackend) TxPoolContent() (map[common.Address][]*types.Transaction, map[common.Address][]*types.Transaction) { - return b.eth.txPool.Content() -} - -func (b *EthAPIBackend) TxPoolContentFrom(addr common.Address) ([]*types.Transaction, []*types.Transaction) { - return b.eth.txPool.ContentFrom(addr) -} - -func (b *EthAPIBackend) TxPool() *txpool.TxPool { - return b.eth.txPool -} - -func (b *EthAPIBackend) SubscribeNewTxsEvent(ch chan<- core.NewTxsEvent) event.Subscription { - return b.eth.txPool.SubscribeTransactions(ch, true) -} - -func (b *EthAPIBackend) SyncProgress() ethereum.SyncProgress { - return b.eth.Downloader().Progress() -} - -func (b *EthAPIBackend) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { - return b.gpo.SuggestTipCap(ctx) -} - -func (b *EthAPIBackend) FeeHistory(ctx context.Context, blockCount uint64, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (firstBlock *big.Int, reward [][]*big.Int, baseFee []*big.Int, gasUsedRatio []float64, err error) { - return b.gpo.FeeHistory(ctx, blockCount, lastBlock, rewardPercentiles) -} - -func (b *EthAPIBackend) ChainDb() ethdb.Database { - return b.eth.ChainDb() -} - -func (b *EthAPIBackend) EventMux() *event.TypeMux { - return b.eth.EventMux() -} - -func (b *EthAPIBackend) AccountManager() *accounts.Manager { - return b.eth.AccountManager() -} - -func (b *EthAPIBackend) ExtRPCEnabled() bool { - return b.extRPCEnabled -} - -func (b *EthAPIBackend) UnprotectedAllowed() bool { - return b.allowUnprotectedTxs -} - -func (b *EthAPIBackend) RPCGasCap() uint64 { - return b.eth.config.RPCGasCap -} - -func (b *EthAPIBackend) RPCEVMTimeout() time.Duration { - return b.eth.config.RPCEVMTimeout -} - -func (b *EthAPIBackend) RPCTxFeeCap() float64 { - return b.eth.config.RPCTxFeeCap -} - -func (b *EthAPIBackend) BloomStatus() (uint64, uint64) { - sections, _, _ := b.eth.bloomIndexer.Sections() - return params.BloomBitsBlocks, sections -} - -func (b *EthAPIBackend) ServiceFilter(ctx context.Context, session *bloombits.MatcherSession) { - for i := 0; i < bloomFilterThreads; i++ { - go session.Multiplex(bloomRetrievalBatch, bloomRetrievalWait, b.eth.bloomRequests) - } -} - -func (b *EthAPIBackend) Engine() consensus.Engine { - return b.eth.engine -} - -func (b *EthAPIBackend) CurrentHeader() *types.Header { - return b.eth.blockchain.CurrentHeader() -} - -func (b *EthAPIBackend) Miner() *miner.Miner { - return b.eth.Miner() -} - -func (b *EthAPIBackend) StartMining() error { - return b.eth.StartMining() -} - -func (b *EthAPIBackend) StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, readOnly bool, preferDisk bool) (*state.StateDB, tracers.StateReleaseFunc, error) { - return b.eth.stateAtBlock(ctx, block, reexec, base, readOnly, preferDisk) -} - -func (b *EthAPIBackend) StateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (*core.Message, vm.BlockContext, *state.StateDB, tracers.StateReleaseFunc, error) { - return b.eth.stateAtTransaction(ctx, block, txIndex, reexec) -} diff --git a/eth/api_debug_test.go b/eth/api_debug_test.go deleted file mode 100644 index 184b90dd09..0000000000 --- a/eth/api_debug_test.go +++ /dev/null @@ -1,222 +0,0 @@ -// Copyright 2017 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 . - -package eth - -import ( - "bytes" - "fmt" - "math/big" - "reflect" - "strings" - "testing" - - "github.com/davecgh/go-spew/spew" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/state" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/trie" - "golang.org/x/exp/slices" -) - -var dumper = spew.ConfigState{Indent: " "} - -func accountRangeTest(t *testing.T, trie *state.Trie, statedb *state.StateDB, start common.Hash, requestedNum int, expectedNum int) state.Dump { - result := statedb.RawDump(&state.DumpConfig{ - SkipCode: true, - SkipStorage: true, - OnlyWithAddresses: false, - Start: start.Bytes(), - Max: uint64(requestedNum), - }) - - if len(result.Accounts) != expectedNum { - t.Fatalf("expected %d results, got %d", expectedNum, len(result.Accounts)) - } - for addr, acc := range result.Accounts { - if strings.HasSuffix(addr, "pre") || acc.Address == nil { - t.Fatalf("account without prestate (address) returned: %v", addr) - } - if !statedb.Exist(*acc.Address) { - t.Fatalf("account not found in state %s", acc.Address.Hex()) - } - } - return result -} - -func TestAccountRange(t *testing.T) { - t.Parallel() - - var ( - statedb = state.NewDatabaseWithConfig(rawdb.NewMemoryDatabase(), &trie.Config{Preimages: true}) - sdb, _ = state.New(types.EmptyRootHash, statedb, nil) - addrs = [AccountRangeMaxResults * 2]common.Address{} - m = map[common.Address]bool{} - ) - - for i := range addrs { - hash := common.HexToHash(fmt.Sprintf("%x", i)) - addr := common.BytesToAddress(crypto.Keccak256Hash(hash.Bytes()).Bytes()) - addrs[i] = addr - sdb.SetBalance(addrs[i], big.NewInt(1)) - if _, ok := m[addr]; ok { - t.Fatalf("bad") - } else { - m[addr] = true - } - } - root, _ := sdb.Commit(0, true) - sdb, _ = state.New(root, statedb, nil) - - trie, err := statedb.OpenTrie(root) - if err != nil { - t.Fatal(err) - } - accountRangeTest(t, &trie, sdb, common.Hash{}, AccountRangeMaxResults/2, AccountRangeMaxResults/2) - // test pagination - firstResult := accountRangeTest(t, &trie, sdb, common.Hash{}, AccountRangeMaxResults, AccountRangeMaxResults) - secondResult := accountRangeTest(t, &trie, sdb, common.BytesToHash(firstResult.Next), AccountRangeMaxResults, AccountRangeMaxResults) - - hList := make([]common.Hash, 0) - for addr1, acc := range firstResult.Accounts { - // If address is non-available, then it makes no sense to compare - // them as they might be two different accounts. - if acc.Address == nil { - continue - } - if _, duplicate := secondResult.Accounts[addr1]; duplicate { - t.Fatalf("pagination test failed: results should not overlap") - } - hList = append(hList, crypto.Keccak256Hash(acc.Address.Bytes())) - } - // Test to see if it's possible to recover from the middle of the previous - // set and get an even split between the first and second sets. - slices.SortFunc(hList, common.Hash.Cmp) - middleH := hList[AccountRangeMaxResults/2] - middleResult := accountRangeTest(t, &trie, sdb, middleH, AccountRangeMaxResults, AccountRangeMaxResults) - missing, infirst, insecond := 0, 0, 0 - for h := range middleResult.Accounts { - if _, ok := firstResult.Accounts[h]; ok { - infirst++ - } else if _, ok := secondResult.Accounts[h]; ok { - insecond++ - } else { - missing++ - } - } - if missing != 0 { - t.Fatalf("%d hashes in the 'middle' set were neither in the first not the second set", missing) - } - if infirst != AccountRangeMaxResults/2 { - t.Fatalf("Imbalance in the number of first-test results: %d != %d", infirst, AccountRangeMaxResults/2) - } - if insecond != AccountRangeMaxResults/2 { - t.Fatalf("Imbalance in the number of second-test results: %d != %d", insecond, AccountRangeMaxResults/2) - } -} - -func TestEmptyAccountRange(t *testing.T) { - t.Parallel() - - var ( - statedb = state.NewDatabase(rawdb.NewMemoryDatabase()) - st, _ = state.New(types.EmptyRootHash, statedb, nil) - ) - // Commit(although nothing to flush) and re-init the statedb - st.Commit(0, true) - st, _ = state.New(types.EmptyRootHash, statedb, nil) - - results := st.RawDump(&state.DumpConfig{ - SkipCode: true, - SkipStorage: true, - OnlyWithAddresses: true, - Max: uint64(AccountRangeMaxResults), - }) - if bytes.Equal(results.Next, (common.Hash{}).Bytes()) { - t.Fatalf("Empty results should not return a second page") - } - if len(results.Accounts) != 0 { - t.Fatalf("Empty state should not return addresses: %v", results.Accounts) - } -} - -func TestStorageRangeAt(t *testing.T) { - t.Parallel() - - // Create a state where account 0x010000... has a few storage entries. - var ( - db = state.NewDatabaseWithConfig(rawdb.NewMemoryDatabase(), &trie.Config{Preimages: true}) - sdb, _ = state.New(types.EmptyRootHash, db, nil) - addr = common.Address{0x01} - keys = []common.Hash{ // hashes of Keys of storage - common.HexToHash("340dd630ad21bf010b4e676dbfa9ba9a02175262d1fa356232cfde6cb5b47ef2"), - common.HexToHash("426fcb404ab2d5d8e61a3d918108006bbb0a9be65e92235bb10eefbdb6dcd053"), - common.HexToHash("48078cfed56339ea54962e72c37c7f588fc4f8e5bc173827ba75cb10a63a96a5"), - common.HexToHash("5723d2c3a83af9b735e3b7f21531e5623d183a9095a56604ead41f3582fdfb75"), - } - storage = storageMap{ - keys[0]: {Key: &common.Hash{0x02}, Value: common.Hash{0x01}}, - keys[1]: {Key: &common.Hash{0x04}, Value: common.Hash{0x02}}, - keys[2]: {Key: &common.Hash{0x01}, Value: common.Hash{0x03}}, - keys[3]: {Key: &common.Hash{0x03}, Value: common.Hash{0x04}}, - } - ) - for _, entry := range storage { - sdb.SetState(addr, *entry.Key, entry.Value) - } - root, _ := sdb.Commit(0, false) - sdb, _ = state.New(root, db, nil) - - // Check a few combinations of limit and start/end. - tests := []struct { - start []byte - limit int - want StorageRangeResult - }{ - { - start: []byte{}, limit: 0, - want: StorageRangeResult{storageMap{}, &keys[0]}, - }, - { - start: []byte{}, limit: 100, - want: StorageRangeResult{storage, nil}, - }, - { - start: []byte{}, limit: 2, - want: StorageRangeResult{storageMap{keys[0]: storage[keys[0]], keys[1]: storage[keys[1]]}, &keys[2]}, - }, - { - start: []byte{0x00}, limit: 4, - want: StorageRangeResult{storage, nil}, - }, - { - start: []byte{0x40}, limit: 2, - want: StorageRangeResult{storageMap{keys[1]: storage[keys[1]], keys[2]: storage[keys[2]]}, &keys[3]}, - }, - } - for _, test := range tests { - result, err := storageRangeAt(sdb, root, addr, test.start, test.limit) - if err != nil { - t.Error(err) - } - if !reflect.DeepEqual(result, test.want) { - t.Fatalf("wrong result for range %#x.., limit %d:\ngot %s\nwant %s", - test.start, test.limit, dumper.Sdump(result), dumper.Sdump(&test.want)) - } - } -} diff --git a/eth/api_miner.go b/eth/api_miner.go deleted file mode 100644 index 477531d494..0000000000 --- a/eth/api_miner.go +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2023 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 . - -package eth - -import ( - "math/big" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" -) - -// MinerAPI provides an API to control the miner. -type MinerAPI struct { - e *Ethereum -} - -// NewMinerAPI create a new MinerAPI instance. -func NewMinerAPI(e *Ethereum) *MinerAPI { - return &MinerAPI{e} -} - -// Start starts the miner with the given number of threads. If threads is nil, -// the number of workers started is equal to the number of logical CPUs that are -// usable by this process. If mining is already running, this method adjust the -// number of threads allowed to use and updates the minimum price required by the -// transaction pool. -func (api *MinerAPI) Start() error { - return api.e.StartMining() -} - -// Stop terminates the miner, both at the consensus engine level as well as at -// the block creation level. -func (api *MinerAPI) Stop() { - api.e.StopMining() -} - -// SetExtra sets the extra data string that is included when this miner mines a block. -func (api *MinerAPI) SetExtra(extra string) (bool, error) { - if err := api.e.Miner().SetExtra([]byte(extra)); err != nil { - return false, err - } - return true, nil -} - -// SetGasPrice sets the minimum accepted gas price for the miner. -func (api *MinerAPI) SetGasPrice(gasPrice hexutil.Big) bool { - api.e.lock.Lock() - api.e.gasPrice = (*big.Int)(&gasPrice) - api.e.lock.Unlock() - - api.e.txPool.SetGasTip((*big.Int)(&gasPrice)) - return true -} - -// SetGasLimit sets the gaslimit to target towards during mining. -func (api *MinerAPI) SetGasLimit(gasLimit hexutil.Uint64) bool { - api.e.Miner().SetGasCeil(uint64(gasLimit)) - return true -} - -// SetEtherbase sets the etherbase of the miner. -func (api *MinerAPI) SetEtherbase(etherbase common.Address) bool { - api.e.SetEtherbase(etherbase) - return true -} - -// SetRecommitInterval updates the interval for miner sealing work recommitting. -func (api *MinerAPI) SetRecommitInterval(interval int) { - api.e.Miner().SetRecommitInterval(time.Duration(interval) * time.Millisecond) -} diff --git a/eth/backend.go b/eth/backend.go deleted file mode 100644 index 774ffaf248..0000000000 --- a/eth/backend.go +++ /dev/null @@ -1,552 +0,0 @@ -// Copyright 2014 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 . - -// Package eth implements the Ethereum protocol. -package eth - -import ( - "errors" - "fmt" - "math/big" - "runtime" - "sync" - - "github.com/ethereum/go-ethereum/accounts" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/consensus" - "github.com/ethereum/go-ethereum/consensus/beacon" - "github.com/ethereum/go-ethereum/consensus/clique" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/bloombits" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/state/pruner" - "github.com/ethereum/go-ethereum/core/txpool" - "github.com/ethereum/go-ethereum/core/txpool/blobpool" - "github.com/ethereum/go-ethereum/core/txpool/legacypool" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/vm" - "github.com/ethereum/go-ethereum/eth/downloader" - "github.com/ethereum/go-ethereum/eth/ethconfig" - "github.com/ethereum/go-ethereum/eth/gasprice" - "github.com/ethereum/go-ethereum/eth/protocols/eth" - "github.com/ethereum/go-ethereum/eth/protocols/snap" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/event" - "github.com/ethereum/go-ethereum/internal/ethapi" - "github.com/ethereum/go-ethereum/internal/shutdowncheck" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/miner" - "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/p2p/dnsdisc" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/rlp" - "github.com/ethereum/go-ethereum/rpc" -) - -// Config contains the configuration options of the ETH protocol. -// Deprecated: use ethconfig.Config instead. -type Config = ethconfig.Config - -// Ethereum implements the Ethereum full node service. -type Ethereum struct { - config *ethconfig.Config - - // Handlers - txPool *txpool.TxPool - - blockchain *core.BlockChain - handler *handler - ethDialCandidates enode.Iterator - snapDialCandidates enode.Iterator - merger *consensus.Merger - - // DB interfaces - chainDb ethdb.Database // Block chain database - - eventMux *event.TypeMux - engine consensus.Engine - accountManager *accounts.Manager - - bloomRequests chan chan *bloombits.Retrieval // Channel receiving bloom data retrieval requests - bloomIndexer *core.ChainIndexer // Bloom indexer operating during block imports - closeBloomHandler chan struct{} - - APIBackend *EthAPIBackend - - miner *miner.Miner - gasPrice *big.Int - etherbase common.Address - - networkID uint64 - netRPCService *ethapi.NetAPI - - p2pServer *p2p.Server - - lock sync.RWMutex // Protects the variadic fields (e.g. gas price and etherbase) - - shutdownTracker *shutdowncheck.ShutdownTracker // Tracks if and when the node has shutdown ungracefully -} - -// New creates a new Ethereum object (including the -// initialisation of the common Ethereum object) -func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { - // Ensure configuration values are compatible and sane - if config.SyncMode == downloader.LightSync { - return nil, errors.New("can't run eth.Ethereum in light sync mode, light mode has been deprecated") - } - if !config.SyncMode.IsValid() { - return nil, fmt.Errorf("invalid sync mode %d", config.SyncMode) - } - if config.Miner.GasPrice == nil || config.Miner.GasPrice.Cmp(common.Big0) <= 0 { - log.Warn("Sanitizing invalid miner gas price", "provided", config.Miner.GasPrice, "updated", ethconfig.Defaults.Miner.GasPrice) - config.Miner.GasPrice = new(big.Int).Set(ethconfig.Defaults.Miner.GasPrice) - } - if config.NoPruning && config.TrieDirtyCache > 0 { - if config.SnapshotCache > 0 { - config.TrieCleanCache += config.TrieDirtyCache * 3 / 5 - config.SnapshotCache += config.TrieDirtyCache * 2 / 5 - } else { - config.TrieCleanCache += config.TrieDirtyCache - } - config.TrieDirtyCache = 0 - } - log.Info("Allocated trie memory caches", "clean", common.StorageSize(config.TrieCleanCache)*1024*1024, "dirty", common.StorageSize(config.TrieDirtyCache)*1024*1024) - - // Assemble the Ethereum object - chainDb, err := stack.OpenDatabaseWithFreezer("chaindata", config.DatabaseCache, config.DatabaseHandles, config.DatabaseFreezer, "eth/db/chaindata/", false) - if err != nil { - return nil, err - } - scheme, err := rawdb.ParseStateScheme(config.StateScheme, chainDb) - if err != nil { - return nil, err - } - // Try to recover offline state pruning only in hash-based. - if scheme == rawdb.HashScheme { - if err := pruner.RecoverPruning(stack.ResolvePath(""), chainDb); err != nil { - log.Error("Failed to recover state", "error", err) - } - } - // Transfer mining-related config to the ethash config. - chainConfig, err := core.LoadChainConfig(chainDb, config.Genesis) - if err != nil { - return nil, err - } - engine, err := ethconfig.CreateConsensusEngine(chainConfig, chainDb) - if err != nil { - return nil, err - } - networkID := config.NetworkId - if networkID == 0 { - networkID = chainConfig.ChainID.Uint64() - } - eth := &Ethereum{ - config: config, - merger: consensus.NewMerger(chainDb), - chainDb: chainDb, - eventMux: stack.EventMux(), - accountManager: stack.AccountManager(), - engine: engine, - closeBloomHandler: make(chan struct{}), - networkID: networkID, - gasPrice: config.Miner.GasPrice, - etherbase: config.Miner.Etherbase, - bloomRequests: make(chan chan *bloombits.Retrieval), - bloomIndexer: core.NewBloomIndexer(chainDb, params.BloomBitsBlocks, params.BloomConfirms), - p2pServer: stack.Server(), - shutdownTracker: shutdowncheck.NewShutdownTracker(chainDb), - } - bcVersion := rawdb.ReadDatabaseVersion(chainDb) - var dbVer = "" - if bcVersion != nil { - dbVer = fmt.Sprintf("%d", *bcVersion) - } - log.Info("Initialising Ethereum protocol", "network", networkID, "dbversion", dbVer) - - if !config.SkipBcVersionCheck { - if bcVersion != nil && *bcVersion > core.BlockChainVersion { - return nil, fmt.Errorf("database version is v%d, Geth %s only supports v%d", *bcVersion, params.VersionWithMeta, core.BlockChainVersion) - } else if bcVersion == nil || *bcVersion < core.BlockChainVersion { - if bcVersion != nil { // only print warning on upgrade, not on init - log.Warn("Upgrade blockchain database version", "from", dbVer, "to", core.BlockChainVersion) - } - rawdb.WriteDatabaseVersion(chainDb, core.BlockChainVersion) - } - } - var ( - vmConfig = vm.Config{ - EnablePreimageRecording: config.EnablePreimageRecording, - } - cacheConfig = &core.CacheConfig{ - TrieCleanLimit: config.TrieCleanCache, - TrieCleanNoPrefetch: config.NoPrefetch, - TrieDirtyLimit: config.TrieDirtyCache, - TrieDirtyDisabled: config.NoPruning, - TrieTimeLimit: config.TrieTimeout, - SnapshotLimit: config.SnapshotCache, - Preimages: config.Preimages, - StateHistory: config.StateHistory, - StateScheme: scheme, - } - ) - // Override the chain config with provided settings. - var overrides core.ChainOverrides - if config.OverrideCancun != nil { - overrides.OverrideCancun = config.OverrideCancun - } - if config.OverrideVerkle != nil { - overrides.OverrideVerkle = config.OverrideVerkle - } - eth.blockchain, err = core.NewBlockChain(chainDb, cacheConfig, config.Genesis, &overrides, eth.engine, vmConfig, eth.shouldPreserve, &config.TransactionHistory) - if err != nil { - return nil, err - } - eth.bloomIndexer.Start(eth.blockchain) - - if config.BlobPool.Datadir != "" { - config.BlobPool.Datadir = stack.ResolvePath(config.BlobPool.Datadir) - } - blobPool := blobpool.New(config.BlobPool, eth.blockchain) - - if config.TxPool.Journal != "" { - config.TxPool.Journal = stack.ResolvePath(config.TxPool.Journal) - } - legacyPool := legacypool.New(config.TxPool, eth.blockchain) - - eth.txPool, err = txpool.New(new(big.Int).SetUint64(config.TxPool.PriceLimit), eth.blockchain, []txpool.SubPool{legacyPool, blobPool}) - if err != nil { - return nil, err - } - // Permit the downloader to use the trie cache allowance during fast sync - cacheLimit := cacheConfig.TrieCleanLimit + cacheConfig.TrieDirtyLimit + cacheConfig.SnapshotLimit - if eth.handler, err = newHandler(&handlerConfig{ - Database: chainDb, - Chain: eth.blockchain, - TxPool: eth.txPool, - Merger: eth.merger, - Network: networkID, - Sync: config.SyncMode, - BloomCache: uint64(cacheLimit), - EventMux: eth.eventMux, - RequiredBlocks: config.RequiredBlocks, - }); err != nil { - return nil, err - } - - eth.miner = miner.New(eth, &config.Miner, eth.blockchain.Config(), eth.EventMux(), eth.engine, eth.isLocalBlock) - eth.miner.SetExtra(makeExtraData(config.Miner.ExtraData)) - - eth.APIBackend = &EthAPIBackend{stack.Config().ExtRPCEnabled(), stack.Config().AllowUnprotectedTxs, eth, nil} - if eth.APIBackend.allowUnprotectedTxs { - log.Info("Unprotected transactions allowed") - } - gpoParams := config.GPO - if gpoParams.Default == nil { - gpoParams.Default = config.Miner.GasPrice - } - eth.APIBackend.gpo = gasprice.NewOracle(eth.APIBackend, gpoParams) - - // Setup DNS discovery iterators. - dnsclient := dnsdisc.NewClient(dnsdisc.Config{}) - eth.ethDialCandidates, err = dnsclient.NewIterator(eth.config.EthDiscoveryURLs...) - if err != nil { - return nil, err - } - eth.snapDialCandidates, err = dnsclient.NewIterator(eth.config.SnapDiscoveryURLs...) - if err != nil { - return nil, err - } - - // Start the RPC service - eth.netRPCService = ethapi.NewNetAPI(eth.p2pServer, networkID) - - // Register the backend on the node - stack.RegisterAPIs(eth.APIs()) - stack.RegisterProtocols(eth.Protocols()) - stack.RegisterLifecycle(eth) - - // Successful startup; push a marker and check previous unclean shutdowns. - eth.shutdownTracker.MarkStartup() - - return eth, nil -} - -func makeExtraData(extra []byte) []byte { - if len(extra) == 0 { - // create default extradata - extra, _ = rlp.EncodeToBytes([]interface{}{ - uint(params.VersionMajor<<16 | params.VersionMinor<<8 | params.VersionPatch), - "geth", - runtime.Version(), - runtime.GOOS, - }) - } - if uint64(len(extra)) > params.MaximumExtraDataSize { - log.Warn("Miner extra data exceed limit", "extra", hexutil.Bytes(extra), "limit", params.MaximumExtraDataSize) - extra = nil - } - return extra -} - -// APIs return the collection of RPC services the ethereum package offers. -// NOTE, some of these services probably need to be moved to somewhere else. -func (s *Ethereum) APIs() []rpc.API { - apis := ethapi.GetAPIs(s.APIBackend) - - // Append any APIs exposed explicitly by the consensus engine - apis = append(apis, s.engine.APIs(s.BlockChain())...) - - // Append all the local APIs and return - return append(apis, []rpc.API{ - { - Namespace: "eth", - Service: NewEthereumAPI(s), - }, { - Namespace: "miner", - Service: NewMinerAPI(s), - }, { - Namespace: "eth", - Service: downloader.NewDownloaderAPI(s.handler.downloader, s.eventMux), - }, { - Namespace: "admin", - Service: NewAdminAPI(s), - }, { - Namespace: "debug", - Service: NewDebugAPI(s), - }, { - Namespace: "net", - Service: s.netRPCService, - }, - }...) -} - -func (s *Ethereum) ResetWithGenesisBlock(gb *types.Block) { - s.blockchain.ResetWithGenesisBlock(gb) -} - -func (s *Ethereum) Etherbase() (eb common.Address, err error) { - s.lock.RLock() - etherbase := s.etherbase - s.lock.RUnlock() - - if etherbase != (common.Address{}) { - return etherbase, nil - } - return common.Address{}, errors.New("etherbase must be explicitly specified") -} - -// isLocalBlock checks whether the specified block is mined -// by local miner accounts. -// -// We regard two types of accounts as local miner account: etherbase -// and accounts specified via `txpool.locals` flag. -func (s *Ethereum) isLocalBlock(header *types.Header) bool { - author, err := s.engine.Author(header) - if err != nil { - log.Warn("Failed to retrieve block author", "number", header.Number.Uint64(), "hash", header.Hash(), "err", err) - return false - } - // Check whether the given address is etherbase. - s.lock.RLock() - etherbase := s.etherbase - s.lock.RUnlock() - if author == etherbase { - return true - } - // Check whether the given address is specified by `txpool.local` - // CLI flag. - for _, account := range s.config.TxPool.Locals { - if account == author { - return true - } - } - return false -} - -// shouldPreserve checks whether we should preserve the given block -// during the chain reorg depending on whether the author of block -// is a local account. -func (s *Ethereum) shouldPreserve(header *types.Header) bool { - // The reason we need to disable the self-reorg preserving for clique - // is it can be probable to introduce a deadlock. - // - // e.g. If there are 7 available signers - // - // r1 A - // r2 B - // r3 C - // r4 D - // r5 A [X] F G - // r6 [X] - // - // In the round5, the in-turn signer E is offline, so the worst case - // is A, F and G sign the block of round5 and reject the block of opponents - // and in the round6, the last available signer B is offline, the whole - // network is stuck. - if _, ok := s.engine.(*clique.Clique); ok { - return false - } - return s.isLocalBlock(header) -} - -// SetEtherbase sets the mining reward address. -func (s *Ethereum) SetEtherbase(etherbase common.Address) { - s.lock.Lock() - s.etherbase = etherbase - s.lock.Unlock() - - s.miner.SetEtherbase(etherbase) -} - -// StartMining starts the miner with the given number of CPU threads. If mining -// is already running, this method adjust the number of threads allowed to use -// and updates the minimum price required by the transaction pool. -func (s *Ethereum) StartMining() error { - // If the miner was not running, initialize it - if !s.IsMining() { - // Propagate the initial price point to the transaction pool - s.lock.RLock() - price := s.gasPrice - s.lock.RUnlock() - s.txPool.SetGasTip(price) - - // Configure the local mining address - eb, err := s.Etherbase() - if err != nil { - log.Error("Cannot start mining without etherbase", "err", err) - return fmt.Errorf("etherbase missing: %v", err) - } - var cli *clique.Clique - if c, ok := s.engine.(*clique.Clique); ok { - cli = c - } else if cl, ok := s.engine.(*beacon.Beacon); ok { - if c, ok := cl.InnerEngine().(*clique.Clique); ok { - cli = c - } - } - if cli != nil { - wallet, err := s.accountManager.Find(accounts.Account{Address: eb}) - if wallet == nil || err != nil { - log.Error("Etherbase account unavailable locally", "err", err) - return fmt.Errorf("signer missing: %v", err) - } - cli.Authorize(eb, wallet.SignData) - } - // If mining is started, we can disable the transaction rejection mechanism - // introduced to speed sync times. - s.handler.enableSyncedFeatures() - - go s.miner.Start() - } - return nil -} - -// StopMining terminates the miner, both at the consensus engine level as well as -// at the block creation level. -func (s *Ethereum) StopMining() { - // Update the thread count within the consensus engine - type threaded interface { - SetThreads(threads int) - } - if th, ok := s.engine.(threaded); ok { - th.SetThreads(-1) - } - // Stop the block creating itself - s.miner.Stop() -} - -func (s *Ethereum) IsMining() bool { return s.miner.Mining() } -func (s *Ethereum) Miner() *miner.Miner { return s.miner } - -func (s *Ethereum) AccountManager() *accounts.Manager { return s.accountManager } -func (s *Ethereum) BlockChain() *core.BlockChain { return s.blockchain } -func (s *Ethereum) TxPool() *txpool.TxPool { return s.txPool } -func (s *Ethereum) EventMux() *event.TypeMux { return s.eventMux } -func (s *Ethereum) Engine() consensus.Engine { return s.engine } -func (s *Ethereum) ChainDb() ethdb.Database { return s.chainDb } -func (s *Ethereum) IsListening() bool { return true } // Always listening -func (s *Ethereum) Downloader() *downloader.Downloader { return s.handler.downloader } -func (s *Ethereum) Synced() bool { return s.handler.synced.Load() } -func (s *Ethereum) SetSynced() { s.handler.enableSyncedFeatures() } -func (s *Ethereum) ArchiveMode() bool { return s.config.NoPruning } -func (s *Ethereum) BloomIndexer() *core.ChainIndexer { return s.bloomIndexer } -func (s *Ethereum) Merger() *consensus.Merger { return s.merger } -func (s *Ethereum) SyncMode() downloader.SyncMode { - mode, _ := s.handler.chainSync.modeAndLocalHead() - return mode -} - -// Protocols returns all the currently configured -// network protocols to start. -func (s *Ethereum) Protocols() []p2p.Protocol { - protos := eth.MakeProtocols((*ethHandler)(s.handler), s.networkID, s.ethDialCandidates) - if s.config.SnapshotCache > 0 { - protos = append(protos, snap.MakeProtocols((*snapHandler)(s.handler), s.snapDialCandidates)...) - } - return protos -} - -// Start implements node.Lifecycle, starting all internal goroutines needed by the -// Ethereum protocol implementation. -func (s *Ethereum) Start() error { - eth.StartENRUpdater(s.blockchain, s.p2pServer.LocalNode()) - - // Start the bloom bits servicing goroutines - s.startBloomHandlers(params.BloomBitsBlocks) - - // Regularly update shutdown marker - s.shutdownTracker.Start() - - // Figure out a max peers count based on the server limits - maxPeers := s.p2pServer.MaxPeers - if s.config.LightServ > 0 { - if s.config.LightPeers >= s.p2pServer.MaxPeers { - return fmt.Errorf("invalid peer config: light peer count (%d) >= total peer count (%d)", s.config.LightPeers, s.p2pServer.MaxPeers) - } - maxPeers -= s.config.LightPeers - } - // Start the networking layer and the light server if requested - s.handler.Start(maxPeers) - return nil -} - -// Stop implements node.Lifecycle, terminating all internal goroutines used by the -// Ethereum protocol. -func (s *Ethereum) Stop() error { - // Stop all the peer-related stuff first. - s.ethDialCandidates.Close() - s.snapDialCandidates.Close() - s.handler.Stop() - - // Then stop everything else. - s.bloomIndexer.Close() - close(s.closeBloomHandler) - s.txPool.Close() - s.miner.Close() - s.blockchain.Stop() - s.engine.Close() - - // Clean shutdown marker as the last thing before closing db - s.shutdownTracker.Stop() - - s.chainDb.Close() - s.eventMux.Stop() - - return nil -} diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go deleted file mode 100644 index 37b0248f28..0000000000 --- a/eth/catalyst/api.go +++ /dev/null @@ -1,837 +0,0 @@ -// Copyright 2021 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 . - -// Package catalyst implements the temporary eth1/eth2 RPC integration. -package catalyst - -import ( - "errors" - "fmt" - "math/big" - "sync" - "time" - - "github.com/ethereum/go-ethereum/beacon/engine" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/eth" - "github.com/ethereum/go-ethereum/eth/downloader" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/miner" - "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/rpc" -) - -// Register adds the engine API to the full node. -func Register(stack *node.Node, backend *eth.Ethereum) error { - log.Warn("Engine API enabled", "protocol", "eth") - stack.RegisterAPIs([]rpc.API{ - { - Namespace: "engine", - Service: NewConsensusAPI(backend), - Authenticated: true, - }, - }) - return nil -} - -const ( - // invalidBlockHitEviction is the number of times an invalid block can be - // referenced in forkchoice update or new payload before it is attempted - // to be reprocessed again. - invalidBlockHitEviction = 128 - - // invalidTipsetsCap is the max number of recent block hashes tracked that - // have lead to some bad ancestor block. It's just an OOM protection. - invalidTipsetsCap = 512 - - // beaconUpdateStartupTimeout is the time to wait for a beacon client to get - // attached before starting to issue warnings. - beaconUpdateStartupTimeout = 30 * time.Second - - // beaconUpdateConsensusTimeout is the max time allowed for a beacon client - // to send a consensus update before it's considered offline and the user is - // warned. - beaconUpdateConsensusTimeout = 2 * time.Minute - - // beaconUpdateWarnFrequency is the frequency at which to warn the user that - // the beacon client is offline. - beaconUpdateWarnFrequency = 5 * time.Minute -) - -// All methods provided over the engine endpoint. -var caps = []string{ - "engine_forkchoiceUpdatedV1", - "engine_forkchoiceUpdatedV2", - "engine_forkchoiceUpdatedV3", - "engine_exchangeTransitionConfigurationV1", - "engine_getPayloadV1", - "engine_getPayloadV2", - "engine_getPayloadV3", - "engine_newPayloadV1", - "engine_newPayloadV2", - "engine_newPayloadV3", - "engine_getPayloadBodiesByHashV1", - "engine_getPayloadBodiesByRangeV1", -} - -type ConsensusAPI struct { - eth *eth.Ethereum - - remoteBlocks *headerQueue // Cache of remote payloads received - localBlocks *payloadQueue // Cache of local payloads generated - - // The forkchoice update and new payload method require us to return the - // latest valid hash in an invalid chain. To support that return, we need - // to track historical bad blocks as well as bad tipsets in case a chain - // is constantly built on it. - // - // There are a few important caveats in this mechanism: - // - The bad block tracking is ephemeral, in-memory only. We must never - // persist any bad block information to disk as a bug in Geth could end - // up blocking a valid chain, even if a later Geth update would accept - // it. - // - Bad blocks will get forgotten after a certain threshold of import - // attempts and will be retried. The rationale is that if the network - // really-really-really tries to feed us a block, we should give it a - // new chance, perhaps us being racey instead of the block being legit - // bad (this happened in Geth at a point with import vs. pending race). - // - Tracking all the blocks built on top of the bad one could be a bit - // problematic, so we will only track the head chain segment of a bad - // chain to allow discarding progressing bad chains and side chains, - // without tracking too much bad data. - invalidBlocksHits map[common.Hash]int // Ephemeral cache to track invalid blocks and their hit count - invalidTipsets map[common.Hash]*types.Header // Ephemeral cache to track invalid tipsets and their bad ancestor - invalidLock sync.Mutex // Protects the invalid maps from concurrent access - - // Geth can appear to be stuck or do strange things if the beacon client is - // offline or is sending us strange data. Stash some update stats away so - // that we can warn the user and not have them open issues on our tracker. - lastTransitionUpdate time.Time - lastTransitionLock sync.Mutex - lastForkchoiceUpdate time.Time - lastForkchoiceLock sync.Mutex - lastNewPayloadUpdate time.Time - lastNewPayloadLock sync.Mutex - - forkchoiceLock sync.Mutex // Lock for the forkChoiceUpdated method - newPayloadLock sync.Mutex // Lock for the NewPayload method -} - -// NewConsensusAPI creates a new consensus api for the given backend. -// The underlying blockchain needs to have a valid terminal total difficulty set. -func NewConsensusAPI(eth *eth.Ethereum) *ConsensusAPI { - api := newConsensusAPIWithoutHeartbeat(eth) - go api.heartbeat() - return api -} - -// newConsensusAPIWithoutHeartbeat creates a new consensus api for the SimulatedBeacon Node. -func newConsensusAPIWithoutHeartbeat(eth *eth.Ethereum) *ConsensusAPI { - if eth.BlockChain().Config().TerminalTotalDifficulty == nil { - log.Warn("Engine API started but chain not configured for merge yet") - } - api := &ConsensusAPI{ - eth: eth, - remoteBlocks: newHeaderQueue(), - localBlocks: newPayloadQueue(), - invalidBlocksHits: make(map[common.Hash]int), - invalidTipsets: make(map[common.Hash]*types.Header), - } - eth.Downloader().SetBadBlockCallback(api.setInvalidAncestor) - return api -} - -// ForkchoiceUpdatedV1 has several responsibilities: -// -// We try to set our blockchain to the headBlock. -// -// If the method is called with an empty head block: we return success, which can be used -// to check if the engine API is enabled. -// -// If the total difficulty was not reached: we return INVALID. -// -// If the finalizedBlockHash is set: we check if we have the finalizedBlockHash in our db, -// if not we start a sync. -// -// If there are payloadAttributes: we try to assemble a block with the payloadAttributes -// and return its payloadID. -func (api *ConsensusAPI) ForkchoiceUpdatedV1(update engine.ForkchoiceStateV1, payloadAttributes *engine.PayloadAttributes) (engine.ForkChoiceResponse, error) { - if payloadAttributes != nil { - if payloadAttributes.Withdrawals != nil { - return engine.STATUS_INVALID, engine.InvalidParams.With(errors.New("withdrawals not supported in V1")) - } - if api.eth.BlockChain().Config().IsShanghai(api.eth.BlockChain().Config().LondonBlock, payloadAttributes.Timestamp) { - return engine.STATUS_INVALID, engine.InvalidParams.With(errors.New("forkChoiceUpdateV1 called post-shanghai")) - } - } - return api.forkchoiceUpdated(update, payloadAttributes) -} - -// ForkchoiceUpdatedV2 is equivalent to V1 with the addition of withdrawals in the payload attributes. -func (api *ConsensusAPI) ForkchoiceUpdatedV2(update engine.ForkchoiceStateV1, payloadAttributes *engine.PayloadAttributes) (engine.ForkChoiceResponse, error) { - if payloadAttributes != nil { - if err := api.verifyPayloadAttributes(payloadAttributes); err != nil { - return engine.STATUS_INVALID, engine.InvalidParams.With(err) - } - } - return api.forkchoiceUpdated(update, payloadAttributes) -} - -// ForkchoiceUpdatedV3 is equivalent to V2 with the addition of parent beacon block root in the payload attributes. -func (api *ConsensusAPI) ForkchoiceUpdatedV3(update engine.ForkchoiceStateV1, payloadAttributes *engine.PayloadAttributes) (engine.ForkChoiceResponse, error) { - if payloadAttributes != nil { - if err := api.verifyPayloadAttributes(payloadAttributes); err != nil { - return engine.STATUS_INVALID, engine.InvalidParams.With(err) - } - } - return api.forkchoiceUpdated(update, payloadAttributes) -} - -func (api *ConsensusAPI) verifyPayloadAttributes(attr *engine.PayloadAttributes) error { - c := api.eth.BlockChain().Config() - - // Verify withdrawals attribute for Shanghai. - if err := checkAttribute(c.IsShanghai, attr.Withdrawals != nil, c.LondonBlock, attr.Timestamp); err != nil { - return fmt.Errorf("invalid withdrawals: %w", err) - } - // Verify beacon root attribute for Cancun. - if err := checkAttribute(c.IsCancun, attr.BeaconRoot != nil, c.LondonBlock, attr.Timestamp); err != nil { - return fmt.Errorf("invalid parent beacon block root: %w", err) - } - return nil -} - -func checkAttribute(active func(*big.Int, uint64) bool, exists bool, block *big.Int, time uint64) error { - if active(block, time) && !exists { - return errors.New("fork active, missing expected attribute") - } - if !active(block, time) && exists { - return errors.New("fork inactive, unexpected attribute set") - } - return nil -} - -func (api *ConsensusAPI) forkchoiceUpdated(update engine.ForkchoiceStateV1, payloadAttributes *engine.PayloadAttributes) (engine.ForkChoiceResponse, error) { - api.forkchoiceLock.Lock() - defer api.forkchoiceLock.Unlock() - - log.Trace("Engine API request received", "method", "ForkchoiceUpdated", "head", update.HeadBlockHash, "finalized", update.FinalizedBlockHash, "safe", update.SafeBlockHash) - if update.HeadBlockHash == (common.Hash{}) { - log.Warn("Forkchoice requested update to zero hash") - return engine.STATUS_INVALID, nil // TODO(karalabe): Why does someone send us this? - } - // Stash away the last update to warn the user if the beacon client goes offline - api.lastForkchoiceLock.Lock() - api.lastForkchoiceUpdate = time.Now() - api.lastForkchoiceLock.Unlock() - - // Check whether we have the block yet in our database or not. If not, we'll - // need to either trigger a sync, or to reject this forkchoice update for a - // reason. - block := api.eth.BlockChain().GetBlockByHash(update.HeadBlockHash) - if block == nil { - // If this block was previously invalidated, keep rejecting it here too - if res := api.checkInvalidAncestor(update.HeadBlockHash, update.HeadBlockHash); res != nil { - return engine.ForkChoiceResponse{PayloadStatus: *res, PayloadID: nil}, nil - } - // If the head hash is unknown (was not given to us in a newPayload request), - // we cannot resolve the header, so not much to do. This could be extended in - // the future to resolve from the `eth` network, but it's an unexpected case - // that should be fixed, not papered over. - header := api.remoteBlocks.get(update.HeadBlockHash) - if header == nil { - log.Warn("Forkchoice requested unknown head", "hash", update.HeadBlockHash) - return engine.STATUS_SYNCING, nil - } - // If the finalized hash is known, we can direct the downloader to move - // potentially more data to the freezer from the get go. - finalized := api.remoteBlocks.get(update.FinalizedBlockHash) - - // Header advertised via a past newPayload request. Start syncing to it. - // Before we do however, make sure any legacy sync in switched off so we - // don't accidentally have 2 cycles running. - if merger := api.eth.Merger(); !merger.TDDReached() { - merger.ReachTTD() - api.eth.Downloader().Cancel() - } - context := []interface{}{"number", header.Number, "hash", header.Hash()} - if update.FinalizedBlockHash != (common.Hash{}) { - if finalized == nil { - context = append(context, []interface{}{"finalized", "unknown"}...) - } else { - context = append(context, []interface{}{"finalized", finalized.Number}...) - } - } - log.Info("Forkchoice requested sync to new head", context...) - if err := api.eth.Downloader().BeaconSync(api.eth.SyncMode(), header, finalized); err != nil { - return engine.STATUS_SYNCING, err - } - return engine.STATUS_SYNCING, nil - } - // Block is known locally, just sanity check that the beacon client does not - // attempt to push us back to before the merge. - if block.Difficulty().BitLen() > 0 || block.NumberU64() == 0 { - var ( - td = api.eth.BlockChain().GetTd(update.HeadBlockHash, block.NumberU64()) - ptd = api.eth.BlockChain().GetTd(block.ParentHash(), block.NumberU64()-1) - ttd = api.eth.BlockChain().Config().TerminalTotalDifficulty - ) - if td == nil || (block.NumberU64() > 0 && ptd == nil) { - log.Error("TDs unavailable for TTD check", "number", block.NumberU64(), "hash", update.HeadBlockHash, "td", td, "parent", block.ParentHash(), "ptd", ptd) - return engine.STATUS_INVALID, errors.New("TDs unavailable for TDD check") - } - if td.Cmp(ttd) < 0 { - log.Error("Refusing beacon update to pre-merge", "number", block.NumberU64(), "hash", update.HeadBlockHash, "diff", block.Difficulty(), "age", common.PrettyAge(time.Unix(int64(block.Time()), 0))) - return engine.ForkChoiceResponse{PayloadStatus: engine.INVALID_TERMINAL_BLOCK, PayloadID: nil}, nil - } - if block.NumberU64() > 0 && ptd.Cmp(ttd) >= 0 { - log.Error("Parent block is already post-ttd", "number", block.NumberU64(), "hash", update.HeadBlockHash, "diff", block.Difficulty(), "age", common.PrettyAge(time.Unix(int64(block.Time()), 0))) - return engine.ForkChoiceResponse{PayloadStatus: engine.INVALID_TERMINAL_BLOCK, PayloadID: nil}, nil - } - } - valid := func(id *engine.PayloadID) engine.ForkChoiceResponse { - return engine.ForkChoiceResponse{ - PayloadStatus: engine.PayloadStatusV1{Status: engine.VALID, LatestValidHash: &update.HeadBlockHash}, - PayloadID: id, - } - } - if rawdb.ReadCanonicalHash(api.eth.ChainDb(), block.NumberU64()) != update.HeadBlockHash { - // Block is not canonical, set head. - if latestValid, err := api.eth.BlockChain().SetCanonical(block); err != nil { - return engine.ForkChoiceResponse{PayloadStatus: engine.PayloadStatusV1{Status: engine.INVALID, LatestValidHash: &latestValid}}, err - } - } else if api.eth.BlockChain().CurrentBlock().Hash() == update.HeadBlockHash { - // If the specified head matches with our local head, do nothing and keep - // generating the payload. It's a special corner case that a few slots are - // missing and we are requested to generate the payload in slot. - } else { - // If the head block is already in our canonical chain, the beacon client is - // probably resyncing. Ignore the update. - log.Info("Ignoring beacon update to old head", "number", block.NumberU64(), "hash", update.HeadBlockHash, "age", common.PrettyAge(time.Unix(int64(block.Time()), 0)), "have", api.eth.BlockChain().CurrentBlock().Number) - return valid(nil), nil - } - api.eth.SetSynced() - - // If the beacon client also advertised a finalized block, mark the local - // chain final and completely in PoS mode. - if update.FinalizedBlockHash != (common.Hash{}) { - if merger := api.eth.Merger(); !merger.PoSFinalized() { - merger.FinalizePoS() - } - // If the finalized block is not in our canonical tree, somethings wrong - finalBlock := api.eth.BlockChain().GetBlockByHash(update.FinalizedBlockHash) - if finalBlock == nil { - log.Warn("Final block not available in database", "hash", update.FinalizedBlockHash) - return engine.STATUS_INVALID, engine.InvalidForkChoiceState.With(errors.New("final block not available in database")) - } else if rawdb.ReadCanonicalHash(api.eth.ChainDb(), finalBlock.NumberU64()) != update.FinalizedBlockHash { - log.Warn("Final block not in canonical chain", "number", block.NumberU64(), "hash", update.HeadBlockHash) - return engine.STATUS_INVALID, engine.InvalidForkChoiceState.With(errors.New("final block not in canonical chain")) - } - // Set the finalized block - api.eth.BlockChain().SetFinalized(finalBlock.Header()) - } - // Check if the safe block hash is in our canonical tree, if not somethings wrong - if update.SafeBlockHash != (common.Hash{}) { - safeBlock := api.eth.BlockChain().GetBlockByHash(update.SafeBlockHash) - if safeBlock == nil { - log.Warn("Safe block not available in database") - return engine.STATUS_INVALID, engine.InvalidForkChoiceState.With(errors.New("safe block not available in database")) - } - if rawdb.ReadCanonicalHash(api.eth.ChainDb(), safeBlock.NumberU64()) != update.SafeBlockHash { - log.Warn("Safe block not in canonical chain") - return engine.STATUS_INVALID, engine.InvalidForkChoiceState.With(errors.New("safe block not in canonical chain")) - } - // Set the safe block - api.eth.BlockChain().SetSafe(safeBlock.Header()) - } - // If payload generation was requested, create a new block to be potentially - // sealed by the beacon client. The payload will be requested later, and we - // will replace it arbitrarily many times in between. - if payloadAttributes != nil { - args := &miner.BuildPayloadArgs{ - Parent: update.HeadBlockHash, - Timestamp: payloadAttributes.Timestamp, - FeeRecipient: payloadAttributes.SuggestedFeeRecipient, - Random: payloadAttributes.Random, - Withdrawals: payloadAttributes.Withdrawals, - BeaconRoot: payloadAttributes.BeaconRoot, - } - id := args.Id() - // If we already are busy generating this work, then we do not need - // to start a second process. - if api.localBlocks.has(id) { - return valid(&id), nil - } - payload, err := api.eth.Miner().BuildPayload(args) - if err != nil { - log.Error("Failed to build payload", "err", err) - return valid(nil), engine.InvalidPayloadAttributes.With(err) - } - api.localBlocks.put(id, payload) - return valid(&id), nil - } - return valid(nil), nil -} - -// ExchangeTransitionConfigurationV1 checks the given configuration against -// the configuration of the node. -func (api *ConsensusAPI) ExchangeTransitionConfigurationV1(config engine.TransitionConfigurationV1) (*engine.TransitionConfigurationV1, error) { - log.Trace("Engine API request received", "method", "ExchangeTransitionConfiguration", "ttd", config.TerminalTotalDifficulty) - if config.TerminalTotalDifficulty == nil { - return nil, errors.New("invalid terminal total difficulty") - } - // Stash away the last update to warn the user if the beacon client goes offline - api.lastTransitionLock.Lock() - api.lastTransitionUpdate = time.Now() - api.lastTransitionLock.Unlock() - - ttd := api.eth.BlockChain().Config().TerminalTotalDifficulty - if ttd == nil || ttd.Cmp(config.TerminalTotalDifficulty.ToInt()) != 0 { - log.Warn("Invalid TTD configured", "geth", ttd, "beacon", config.TerminalTotalDifficulty) - return nil, fmt.Errorf("invalid ttd: execution %v consensus %v", ttd, config.TerminalTotalDifficulty) - } - if config.TerminalBlockHash != (common.Hash{}) { - if hash := api.eth.BlockChain().GetCanonicalHash(uint64(config.TerminalBlockNumber)); hash == config.TerminalBlockHash { - return &engine.TransitionConfigurationV1{ - TerminalTotalDifficulty: (*hexutil.Big)(ttd), - TerminalBlockHash: config.TerminalBlockHash, - TerminalBlockNumber: config.TerminalBlockNumber, - }, nil - } - return nil, errors.New("invalid terminal block hash") - } - return &engine.TransitionConfigurationV1{TerminalTotalDifficulty: (*hexutil.Big)(ttd)}, nil -} - -// GetPayloadV1 returns a cached payload by id. -func (api *ConsensusAPI) GetPayloadV1(payloadID engine.PayloadID) (*engine.ExecutableData, error) { - data, err := api.getPayload(payloadID, false) - if err != nil { - return nil, err - } - return data.ExecutionPayload, nil -} - -// GetPayloadV2 returns a cached payload by id. -func (api *ConsensusAPI) GetPayloadV2(payloadID engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error) { - return api.getPayload(payloadID, false) -} - -// GetPayloadV3 returns a cached payload by id. -func (api *ConsensusAPI) GetPayloadV3(payloadID engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error) { - return api.getPayload(payloadID, false) -} - -func (api *ConsensusAPI) getPayload(payloadID engine.PayloadID, full bool) (*engine.ExecutionPayloadEnvelope, error) { - log.Trace("Engine API request received", "method", "GetPayload", "id", payloadID) - data := api.localBlocks.get(payloadID, full) - if data == nil { - return nil, engine.UnknownPayload - } - return data, nil -} - -// NewPayloadV1 creates an Eth1 block, inserts it in the chain, and returns the status of the chain. -func (api *ConsensusAPI) NewPayloadV1(params engine.ExecutableData) (engine.PayloadStatusV1, error) { - if params.Withdrawals != nil { - return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("withdrawals not supported in V1")) - } - return api.newPayload(params, nil, nil) -} - -// NewPayloadV2 creates an Eth1 block, inserts it in the chain, and returns the status of the chain. -func (api *ConsensusAPI) NewPayloadV2(params engine.ExecutableData) (engine.PayloadStatusV1, error) { - if api.eth.BlockChain().Config().IsShanghai(new(big.Int).SetUint64(params.Number), params.Timestamp) { - if params.Withdrawals == nil { - return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("nil withdrawals post-shanghai")) - } - } else if params.Withdrawals != nil { - return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("non-nil withdrawals pre-shanghai")) - } - if api.eth.BlockChain().Config().IsCancun(new(big.Int).SetUint64(params.Number), params.Timestamp) { - return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("newPayloadV2 called post-cancun")) - } - return api.newPayload(params, nil, nil) -} - -// NewPayloadV3 creates an Eth1 block, inserts it in the chain, and returns the status of the chain. -func (api *ConsensusAPI) NewPayloadV3(params engine.ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash) (engine.PayloadStatusV1, error) { - if params.ExcessBlobGas == nil { - return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("nil excessBlobGas post-cancun")) - } - if params.BlobGasUsed == nil { - return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("nil params.BlobGasUsed post-cancun")) - } - if versionedHashes == nil { - return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("nil versionedHashes post-cancun")) - } - if beaconRoot == nil { - return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("nil parentBeaconBlockRoot post-cancun")) - } - - if !api.eth.BlockChain().Config().IsCancun(new(big.Int).SetUint64(params.Number), params.Timestamp) { - return engine.PayloadStatusV1{Status: engine.INVALID}, engine.UnsupportedFork.With(errors.New("newPayloadV3 called pre-cancun")) - } - - return api.newPayload(params, versionedHashes, beaconRoot) -} - -func (api *ConsensusAPI) newPayload(params engine.ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash) (engine.PayloadStatusV1, error) { - // The locking here is, strictly, not required. Without these locks, this can happen: - // - // 1. NewPayload( execdata-N ) is invoked from the CL. It goes all the way down to - // api.eth.BlockChain().InsertBlockWithoutSetHead, where it is blocked on - // e.g database compaction. - // 2. The call times out on the CL layer, which issues another NewPayload (execdata-N) call. - // Similarly, this also get stuck on the same place. Importantly, since the - // first call has not gone through, the early checks for "do we already have this block" - // will all return false. - // 3. When the db compaction ends, then N calls inserting the same payload are processed - // sequentially. - // Hence, we use a lock here, to be sure that the previous call has finished before we - // check whether we already have the block locally. - api.newPayloadLock.Lock() - defer api.newPayloadLock.Unlock() - - log.Trace("Engine API request received", "method", "NewPayload", "number", params.Number, "hash", params.BlockHash) - block, err := engine.ExecutableDataToBlock(params, versionedHashes, beaconRoot) - if err != nil { - log.Warn("Invalid NewPayload params", "params", params, "error", err) - return api.invalid(err, nil), nil - } - // Stash away the last update to warn the user if the beacon client goes offline - api.lastNewPayloadLock.Lock() - api.lastNewPayloadUpdate = time.Now() - api.lastNewPayloadLock.Unlock() - - // If we already have the block locally, ignore the entire execution and just - // return a fake success. - if block := api.eth.BlockChain().GetBlockByHash(params.BlockHash); block != nil { - log.Warn("Ignoring already known beacon payload", "number", params.Number, "hash", params.BlockHash, "age", common.PrettyAge(time.Unix(int64(block.Time()), 0))) - hash := block.Hash() - return engine.PayloadStatusV1{Status: engine.VALID, LatestValidHash: &hash}, nil - } - // If this block was rejected previously, keep rejecting it - if res := api.checkInvalidAncestor(block.Hash(), block.Hash()); res != nil { - return *res, nil - } - // If the parent is missing, we - in theory - could trigger a sync, but that - // would also entail a reorg. That is problematic if multiple sibling blocks - // are being fed to us, and even more so, if some semi-distant uncle shortens - // our live chain. As such, payload execution will not permit reorgs and thus - // will not trigger a sync cycle. That is fine though, if we get a fork choice - // update after legit payload executions. - parent := api.eth.BlockChain().GetBlock(block.ParentHash(), block.NumberU64()-1) - if parent == nil { - return api.delayPayloadImport(block) - } - // We have an existing parent, do some sanity checks to avoid the beacon client - // triggering too early - var ( - ptd = api.eth.BlockChain().GetTd(parent.Hash(), parent.NumberU64()) - ttd = api.eth.BlockChain().Config().TerminalTotalDifficulty - gptd = api.eth.BlockChain().GetTd(parent.ParentHash(), parent.NumberU64()-1) - ) - if ptd.Cmp(ttd) < 0 { - log.Warn("Ignoring pre-merge payload", "number", params.Number, "hash", params.BlockHash, "td", ptd, "ttd", ttd) - return engine.INVALID_TERMINAL_BLOCK, nil - } - if parent.Difficulty().BitLen() > 0 && gptd != nil && gptd.Cmp(ttd) >= 0 { - log.Error("Ignoring pre-merge parent block", "number", params.Number, "hash", params.BlockHash, "td", ptd, "ttd", ttd) - return engine.INVALID_TERMINAL_BLOCK, nil - } - if block.Time() <= parent.Time() { - log.Warn("Invalid timestamp", "parent", block.Time(), "block", block.Time()) - return api.invalid(errors.New("invalid timestamp"), parent.Header()), nil - } - // Another corner case: if the node is in snap sync mode, but the CL client - // tries to make it import a block. That should be denied as pushing something - // into the database directly will conflict with the assumptions of snap sync - // that it has an empty db that it can fill itself. - if api.eth.SyncMode() != downloader.FullSync { - return api.delayPayloadImport(block) - } - if !api.eth.BlockChain().HasBlockAndState(block.ParentHash(), block.NumberU64()-1) { - api.remoteBlocks.put(block.Hash(), block.Header()) - log.Warn("State not available, ignoring new payload") - return engine.PayloadStatusV1{Status: engine.ACCEPTED}, nil - } - log.Trace("Inserting block without sethead", "hash", block.Hash(), "number", block.Number) - if err := api.eth.BlockChain().InsertBlockWithoutSetHead(block); err != nil { - log.Warn("NewPayloadV1: inserting block failed", "error", err) - - api.invalidLock.Lock() - api.invalidBlocksHits[block.Hash()] = 1 - api.invalidTipsets[block.Hash()] = block.Header() - api.invalidLock.Unlock() - - return api.invalid(err, parent.Header()), nil - } - // We've accepted a valid payload from the beacon client. Mark the local - // chain transitions to notify other subsystems (e.g. downloader) of the - // behavioral change. - if merger := api.eth.Merger(); !merger.TDDReached() { - merger.ReachTTD() - api.eth.Downloader().Cancel() - } - hash := block.Hash() - return engine.PayloadStatusV1{Status: engine.VALID, LatestValidHash: &hash}, nil -} - -// delayPayloadImport stashes the given block away for import at a later time, -// either via a forkchoice update or a sync extension. This method is meant to -// be called by the newpayload command when the block seems to be ok, but some -// prerequisite prevents it from being processed (e.g. no parent, or snap sync). -func (api *ConsensusAPI) delayPayloadImport(block *types.Block) (engine.PayloadStatusV1, error) { - // Sanity check that this block's parent is not on a previously invalidated - // chain. If it is, mark the block as invalid too. - if res := api.checkInvalidAncestor(block.ParentHash(), block.Hash()); res != nil { - return *res, nil - } - // Stash the block away for a potential forced forkchoice update to it - // at a later time. - api.remoteBlocks.put(block.Hash(), block.Header()) - - // Although we don't want to trigger a sync, if there is one already in - // progress, try to extend if with the current payload request to relieve - // some strain from the forkchoice update. - err := api.eth.Downloader().BeaconExtend(api.eth.SyncMode(), block.Header()) - if err == nil { - log.Debug("Payload accepted for sync extension", "number", block.NumberU64(), "hash", block.Hash()) - return engine.PayloadStatusV1{Status: engine.SYNCING}, nil - } - // Either no beacon sync was started yet, or it rejected the delivered - // payload as non-integratable on top of the existing sync. We'll just - // have to rely on the beacon client to forcefully update the head with - // a forkchoice update request. - if api.eth.SyncMode() == downloader.FullSync { - // In full sync mode, failure to import a well-formed block can only mean - // that the parent state is missing and the syncer rejected extending the - // current cycle with the new payload. - log.Warn("Ignoring payload with missing parent", "number", block.NumberU64(), "hash", block.Hash(), "parent", block.ParentHash(), "reason", err) - } else { - // In non-full sync mode (i.e. snap sync) all payloads are rejected until - // snap sync terminates as snap sync relies on direct database injections - // and cannot afford concurrent out-if-band modifications via imports. - log.Warn("Ignoring payload while snap syncing", "number", block.NumberU64(), "hash", block.Hash(), "reason", err) - } - return engine.PayloadStatusV1{Status: engine.SYNCING}, nil -} - -// setInvalidAncestor is a callback for the downloader to notify us if a bad block -// is encountered during the async sync. -func (api *ConsensusAPI) setInvalidAncestor(invalid *types.Header, origin *types.Header) { - api.invalidLock.Lock() - defer api.invalidLock.Unlock() - - api.invalidTipsets[origin.Hash()] = invalid - api.invalidBlocksHits[invalid.Hash()]++ -} - -// checkInvalidAncestor checks whether the specified chain end links to a known -// bad ancestor. If yes, it constructs the payload failure response to return. -func (api *ConsensusAPI) checkInvalidAncestor(check common.Hash, head common.Hash) *engine.PayloadStatusV1 { - api.invalidLock.Lock() - defer api.invalidLock.Unlock() - - // If the hash to check is unknown, return valid - invalid, ok := api.invalidTipsets[check] - if !ok { - return nil - } - // If the bad hash was hit too many times, evict it and try to reprocess in - // the hopes that we have a data race that we can exit out of. - badHash := invalid.Hash() - - api.invalidBlocksHits[badHash]++ - if api.invalidBlocksHits[badHash] >= invalidBlockHitEviction { - log.Warn("Too many bad block import attempt, trying", "number", invalid.Number, "hash", badHash) - delete(api.invalidBlocksHits, badHash) - - for descendant, badHeader := range api.invalidTipsets { - if badHeader.Hash() == badHash { - delete(api.invalidTipsets, descendant) - } - } - return nil - } - // Not too many failures yet, mark the head of the invalid chain as invalid - if check != head { - log.Warn("Marked new chain head as invalid", "hash", head, "badnumber", invalid.Number, "badhash", badHash) - for len(api.invalidTipsets) >= invalidTipsetsCap { - for key := range api.invalidTipsets { - delete(api.invalidTipsets, key) - break - } - } - api.invalidTipsets[head] = invalid - } - // If the last valid hash is the terminal pow block, return 0x0 for latest valid hash - lastValid := &invalid.ParentHash - if header := api.eth.BlockChain().GetHeader(invalid.ParentHash, invalid.Number.Uint64()-1); header != nil && header.Difficulty.Sign() != 0 { - lastValid = &common.Hash{} - } - failure := "links to previously rejected block" - return &engine.PayloadStatusV1{ - Status: engine.INVALID, - LatestValidHash: lastValid, - ValidationError: &failure, - } -} - -// invalid returns a response "INVALID" with the latest valid hash supplied by latest. -func (api *ConsensusAPI) invalid(err error, latestValid *types.Header) engine.PayloadStatusV1 { - var currentHash *common.Hash - if latestValid != nil { - if latestValid.Difficulty.BitLen() != 0 { - // Set latest valid hash to 0x0 if parent is PoW block - currentHash = &common.Hash{} - } else { - // Otherwise set latest valid hash to parent hash - h := latestValid.Hash() - currentHash = &h - } - } - errorMsg := err.Error() - return engine.PayloadStatusV1{Status: engine.INVALID, LatestValidHash: currentHash, ValidationError: &errorMsg} -} - -// heartbeat loops indefinitely, and checks if there have been beacon client updates -// received in the last while. If not - or if they but strange ones - it warns the -// user that something might be off with their consensus node. -// -// TODO(karalabe): Spin this goroutine down somehow -func (api *ConsensusAPI) heartbeat() { - // Sleep a bit on startup since there's obviously no beacon client yet - // attached, so no need to print scary warnings to the user. - time.Sleep(beaconUpdateStartupTimeout) - - // If the network is not yet merged/merging, don't bother continuing. - if api.eth.BlockChain().Config().TerminalTotalDifficulty == nil { - return - } - - var offlineLogged time.Time - - for { - // Sleep a bit and retrieve the last known consensus updates - time.Sleep(5 * time.Second) - - api.lastTransitionLock.Lock() - lastTransitionUpdate := api.lastTransitionUpdate - api.lastTransitionLock.Unlock() - - api.lastForkchoiceLock.Lock() - lastForkchoiceUpdate := api.lastForkchoiceUpdate - api.lastForkchoiceLock.Unlock() - - api.lastNewPayloadLock.Lock() - lastNewPayloadUpdate := api.lastNewPayloadUpdate - api.lastNewPayloadLock.Unlock() - - // If there have been no updates for the past while, warn the user - // that the beacon client is probably offline - if api.eth.BlockChain().Config().TerminalTotalDifficultyPassed || api.eth.Merger().TDDReached() { - if time.Since(lastForkchoiceUpdate) <= beaconUpdateConsensusTimeout || time.Since(lastNewPayloadUpdate) <= beaconUpdateConsensusTimeout { - offlineLogged = time.Time{} - continue - } - - if time.Since(offlineLogged) > beaconUpdateWarnFrequency { - if lastForkchoiceUpdate.IsZero() && lastNewPayloadUpdate.IsZero() { - if lastTransitionUpdate.IsZero() { - log.Warn("Post-merge network, but no beacon client seen. Please launch one to follow the chain!") - } else { - log.Warn("Beacon client online, but never received consensus updates. Please ensure your beacon client is operational to follow the chain!") - } - } else { - log.Warn("Beacon client online, but no consensus updates received in a while. Please fix your beacon client to follow the chain!") - } - offlineLogged = time.Now() - } - continue - } - } -} - -// ExchangeCapabilities returns the current methods provided by this node. -func (api *ConsensusAPI) ExchangeCapabilities([]string) []string { - return caps -} - -// GetPayloadBodiesByHashV1 implements engine_getPayloadBodiesByHashV1 which allows for retrieval of a list -// of block bodies by the engine api. -func (api *ConsensusAPI) GetPayloadBodiesByHashV1(hashes []common.Hash) []*engine.ExecutionPayloadBodyV1 { - bodies := make([]*engine.ExecutionPayloadBodyV1, len(hashes)) - for i, hash := range hashes { - block := api.eth.BlockChain().GetBlockByHash(hash) - bodies[i] = getBody(block) - } - return bodies -} - -// GetPayloadBodiesByRangeV1 implements engine_getPayloadBodiesByRangeV1 which allows for retrieval of a range -// of block bodies by the engine api. -func (api *ConsensusAPI) GetPayloadBodiesByRangeV1(start, count hexutil.Uint64) ([]*engine.ExecutionPayloadBodyV1, error) { - if start == 0 || count == 0 { - return nil, engine.InvalidParams.With(fmt.Errorf("invalid start or count, start: %v count: %v", start, count)) - } - if count > 1024 { - return nil, engine.TooLargeRequest.With(fmt.Errorf("requested count too large: %v", count)) - } - // limit count up until current - current := api.eth.BlockChain().CurrentBlock().Number.Uint64() - last := uint64(start) + uint64(count) - 1 - if last > current { - last = current - } - bodies := make([]*engine.ExecutionPayloadBodyV1, 0, uint64(count)) - for i := uint64(start); i <= last; i++ { - block := api.eth.BlockChain().GetBlockByNumber(i) - bodies = append(bodies, getBody(block)) - } - return bodies, nil -} - -func getBody(block *types.Block) *engine.ExecutionPayloadBodyV1 { - if block == nil { - return nil - } - - var ( - body = block.Body() - txs = make([]hexutil.Bytes, len(body.Transactions)) - withdrawals = body.Withdrawals - ) - - for j, tx := range body.Transactions { - data, _ := tx.MarshalBinary() - txs[j] = hexutil.Bytes(data) - } - - // Post-shanghai withdrawals MUST be set to empty slice instead of nil - if withdrawals == nil && block.Header().WithdrawalsHash != nil { - withdrawals = make([]*types.Withdrawal, 0) - } - - return &engine.ExecutionPayloadBodyV1{ - TransactionData: txs, - Withdrawals: withdrawals, - } -} diff --git a/eth/catalyst/api_test.go b/eth/catalyst/api_test.go deleted file mode 100644 index c875c485dd..0000000000 --- a/eth/catalyst/api_test.go +++ /dev/null @@ -1,1644 +0,0 @@ -// Copyright 2021 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 . - -package catalyst - -import ( - "bytes" - "context" - crand "crypto/rand" - "fmt" - "math/big" - "math/rand" - "reflect" - "sync" - "testing" - "time" - - "github.com/ethereum/go-ethereum/beacon/engine" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/consensus" - beaconConsensus "github.com/ethereum/go-ethereum/consensus/beacon" - "github.com/ethereum/go-ethereum/consensus/ethash" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/crypto/kzg4844" - "github.com/ethereum/go-ethereum/eth" - "github.com/ethereum/go-ethereum/eth/downloader" - "github.com/ethereum/go-ethereum/eth/ethconfig" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/miner" - "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/rpc" - "github.com/ethereum/go-ethereum/trie" - "github.com/mattn/go-colorable" -) - -var ( - // testKey is a private key to use for funding a tester account. - testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") - - // testAddr is the Ethereum address of the tester account. - testAddr = crypto.PubkeyToAddress(testKey.PublicKey) - - testBalance = big.NewInt(2e18) -) - -func generateMergeChain(n int, merged bool) (*core.Genesis, []*types.Block) { - config := *params.AllEthashProtocolChanges - engine := consensus.Engine(beaconConsensus.New(ethash.NewFaker())) - if merged { - config.TerminalTotalDifficulty = common.Big0 - config.TerminalTotalDifficultyPassed = true - engine = beaconConsensus.NewFaker() - } - genesis := &core.Genesis{ - Config: &config, - Alloc: core.GenesisAlloc{ - testAddr: {Balance: testBalance}, - params.BeaconRootsStorageAddress: {Balance: common.Big0, Code: common.Hex2Bytes("3373fffffffffffffffffffffffffffffffffffffffe14604457602036146024575f5ffd5b620180005f350680545f35146037575f5ffd5b6201800001545f5260205ff35b6201800042064281555f359062018000015500")}, - }, - ExtraData: []byte("test genesis"), - Timestamp: 9000, - BaseFee: big.NewInt(params.InitialBaseFee), - Difficulty: big.NewInt(0), - } - testNonce := uint64(0) - generate := func(i int, g *core.BlockGen) { - g.OffsetTime(5) - g.SetExtra([]byte("test")) - tx, _ := types.SignTx(types.NewTransaction(testNonce, common.HexToAddress("0x9a9070028361F7AAbeB3f2F2Dc07F82C4a98A02a"), big.NewInt(1), params.TxGas, big.NewInt(params.InitialBaseFee*2), nil), types.LatestSigner(&config), testKey) - g.AddTx(tx) - testNonce++ - } - _, blocks, _ := core.GenerateChainWithGenesis(genesis, engine, n, generate) - - if !merged { - totalDifficulty := big.NewInt(0) - for _, b := range blocks { - totalDifficulty.Add(totalDifficulty, b.Difficulty()) - } - config.TerminalTotalDifficulty = totalDifficulty - } - - return genesis, blocks -} - -func TestEth2AssembleBlock(t *testing.T) { - genesis, blocks := generateMergeChain(10, false) - n, ethservice := startEthService(t, genesis, blocks) - defer n.Close() - - api := NewConsensusAPI(ethservice) - signer := types.NewEIP155Signer(ethservice.BlockChain().Config().ChainID) - tx, err := types.SignTx(types.NewTransaction(uint64(10), blocks[9].Coinbase(), big.NewInt(1000), params.TxGas, big.NewInt(params.InitialBaseFee), nil), signer, testKey) - if err != nil { - t.Fatalf("error signing transaction, err=%v", err) - } - ethservice.TxPool().Add([]*types.Transaction{tx}, true, false) - blockParams := engine.PayloadAttributes{ - Timestamp: blocks[9].Time() + 5, - } - // The miner needs to pick up on the txs in the pool, so a few retries might be - // needed. - if _, testErr := assembleWithTransactions(api, blocks[9].Hash(), &blockParams, 1); testErr != nil { - t.Fatal(testErr) - } -} - -// assembleWithTransactions tries to assemble a block, retrying until it has 'want', -// number of transactions in it, or it has retried three times. -func assembleWithTransactions(api *ConsensusAPI, parentHash common.Hash, params *engine.PayloadAttributes, want int) (execData *engine.ExecutableData, err error) { - for retries := 3; retries > 0; retries-- { - execData, err = assembleBlock(api, parentHash, params) - if err != nil { - return nil, err - } - if have, want := len(execData.Transactions), want; have != want { - err = fmt.Errorf("invalid number of transactions, have %d want %d", have, want) - continue - } - return execData, nil - } - return nil, err -} - -func TestEth2AssembleBlockWithAnotherBlocksTxs(t *testing.T) { - genesis, blocks := generateMergeChain(10, false) - n, ethservice := startEthService(t, genesis, blocks[:9]) - defer n.Close() - - api := NewConsensusAPI(ethservice) - - // Put the 10th block's tx in the pool and produce a new block - txs := blocks[9].Transactions() - api.eth.TxPool().Add(txs, false, true) - blockParams := engine.PayloadAttributes{ - Timestamp: blocks[8].Time() + 5, - } - // The miner needs to pick up on the txs in the pool, so a few retries might be - // needed. - if _, err := assembleWithTransactions(api, blocks[8].Hash(), &blockParams, blocks[9].Transactions().Len()); err != nil { - t.Fatal(err) - } -} - -func TestSetHeadBeforeTotalDifficulty(t *testing.T) { - genesis, blocks := generateMergeChain(10, false) - n, ethservice := startEthService(t, genesis, blocks) - defer n.Close() - - api := NewConsensusAPI(ethservice) - fcState := engine.ForkchoiceStateV1{ - HeadBlockHash: blocks[5].Hash(), - SafeBlockHash: common.Hash{}, - FinalizedBlockHash: common.Hash{}, - } - if resp, err := api.ForkchoiceUpdatedV1(fcState, nil); err != nil { - t.Errorf("fork choice updated should not error: %v", err) - } else if resp.PayloadStatus.Status != engine.INVALID_TERMINAL_BLOCK.Status { - t.Errorf("fork choice updated before total terminal difficulty should be INVALID") - } -} - -func TestEth2PrepareAndGetPayload(t *testing.T) { - genesis, blocks := generateMergeChain(10, false) - // We need to properly set the terminal total difficulty - genesis.Config.TerminalTotalDifficulty.Sub(genesis.Config.TerminalTotalDifficulty, blocks[9].Difficulty()) - n, ethservice := startEthService(t, genesis, blocks[:9]) - defer n.Close() - - api := NewConsensusAPI(ethservice) - - // Put the 10th block's tx in the pool and produce a new block - txs := blocks[9].Transactions() - ethservice.TxPool().Add(txs, true, false) - blockParams := engine.PayloadAttributes{ - Timestamp: blocks[8].Time() + 5, - } - fcState := engine.ForkchoiceStateV1{ - HeadBlockHash: blocks[8].Hash(), - SafeBlockHash: common.Hash{}, - FinalizedBlockHash: common.Hash{}, - } - _, err := api.ForkchoiceUpdatedV1(fcState, &blockParams) - if err != nil { - t.Fatalf("error preparing payload, err=%v", err) - } - // give the payload some time to be built - time.Sleep(100 * time.Millisecond) - payloadID := (&miner.BuildPayloadArgs{ - Parent: fcState.HeadBlockHash, - Timestamp: blockParams.Timestamp, - FeeRecipient: blockParams.SuggestedFeeRecipient, - Random: blockParams.Random, - BeaconRoot: blockParams.BeaconRoot, - }).Id() - execData, err := api.GetPayloadV1(payloadID) - if err != nil { - t.Fatalf("error getting payload, err=%v", err) - } - if len(execData.Transactions) != blocks[9].Transactions().Len() { - t.Fatalf("invalid number of transactions %d != 1", len(execData.Transactions)) - } - // Test invalid payloadID - var invPayload engine.PayloadID - copy(invPayload[:], payloadID[:]) - invPayload[0] = ^invPayload[0] - _, err = api.GetPayloadV1(invPayload) - if err == nil { - t.Fatal("expected error retrieving invalid payload") - } -} - -func checkLogEvents(t *testing.T, logsCh <-chan []*types.Log, rmLogsCh <-chan core.RemovedLogsEvent, wantNew, wantRemoved int) { - t.Helper() - - if len(logsCh) != wantNew { - t.Fatalf("wrong number of log events: got %d, want %d", len(logsCh), wantNew) - } - if len(rmLogsCh) != wantRemoved { - t.Fatalf("wrong number of removed log events: got %d, want %d", len(rmLogsCh), wantRemoved) - } - // Drain events. - for i := 0; i < len(logsCh); i++ { - <-logsCh - } - for i := 0; i < len(rmLogsCh); i++ { - <-rmLogsCh - } -} - -func TestInvalidPayloadTimestamp(t *testing.T) { - genesis, preMergeBlocks := generateMergeChain(10, false) - n, ethservice := startEthService(t, genesis, preMergeBlocks) - defer n.Close() - var ( - api = NewConsensusAPI(ethservice) - parent = ethservice.BlockChain().CurrentBlock() - ) - tests := []struct { - time uint64 - shouldErr bool - }{ - {0, true}, - {parent.Time, true}, - {parent.Time - 1, true}, - - // TODO (MariusVanDerWijden) following tests are currently broken, - // fixed in upcoming merge-kiln-v2 pr - //{parent.Time() + 1, false}, - //{uint64(time.Now().Unix()) + uint64(time.Minute), false}, - } - - for i, test := range tests { - t.Run(fmt.Sprintf("Timestamp test: %v", i), func(t *testing.T) { - params := engine.PayloadAttributes{ - Timestamp: test.time, - Random: crypto.Keccak256Hash([]byte{byte(123)}), - SuggestedFeeRecipient: parent.Coinbase, - } - fcState := engine.ForkchoiceStateV1{ - HeadBlockHash: parent.Hash(), - SafeBlockHash: common.Hash{}, - FinalizedBlockHash: common.Hash{}, - } - _, err := api.ForkchoiceUpdatedV1(fcState, ¶ms) - if test.shouldErr && err == nil { - t.Fatalf("expected error preparing payload with invalid timestamp, err=%v", err) - } else if !test.shouldErr && err != nil { - t.Fatalf("error preparing payload with valid timestamp, err=%v", err) - } - }) - } -} - -func TestEth2NewBlock(t *testing.T) { - genesis, preMergeBlocks := generateMergeChain(10, false) - n, ethservice := startEthService(t, genesis, preMergeBlocks) - defer n.Close() - - var ( - api = NewConsensusAPI(ethservice) - parent = preMergeBlocks[len(preMergeBlocks)-1] - - // This EVM code generates a log when the contract is created. - logCode = common.Hex2Bytes("60606040525b7f24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b60405180905060405180910390a15b600a8060416000396000f360606040526008565b00") - ) - // The event channels. - newLogCh := make(chan []*types.Log, 10) - rmLogsCh := make(chan core.RemovedLogsEvent, 10) - ethservice.BlockChain().SubscribeLogsEvent(newLogCh) - ethservice.BlockChain().SubscribeRemovedLogsEvent(rmLogsCh) - - for i := 0; i < 10; i++ { - statedb, _ := ethservice.BlockChain().StateAt(parent.Root()) - nonce := statedb.GetNonce(testAddr) - tx, _ := types.SignTx(types.NewContractCreation(nonce, new(big.Int), 1000000, big.NewInt(2*params.InitialBaseFee), logCode), types.LatestSigner(ethservice.BlockChain().Config()), testKey) - ethservice.TxPool().Add([]*types.Transaction{tx}, true, false) - - execData, err := assembleWithTransactions(api, parent.Hash(), &engine.PayloadAttributes{ - Timestamp: parent.Time() + 5, - }, 1) - if err != nil { - t.Fatalf("Failed to create the executable data %v", err) - } - block, err := engine.ExecutableDataToBlock(*execData, nil, nil) - if err != nil { - t.Fatalf("Failed to convert executable data to block %v", err) - } - newResp, err := api.NewPayloadV1(*execData) - switch { - case err != nil: - t.Fatalf("Failed to insert block: %v", err) - case newResp.Status != "VALID": - t.Fatalf("Failed to insert block: %v", newResp.Status) - case ethservice.BlockChain().CurrentBlock().Number.Uint64() != block.NumberU64()-1: - t.Fatalf("Chain head shouldn't be updated") - } - checkLogEvents(t, newLogCh, rmLogsCh, 0, 0) - fcState := engine.ForkchoiceStateV1{ - HeadBlockHash: block.Hash(), - SafeBlockHash: block.Hash(), - FinalizedBlockHash: block.Hash(), - } - if _, err := api.ForkchoiceUpdatedV1(fcState, nil); err != nil { - t.Fatalf("Failed to insert block: %v", err) - } - if have, want := ethservice.BlockChain().CurrentBlock().Number.Uint64(), block.NumberU64(); have != want { - t.Fatalf("Chain head should be updated, have %d want %d", have, want) - } - checkLogEvents(t, newLogCh, rmLogsCh, 1, 0) - - parent = block - } - - // Introduce fork chain - var ( - head = ethservice.BlockChain().CurrentBlock().Number.Uint64() - ) - parent = preMergeBlocks[len(preMergeBlocks)-1] - for i := 0; i < 10; i++ { - execData, err := assembleBlock(api, parent.Hash(), &engine.PayloadAttributes{ - Timestamp: parent.Time() + 6, - }) - if err != nil { - t.Fatalf("Failed to create the executable data %v", err) - } - block, err := engine.ExecutableDataToBlock(*execData, nil, nil) - if err != nil { - t.Fatalf("Failed to convert executable data to block %v", err) - } - newResp, err := api.NewPayloadV1(*execData) - if err != nil || newResp.Status != "VALID" { - t.Fatalf("Failed to insert block: %v", err) - } - if ethservice.BlockChain().CurrentBlock().Number.Uint64() != head { - t.Fatalf("Chain head shouldn't be updated") - } - - fcState := engine.ForkchoiceStateV1{ - HeadBlockHash: block.Hash(), - SafeBlockHash: block.Hash(), - FinalizedBlockHash: block.Hash(), - } - if _, err := api.ForkchoiceUpdatedV1(fcState, nil); err != nil { - t.Fatalf("Failed to insert block: %v", err) - } - if ethservice.BlockChain().CurrentBlock().Number.Uint64() != block.NumberU64() { - t.Fatalf("Chain head should be updated") - } - parent, head = block, block.NumberU64() - } -} - -func TestEth2DeepReorg(t *testing.T) { - // TODO (MariusVanDerWijden) TestEth2DeepReorg is currently broken, because it tries to reorg - // before the totalTerminalDifficulty threshold - /* - genesis, preMergeBlocks := generateMergeChain(core.TriesInMemory * 2, false) - n, ethservice := startEthService(t, genesis, preMergeBlocks) - defer n.Close() - - var ( - api = NewConsensusAPI(ethservice, nil) - parent = preMergeBlocks[len(preMergeBlocks)-core.TriesInMemory-1] - head = ethservice.BlockChain().CurrentBlock().Number.Uint64()() - ) - if ethservice.BlockChain().HasBlockAndState(parent.Hash(), parent.NumberU64()) { - t.Errorf("Block %d not pruned", parent.NumberU64()) - } - for i := 0; i < 10; i++ { - execData, err := api.assembleBlock(AssembleBlockParams{ - ParentHash: parent.Hash(), - Timestamp: parent.Time() + 5, - }) - if err != nil { - t.Fatalf("Failed to create the executable data %v", err) - } - block, err := ExecutableDataToBlock(ethservice.BlockChain().Config(), parent.Header(), *execData) - if err != nil { - t.Fatalf("Failed to convert executable data to block %v", err) - } - newResp, err := api.ExecutePayload(*execData) - if err != nil || newResp.Status != "VALID" { - t.Fatalf("Failed to insert block: %v", err) - } - if ethservice.BlockChain().CurrentBlock().Number.Uint64()() != head { - t.Fatalf("Chain head shouldn't be updated") - } - if err := api.setHead(block.Hash()); err != nil { - t.Fatalf("Failed to set head: %v", err) - } - if ethservice.BlockChain().CurrentBlock().Number.Uint64()() != block.NumberU64() { - t.Fatalf("Chain head should be updated") - } - parent, head = block, block.NumberU64() - } - */ -} - -// startEthService creates a full node instance for testing. -func startEthService(t *testing.T, genesis *core.Genesis, blocks []*types.Block) (*node.Node, *eth.Ethereum) { - t.Helper() - - n, err := node.New(&node.Config{ - P2P: p2p.Config{ - ListenAddr: "0.0.0.0:0", - NoDiscovery: true, - MaxPeers: 25, - }}) - if err != nil { - t.Fatal("can't create node:", err) - } - - ethcfg := ðconfig.Config{Genesis: genesis, SyncMode: downloader.FullSync, TrieTimeout: time.Minute, TrieDirtyCache: 256, TrieCleanCache: 256} - ethservice, err := eth.New(n, ethcfg) - if err != nil { - t.Fatal("can't create eth service:", err) - } - if err := n.Start(); err != nil { - t.Fatal("can't start node:", err) - } - if _, err := ethservice.BlockChain().InsertChain(blocks); err != nil { - n.Close() - t.Fatal("can't import test blocks:", err) - } - - ethservice.SetEtherbase(testAddr) - ethservice.SetSynced() - return n, ethservice -} - -func TestFullAPI(t *testing.T) { - genesis, preMergeBlocks := generateMergeChain(10, false) - n, ethservice := startEthService(t, genesis, preMergeBlocks) - defer n.Close() - var ( - parent = ethservice.BlockChain().CurrentBlock() - // This EVM code generates a log when the contract is created. - logCode = common.Hex2Bytes("60606040525b7f24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b60405180905060405180910390a15b600a8060416000396000f360606040526008565b00") - ) - - callback := func(parent *types.Header) { - statedb, _ := ethservice.BlockChain().StateAt(parent.Root) - nonce := statedb.GetNonce(testAddr) - tx, _ := types.SignTx(types.NewContractCreation(nonce, new(big.Int), 1000000, big.NewInt(2*params.InitialBaseFee), logCode), types.LatestSigner(ethservice.BlockChain().Config()), testKey) - ethservice.TxPool().Add([]*types.Transaction{tx}, true, false) - } - - setupBlocks(t, ethservice, 10, parent, callback, nil) -} - -func setupBlocks(t *testing.T, ethservice *eth.Ethereum, n int, parent *types.Header, callback func(parent *types.Header), withdrawals [][]*types.Withdrawal) []*types.Header { - api := NewConsensusAPI(ethservice) - var blocks []*types.Header - for i := 0; i < n; i++ { - callback(parent) - var w []*types.Withdrawal - if withdrawals != nil { - w = withdrawals[i] - } - - payload := getNewPayload(t, api, parent, w) - execResp, err := api.NewPayloadV2(*payload) - if err != nil { - t.Fatalf("can't execute payload: %v", err) - } - if execResp.Status != engine.VALID { - t.Fatalf("invalid status: %v", execResp.Status) - } - fcState := engine.ForkchoiceStateV1{ - HeadBlockHash: payload.BlockHash, - SafeBlockHash: payload.ParentHash, - FinalizedBlockHash: payload.ParentHash, - } - if _, err := api.ForkchoiceUpdatedV1(fcState, nil); err != nil { - t.Fatalf("Failed to insert block: %v", err) - } - if ethservice.BlockChain().CurrentBlock().Number.Uint64() != payload.Number { - t.Fatal("Chain head should be updated") - } - if ethservice.BlockChain().CurrentFinalBlock().Number.Uint64() != payload.Number-1 { - t.Fatal("Finalized block should be updated") - } - parent = ethservice.BlockChain().CurrentBlock() - blocks = append(blocks, parent) - } - return blocks -} - -func TestExchangeTransitionConfig(t *testing.T) { - genesis, preMergeBlocks := generateMergeChain(10, false) - n, ethservice := startEthService(t, genesis, preMergeBlocks) - defer n.Close() - - // invalid ttd - api := NewConsensusAPI(ethservice) - config := engine.TransitionConfigurationV1{ - TerminalTotalDifficulty: (*hexutil.Big)(big.NewInt(0)), - TerminalBlockHash: common.Hash{}, - TerminalBlockNumber: 0, - } - if _, err := api.ExchangeTransitionConfigurationV1(config); err == nil { - t.Fatal("expected error on invalid config, invalid ttd") - } - // invalid terminal block hash - config = engine.TransitionConfigurationV1{ - TerminalTotalDifficulty: (*hexutil.Big)(genesis.Config.TerminalTotalDifficulty), - TerminalBlockHash: common.Hash{1}, - TerminalBlockNumber: 0, - } - if _, err := api.ExchangeTransitionConfigurationV1(config); err == nil { - t.Fatal("expected error on invalid config, invalid hash") - } - // valid config - config = engine.TransitionConfigurationV1{ - TerminalTotalDifficulty: (*hexutil.Big)(genesis.Config.TerminalTotalDifficulty), - TerminalBlockHash: common.Hash{}, - TerminalBlockNumber: 0, - } - if _, err := api.ExchangeTransitionConfigurationV1(config); err != nil { - t.Fatalf("expected no error on valid config, got %v", err) - } - // valid config - config = engine.TransitionConfigurationV1{ - TerminalTotalDifficulty: (*hexutil.Big)(genesis.Config.TerminalTotalDifficulty), - TerminalBlockHash: preMergeBlocks[5].Hash(), - TerminalBlockNumber: 6, - } - if _, err := api.ExchangeTransitionConfigurationV1(config); err != nil { - t.Fatalf("expected no error on valid config, got %v", err) - } -} - -/* -TestNewPayloadOnInvalidChain sets up a valid chain and tries to feed blocks -from an invalid chain to test if latestValidHash (LVH) works correctly. - -We set up the following chain where P1 ... Pn and P1” are valid while -P1' is invalid. -We expect -(1) The LVH to point to the current inserted payload if it was valid. -(2) The LVH to point to the valid parent on an invalid payload (if the parent is available). -(3) If the parent is unavailable, the LVH should not be set. - - CommonAncestor◄─▲── P1 ◄── P2 ◄─ P3 ◄─ ... ◄─ Pn - │ - └── P1' ◄─ P2' ◄─ P3' ◄─ ... ◄─ Pn' - │ - └── P1'' -*/ -func TestNewPayloadOnInvalidChain(t *testing.T) { - genesis, preMergeBlocks := generateMergeChain(10, false) - n, ethservice := startEthService(t, genesis, preMergeBlocks) - defer n.Close() - - var ( - api = NewConsensusAPI(ethservice) - parent = ethservice.BlockChain().CurrentBlock() - signer = types.LatestSigner(ethservice.BlockChain().Config()) - // This EVM code generates a log when the contract is created. - logCode = common.Hex2Bytes("60606040525b7f24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b60405180905060405180910390a15b600a8060416000396000f360606040526008565b00") - ) - for i := 0; i < 10; i++ { - statedb, _ := ethservice.BlockChain().StateAt(parent.Root) - tx := types.MustSignNewTx(testKey, signer, &types.LegacyTx{ - Nonce: statedb.GetNonce(testAddr), - Value: new(big.Int), - Gas: 1000000, - GasPrice: big.NewInt(2 * params.InitialBaseFee), - Data: logCode, - }) - ethservice.TxPool().Add([]*types.Transaction{tx}, false, true) - var ( - params = engine.PayloadAttributes{ - Timestamp: parent.Time + 1, - Random: crypto.Keccak256Hash([]byte{byte(i)}), - SuggestedFeeRecipient: parent.Coinbase, - } - fcState = engine.ForkchoiceStateV1{ - HeadBlockHash: parent.Hash(), - SafeBlockHash: common.Hash{}, - FinalizedBlockHash: common.Hash{}, - } - payload *engine.ExecutableData - resp engine.ForkChoiceResponse - err error - ) - for i := 0; ; i++ { - if resp, err = api.ForkchoiceUpdatedV1(fcState, ¶ms); err != nil { - t.Fatalf("error preparing payload, err=%v", err) - } - if resp.PayloadStatus.Status != engine.VALID { - t.Fatalf("error preparing payload, invalid status: %v", resp.PayloadStatus.Status) - } - // give the payload some time to be built - time.Sleep(50 * time.Millisecond) - if payload, err = api.GetPayloadV1(*resp.PayloadID); err != nil { - t.Fatalf("can't get payload: %v", err) - } - if len(payload.Transactions) > 0 { - break - } - // No luck this time we need to update the params and try again. - params.Timestamp = params.Timestamp + 1 - if i > 10 { - t.Fatalf("payload should not be empty") - } - } - execResp, err := api.NewPayloadV1(*payload) - if err != nil { - t.Fatalf("can't execute payload: %v", err) - } - if execResp.Status != engine.VALID { - t.Fatalf("invalid status: %v", execResp.Status) - } - fcState = engine.ForkchoiceStateV1{ - HeadBlockHash: payload.BlockHash, - SafeBlockHash: payload.ParentHash, - FinalizedBlockHash: payload.ParentHash, - } - if _, err := api.ForkchoiceUpdatedV1(fcState, nil); err != nil { - t.Fatalf("Failed to insert block: %v", err) - } - if ethservice.BlockChain().CurrentBlock().Number.Uint64() != payload.Number { - t.Fatalf("Chain head should be updated") - } - parent = ethservice.BlockChain().CurrentBlock() - } -} - -func assembleBlock(api *ConsensusAPI, parentHash common.Hash, params *engine.PayloadAttributes) (*engine.ExecutableData, error) { - args := &miner.BuildPayloadArgs{ - Parent: parentHash, - Timestamp: params.Timestamp, - FeeRecipient: params.SuggestedFeeRecipient, - Random: params.Random, - Withdrawals: params.Withdrawals, - BeaconRoot: params.BeaconRoot, - } - payload, err := api.eth.Miner().BuildPayload(args) - if err != nil { - return nil, err - } - return payload.ResolveFull().ExecutionPayload, nil -} - -func TestEmptyBlocks(t *testing.T) { - genesis, preMergeBlocks := generateMergeChain(10, false) - n, ethservice := startEthService(t, genesis, preMergeBlocks) - defer n.Close() - - commonAncestor := ethservice.BlockChain().CurrentBlock() - api := NewConsensusAPI(ethservice) - - // Setup 10 blocks on the canonical chain - setupBlocks(t, ethservice, 10, commonAncestor, func(parent *types.Header) {}, nil) - - // (1) check LatestValidHash by sending a normal payload (P1'') - payload := getNewPayload(t, api, commonAncestor, nil) - - status, err := api.NewPayloadV1(*payload) - if err != nil { - t.Fatal(err) - } - if status.Status != engine.VALID { - t.Errorf("invalid status: expected VALID got: %v", status.Status) - } - if !bytes.Equal(status.LatestValidHash[:], payload.BlockHash[:]) { - t.Fatalf("invalid LVH: got %v want %v", status.LatestValidHash, payload.BlockHash) - } - - // (2) Now send P1' which is invalid - payload = getNewPayload(t, api, commonAncestor, nil) - payload.GasUsed += 1 - payload = setBlockhash(payload) - // Now latestValidHash should be the common ancestor - status, err = api.NewPayloadV1(*payload) - if err != nil { - t.Fatal(err) - } - if status.Status != engine.INVALID { - t.Errorf("invalid status: expected INVALID got: %v", status.Status) - } - // Expect 0x0 on INVALID block on top of PoW block - expected := common.Hash{} - if !bytes.Equal(status.LatestValidHash[:], expected[:]) { - t.Fatalf("invalid LVH: got %v want %v", status.LatestValidHash, expected) - } - - // (3) Now send a payload with unknown parent - payload = getNewPayload(t, api, commonAncestor, nil) - payload.ParentHash = common.Hash{1} - payload = setBlockhash(payload) - // Now latestValidHash should be the common ancestor - status, err = api.NewPayloadV1(*payload) - if err != nil { - t.Fatal(err) - } - if status.Status != engine.SYNCING { - t.Errorf("invalid status: expected SYNCING got: %v", status.Status) - } - if status.LatestValidHash != nil { - t.Fatalf("invalid LVH: got %v wanted nil", status.LatestValidHash) - } -} - -func getNewPayload(t *testing.T, api *ConsensusAPI, parent *types.Header, withdrawals []*types.Withdrawal) *engine.ExecutableData { - params := engine.PayloadAttributes{ - Timestamp: parent.Time + 1, - Random: crypto.Keccak256Hash([]byte{byte(1)}), - SuggestedFeeRecipient: parent.Coinbase, - Withdrawals: withdrawals, - } - - payload, err := assembleBlock(api, parent.Hash(), ¶ms) - if err != nil { - t.Fatal(err) - } - return payload -} - -// setBlockhash sets the blockhash of a modified ExecutableData. -// Can be used to make modified payloads look valid. -func setBlockhash(data *engine.ExecutableData) *engine.ExecutableData { - txs, _ := decodeTransactions(data.Transactions) - number := big.NewInt(0) - number.SetUint64(data.Number) - header := &types.Header{ - ParentHash: data.ParentHash, - UncleHash: types.EmptyUncleHash, - Coinbase: data.FeeRecipient, - Root: data.StateRoot, - TxHash: types.DeriveSha(types.Transactions(txs), trie.NewStackTrie(nil)), - ReceiptHash: data.ReceiptsRoot, - Bloom: types.BytesToBloom(data.LogsBloom), - Difficulty: common.Big0, - Number: number, - GasLimit: data.GasLimit, - GasUsed: data.GasUsed, - Time: data.Timestamp, - BaseFee: data.BaseFeePerGas, - Extra: data.ExtraData, - MixDigest: data.Random, - } - block := types.NewBlockWithHeader(header).WithBody(txs, nil /* uncles */) - data.BlockHash = block.Hash() - return data -} - -func decodeTransactions(enc [][]byte) ([]*types.Transaction, error) { - var txs = make([]*types.Transaction, len(enc)) - for i, encTx := range enc { - var tx types.Transaction - if err := tx.UnmarshalBinary(encTx); err != nil { - return nil, fmt.Errorf("invalid transaction %d: %v", i, err) - } - txs[i] = &tx - } - return txs, nil -} - -func TestTrickRemoteBlockCache(t *testing.T) { - // Setup two nodes - genesis, preMergeBlocks := generateMergeChain(10, false) - nodeA, ethserviceA := startEthService(t, genesis, preMergeBlocks) - nodeB, ethserviceB := startEthService(t, genesis, preMergeBlocks) - defer nodeA.Close() - defer nodeB.Close() - for nodeB.Server().NodeInfo().Ports.Listener == 0 { - time.Sleep(250 * time.Millisecond) - } - nodeA.Server().AddPeer(nodeB.Server().Self()) - nodeB.Server().AddPeer(nodeA.Server().Self()) - apiA := NewConsensusAPI(ethserviceA) - apiB := NewConsensusAPI(ethserviceB) - - commonAncestor := ethserviceA.BlockChain().CurrentBlock() - - // Setup 10 blocks on the canonical chain - setupBlocks(t, ethserviceA, 10, commonAncestor, func(parent *types.Header) {}, nil) - commonAncestor = ethserviceA.BlockChain().CurrentBlock() - - var invalidChain []*engine.ExecutableData - // create a valid payload (P1) - //payload1 := getNewPayload(t, apiA, commonAncestor) - //invalidChain = append(invalidChain, payload1) - - // create an invalid payload2 (P2) - payload2 := getNewPayload(t, apiA, commonAncestor, nil) - //payload2.ParentHash = payload1.BlockHash - payload2.GasUsed += 1 - payload2 = setBlockhash(payload2) - invalidChain = append(invalidChain, payload2) - - head := payload2 - // create some valid payloads on top - for i := 0; i < 10; i++ { - payload := getNewPayload(t, apiA, commonAncestor, nil) - payload.ParentHash = head.BlockHash - payload = setBlockhash(payload) - invalidChain = append(invalidChain, payload) - head = payload - } - - // feed the payloads to node B - for _, payload := range invalidChain { - status, err := apiB.NewPayloadV1(*payload) - if err != nil { - panic(err) - } - if status.Status == engine.VALID { - t.Error("invalid status: VALID on an invalid chain") - } - // Now reorg to the head of the invalid chain - resp, err := apiB.ForkchoiceUpdatedV1(engine.ForkchoiceStateV1{HeadBlockHash: payload.BlockHash, SafeBlockHash: payload.BlockHash, FinalizedBlockHash: payload.ParentHash}, nil) - if err != nil { - t.Fatal(err) - } - if resp.PayloadStatus.Status == engine.VALID { - t.Error("invalid status: VALID on an invalid chain") - } - time.Sleep(100 * time.Millisecond) - } -} - -func TestInvalidBloom(t *testing.T) { - genesis, preMergeBlocks := generateMergeChain(10, false) - n, ethservice := startEthService(t, genesis, preMergeBlocks) - ethservice.Merger().ReachTTD() - defer n.Close() - - commonAncestor := ethservice.BlockChain().CurrentBlock() - api := NewConsensusAPI(ethservice) - - // Setup 10 blocks on the canonical chain - setupBlocks(t, ethservice, 10, commonAncestor, func(parent *types.Header) {}, nil) - - // (1) check LatestValidHash by sending a normal payload (P1'') - payload := getNewPayload(t, api, commonAncestor, nil) - payload.LogsBloom = append(payload.LogsBloom, byte(1)) - status, err := api.NewPayloadV1(*payload) - if err != nil { - t.Fatal(err) - } - if status.Status != engine.INVALID { - t.Errorf("invalid status: expected INVALID got: %v", status.Status) - } -} - -func TestNewPayloadOnInvalidTerminalBlock(t *testing.T) { - genesis, preMergeBlocks := generateMergeChain(100, false) - n, ethservice := startEthService(t, genesis, preMergeBlocks) - defer n.Close() - api := NewConsensusAPI(ethservice) - - // Test parent already post TTD in FCU - parent := preMergeBlocks[len(preMergeBlocks)-2] - fcState := engine.ForkchoiceStateV1{ - HeadBlockHash: parent.Hash(), - SafeBlockHash: common.Hash{}, - FinalizedBlockHash: common.Hash{}, - } - resp, err := api.ForkchoiceUpdatedV1(fcState, nil) - if err != nil { - t.Fatalf("error sending forkchoice, err=%v", err) - } - if resp.PayloadStatus != engine.INVALID_TERMINAL_BLOCK { - t.Fatalf("error sending invalid forkchoice, invalid status: %v", resp.PayloadStatus.Status) - } - - // Test parent already post TTD in NewPayload - args := &miner.BuildPayloadArgs{ - Parent: parent.Hash(), - Timestamp: parent.Time() + 1, - Random: crypto.Keccak256Hash([]byte{byte(1)}), - FeeRecipient: parent.Coinbase(), - } - payload, err := api.eth.Miner().BuildPayload(args) - if err != nil { - t.Fatalf("error preparing payload, err=%v", err) - } - data := *payload.Resolve().ExecutionPayload - // We need to recompute the blockhash, since the miner computes a wrong (correct) blockhash - txs, _ := decodeTransactions(data.Transactions) - header := &types.Header{ - ParentHash: data.ParentHash, - UncleHash: types.EmptyUncleHash, - Coinbase: data.FeeRecipient, - Root: data.StateRoot, - TxHash: types.DeriveSha(types.Transactions(txs), trie.NewStackTrie(nil)), - ReceiptHash: data.ReceiptsRoot, - Bloom: types.BytesToBloom(data.LogsBloom), - Difficulty: common.Big0, - Number: new(big.Int).SetUint64(data.Number), - GasLimit: data.GasLimit, - GasUsed: data.GasUsed, - Time: data.Timestamp, - BaseFee: data.BaseFeePerGas, - Extra: data.ExtraData, - MixDigest: data.Random, - } - block := types.NewBlockWithHeader(header).WithBody(txs, nil /* uncles */) - data.BlockHash = block.Hash() - // Send the new payload - resp2, err := api.NewPayloadV1(data) - if err != nil { - t.Fatalf("error sending NewPayload, err=%v", err) - } - if resp2 != engine.INVALID_TERMINAL_BLOCK { - t.Fatalf("error sending invalid forkchoice, invalid status: %v", resp.PayloadStatus.Status) - } -} - -// TestSimultaneousNewBlock does several parallel inserts, both as -// newPayLoad and forkchoiceUpdate. This is to test that the api behaves -// well even of the caller is not being 'serial'. -func TestSimultaneousNewBlock(t *testing.T) { - genesis, preMergeBlocks := generateMergeChain(10, false) - n, ethservice := startEthService(t, genesis, preMergeBlocks) - defer n.Close() - - var ( - api = NewConsensusAPI(ethservice) - parent = preMergeBlocks[len(preMergeBlocks)-1] - ) - for i := 0; i < 10; i++ { - execData, err := assembleBlock(api, parent.Hash(), &engine.PayloadAttributes{ - Timestamp: parent.Time() + 5, - }) - if err != nil { - t.Fatalf("Failed to create the executable data %v", err) - } - // Insert it 10 times in parallel. Should be ignored. - { - var ( - wg sync.WaitGroup - testErr error - errMu sync.Mutex - ) - wg.Add(10) - for ii := 0; ii < 10; ii++ { - go func() { - defer wg.Done() - if newResp, err := api.NewPayloadV1(*execData); err != nil { - errMu.Lock() - testErr = fmt.Errorf("Failed to insert block: %w", err) - errMu.Unlock() - } else if newResp.Status != "VALID" { - errMu.Lock() - testErr = fmt.Errorf("Failed to insert block: %v", newResp.Status) - errMu.Unlock() - } - }() - } - wg.Wait() - if testErr != nil { - t.Fatal(testErr) - } - } - block, err := engine.ExecutableDataToBlock(*execData, nil, nil) - if err != nil { - t.Fatalf("Failed to convert executable data to block %v", err) - } - if ethservice.BlockChain().CurrentBlock().Number.Uint64() != block.NumberU64()-1 { - t.Fatalf("Chain head shouldn't be updated") - } - fcState := engine.ForkchoiceStateV1{ - HeadBlockHash: block.Hash(), - SafeBlockHash: block.Hash(), - FinalizedBlockHash: block.Hash(), - } - { - var ( - wg sync.WaitGroup - testErr error - errMu sync.Mutex - ) - wg.Add(10) - // Do each FCU 10 times - for ii := 0; ii < 10; ii++ { - go func() { - defer wg.Done() - if _, err := api.ForkchoiceUpdatedV1(fcState, nil); err != nil { - errMu.Lock() - testErr = fmt.Errorf("Failed to insert block: %w", err) - errMu.Unlock() - } - }() - } - wg.Wait() - if testErr != nil { - t.Fatal(testErr) - } - } - if have, want := ethservice.BlockChain().CurrentBlock().Number.Uint64(), block.NumberU64(); have != want { - t.Fatalf("Chain head should be updated, have %d want %d", have, want) - } - parent = block - } -} - -// TestWithdrawals creates and verifies two post-Shanghai blocks. The first -// includes zero withdrawals and the second includes two. -func TestWithdrawals(t *testing.T) { - genesis, blocks := generateMergeChain(10, true) - // Set shanghai time to last block + 5 seconds (first post-merge block) - time := blocks[len(blocks)-1].Time() + 5 - genesis.Config.ShanghaiTime = &time - - n, ethservice := startEthService(t, genesis, blocks) - ethservice.Merger().ReachTTD() - defer n.Close() - - api := NewConsensusAPI(ethservice) - - // 10: Build Shanghai block with no withdrawals. - parent := ethservice.BlockChain().CurrentHeader() - blockParams := engine.PayloadAttributes{ - Timestamp: parent.Time + 5, - Withdrawals: make([]*types.Withdrawal, 0), - } - fcState := engine.ForkchoiceStateV1{ - HeadBlockHash: parent.Hash(), - } - resp, err := api.ForkchoiceUpdatedV2(fcState, &blockParams) - if err != nil { - t.Fatalf("error preparing payload, err=%v", err) - } - if resp.PayloadStatus.Status != engine.VALID { - t.Fatalf("unexpected status (got: %s, want: %s)", resp.PayloadStatus.Status, engine.VALID) - } - - // 10: verify state root is the same as parent - payloadID := (&miner.BuildPayloadArgs{ - Parent: fcState.HeadBlockHash, - Timestamp: blockParams.Timestamp, - FeeRecipient: blockParams.SuggestedFeeRecipient, - Random: blockParams.Random, - Withdrawals: blockParams.Withdrawals, - BeaconRoot: blockParams.BeaconRoot, - }).Id() - execData, err := api.GetPayloadV2(payloadID) - if err != nil { - t.Fatalf("error getting payload, err=%v", err) - } - if execData.ExecutionPayload.StateRoot != parent.Root { - t.Fatalf("mismatch state roots (got: %s, want: %s)", execData.ExecutionPayload.StateRoot, blocks[8].Root()) - } - - // 10: verify locally built block - if status, err := api.NewPayloadV2(*execData.ExecutionPayload); err != nil { - t.Fatalf("error validating payload: %v", err) - } else if status.Status != engine.VALID { - t.Fatalf("invalid payload") - } - - // 11: build shanghai block with withdrawal - aa := common.Address{0xaa} - bb := common.Address{0xbb} - blockParams = engine.PayloadAttributes{ - Timestamp: execData.ExecutionPayload.Timestamp + 5, - Withdrawals: []*types.Withdrawal{ - { - Index: 0, - Address: aa, - Amount: 32, - }, - { - Index: 1, - Address: bb, - Amount: 33, - }, - }, - } - fcState.HeadBlockHash = execData.ExecutionPayload.BlockHash - _, err = api.ForkchoiceUpdatedV2(fcState, &blockParams) - if err != nil { - t.Fatalf("error preparing payload, err=%v", err) - } - - // 11: verify locally build block. - payloadID = (&miner.BuildPayloadArgs{ - Parent: fcState.HeadBlockHash, - Timestamp: blockParams.Timestamp, - FeeRecipient: blockParams.SuggestedFeeRecipient, - Random: blockParams.Random, - Withdrawals: blockParams.Withdrawals, - BeaconRoot: blockParams.BeaconRoot, - }).Id() - execData, err = api.GetPayloadV2(payloadID) - if err != nil { - t.Fatalf("error getting payload, err=%v", err) - } - if status, err := api.NewPayloadV2(*execData.ExecutionPayload); err != nil { - t.Fatalf("error validating payload: %v", err) - } else if status.Status != engine.VALID { - t.Fatalf("invalid payload") - } - - // 11: set block as head. - fcState.HeadBlockHash = execData.ExecutionPayload.BlockHash - _, err = api.ForkchoiceUpdatedV2(fcState, nil) - if err != nil { - t.Fatalf("error preparing payload, err=%v", err) - } - - // 11: verify withdrawals were processed. - db, _, err := ethservice.APIBackend.StateAndHeaderByNumber(context.Background(), rpc.BlockNumber(execData.ExecutionPayload.Number)) - if err != nil { - t.Fatalf("unable to load db: %v", err) - } - for i, w := range blockParams.Withdrawals { - // w.Amount is in gwei, balance in wei - if db.GetBalance(w.Address).Uint64() != w.Amount*params.GWei { - t.Fatalf("failed to process withdrawal %d", i) - } - } -} - -func TestNilWithdrawals(t *testing.T) { - genesis, blocks := generateMergeChain(10, true) - // Set shanghai time to last block + 4 seconds (first post-merge block) - time := blocks[len(blocks)-1].Time() + 4 - genesis.Config.ShanghaiTime = &time - - n, ethservice := startEthService(t, genesis, blocks) - ethservice.Merger().ReachTTD() - defer n.Close() - - api := NewConsensusAPI(ethservice) - parent := ethservice.BlockChain().CurrentHeader() - aa := common.Address{0xaa} - - type test struct { - blockParams engine.PayloadAttributes - wantErr bool - } - tests := []test{ - // Before Shanghai - { - blockParams: engine.PayloadAttributes{ - Timestamp: parent.Time + 2, - Withdrawals: nil, - }, - wantErr: false, - }, - { - blockParams: engine.PayloadAttributes{ - Timestamp: parent.Time + 2, - Withdrawals: make([]*types.Withdrawal, 0), - }, - wantErr: true, - }, - { - blockParams: engine.PayloadAttributes{ - Timestamp: parent.Time + 2, - Withdrawals: []*types.Withdrawal{ - { - Index: 0, - Address: aa, - Amount: 32, - }, - }, - }, - wantErr: true, - }, - // After Shanghai - { - blockParams: engine.PayloadAttributes{ - Timestamp: parent.Time + 5, - Withdrawals: nil, - }, - wantErr: true, - }, - { - blockParams: engine.PayloadAttributes{ - Timestamp: parent.Time + 5, - Withdrawals: make([]*types.Withdrawal, 0), - }, - wantErr: false, - }, - { - blockParams: engine.PayloadAttributes{ - Timestamp: parent.Time + 5, - Withdrawals: []*types.Withdrawal{ - { - Index: 0, - Address: aa, - Amount: 32, - }, - }, - }, - wantErr: false, - }, - } - - fcState := engine.ForkchoiceStateV1{ - HeadBlockHash: parent.Hash(), - } - - for _, test := range tests { - _, err := api.ForkchoiceUpdatedV2(fcState, &test.blockParams) - if test.wantErr { - if err == nil { - t.Fatal("wanted error on fcuv2 with invalid withdrawals") - } - continue - } - if err != nil { - t.Fatalf("error preparing payload, err=%v", err) - } - - // 11: verify locally build block. - payloadID := (&miner.BuildPayloadArgs{ - Parent: fcState.HeadBlockHash, - Timestamp: test.blockParams.Timestamp, - FeeRecipient: test.blockParams.SuggestedFeeRecipient, - Random: test.blockParams.Random, - BeaconRoot: test.blockParams.BeaconRoot, - }).Id() - execData, err := api.GetPayloadV2(payloadID) - if err != nil { - t.Fatalf("error getting payload, err=%v", err) - } - if status, err := api.NewPayloadV2(*execData.ExecutionPayload); err != nil { - t.Fatalf("error validating payload: %v", err) - } else if status.Status != engine.VALID { - t.Fatalf("invalid payload") - } - } -} - -func setupBodies(t *testing.T) (*node.Node, *eth.Ethereum, []*types.Block) { - genesis, blocks := generateMergeChain(10, true) - // enable shanghai on the last block - time := blocks[len(blocks)-1].Header().Time + 1 - genesis.Config.ShanghaiTime = &time - n, ethservice := startEthService(t, genesis, blocks) - - var ( - parent = ethservice.BlockChain().CurrentBlock() - // This EVM code generates a log when the contract is created. - logCode = common.Hex2Bytes("60606040525b7f24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b60405180905060405180910390a15b600a8060416000396000f360606040526008565b00") - ) - - callback := func(parent *types.Header) { - statedb, _ := ethservice.BlockChain().StateAt(parent.Root) - nonce := statedb.GetNonce(testAddr) - tx, _ := types.SignTx(types.NewContractCreation(nonce, new(big.Int), 1000000, big.NewInt(2*params.InitialBaseFee), logCode), types.LatestSigner(ethservice.BlockChain().Config()), testKey) - ethservice.TxPool().Add([]*types.Transaction{tx}, false, false) - } - - withdrawals := make([][]*types.Withdrawal, 10) - withdrawals[0] = nil // should be filtered out by miner - withdrawals[1] = make([]*types.Withdrawal, 0) - for i := 2; i < len(withdrawals); i++ { - addr := make([]byte, 20) - crand.Read(addr) - withdrawals[i] = []*types.Withdrawal{ - {Index: rand.Uint64(), Validator: rand.Uint64(), Amount: rand.Uint64(), Address: common.BytesToAddress(addr)}, - } - } - - postShanghaiHeaders := setupBlocks(t, ethservice, 10, parent, callback, withdrawals) - postShanghaiBlocks := make([]*types.Block, len(postShanghaiHeaders)) - for i, header := range postShanghaiHeaders { - postShanghaiBlocks[i] = ethservice.BlockChain().GetBlock(header.Hash(), header.Number.Uint64()) - } - return n, ethservice, append(blocks, postShanghaiBlocks...) -} - -func allHashes(blocks []*types.Block) []common.Hash { - var hashes []common.Hash - for _, b := range blocks { - hashes = append(hashes, b.Hash()) - } - return hashes -} -func allBodies(blocks []*types.Block) []*types.Body { - var bodies []*types.Body - for _, b := range blocks { - bodies = append(bodies, b.Body()) - } - return bodies -} - -func TestGetBlockBodiesByHash(t *testing.T) { - node, eth, blocks := setupBodies(t) - api := NewConsensusAPI(eth) - defer node.Close() - - tests := []struct { - results []*types.Body - hashes []common.Hash - }{ - // First pow block - { - results: []*types.Body{eth.BlockChain().GetBlockByNumber(0).Body()}, - hashes: []common.Hash{eth.BlockChain().GetBlockByNumber(0).Hash()}, - }, - // Last pow block - { - results: []*types.Body{blocks[9].Body()}, - hashes: []common.Hash{blocks[9].Hash()}, - }, - // First post-merge block - { - results: []*types.Body{blocks[10].Body()}, - hashes: []common.Hash{blocks[10].Hash()}, - }, - // Pre & post merge blocks - { - results: []*types.Body{blocks[0].Body(), blocks[9].Body(), blocks[14].Body()}, - hashes: []common.Hash{blocks[0].Hash(), blocks[9].Hash(), blocks[14].Hash()}, - }, - // unavailable block - { - results: []*types.Body{blocks[0].Body(), nil, blocks[14].Body()}, - hashes: []common.Hash{blocks[0].Hash(), {1, 2}, blocks[14].Hash()}, - }, - // same block multiple times - { - results: []*types.Body{blocks[0].Body(), nil, blocks[0].Body(), blocks[0].Body()}, - hashes: []common.Hash{blocks[0].Hash(), {1, 2}, blocks[0].Hash(), blocks[0].Hash()}, - }, - // all blocks - { - results: allBodies(blocks), - hashes: allHashes(blocks), - }, - } - - for k, test := range tests { - result := api.GetPayloadBodiesByHashV1(test.hashes) - for i, r := range result { - if !equalBody(test.results[i], r) { - t.Fatalf("test %v: invalid response: expected %+v got %+v", k, test.results[i], r) - } - } - } -} - -func TestGetBlockBodiesByRange(t *testing.T) { - node, eth, blocks := setupBodies(t) - api := NewConsensusAPI(eth) - defer node.Close() - - tests := []struct { - results []*types.Body - start hexutil.Uint64 - count hexutil.Uint64 - }{ - { - results: []*types.Body{blocks[9].Body()}, - start: 10, - count: 1, - }, - // Genesis - { - results: []*types.Body{blocks[0].Body()}, - start: 1, - count: 1, - }, - // First post-merge block - { - results: []*types.Body{blocks[9].Body()}, - start: 10, - count: 1, - }, - // Pre & post merge blocks - { - results: []*types.Body{blocks[7].Body(), blocks[8].Body(), blocks[9].Body(), blocks[10].Body()}, - start: 8, - count: 4, - }, - // unavailable block - { - results: []*types.Body{blocks[18].Body(), blocks[19].Body()}, - start: 19, - count: 3, - }, - // unavailable block - { - results: []*types.Body{blocks[19].Body()}, - start: 20, - count: 2, - }, - { - results: []*types.Body{blocks[19].Body()}, - start: 20, - count: 1, - }, - // whole range unavailable - { - results: make([]*types.Body, 0), - start: 22, - count: 2, - }, - // allBlocks - { - results: allBodies(blocks), - start: 1, - count: hexutil.Uint64(len(blocks)), - }, - } - - for k, test := range tests { - result, err := api.GetPayloadBodiesByRangeV1(test.start, test.count) - if err != nil { - t.Fatal(err) - } - if len(result) == len(test.results) { - for i, r := range result { - if !equalBody(test.results[i], r) { - t.Fatalf("test %d: invalid response: expected \n%+v\ngot\n%+v", k, test.results[i], r) - } - } - } else { - t.Fatalf("test %d: invalid length want %v got %v", k, len(test.results), len(result)) - } - } -} - -func TestGetBlockBodiesByRangeInvalidParams(t *testing.T) { - node, eth, _ := setupBodies(t) - api := NewConsensusAPI(eth) - defer node.Close() - tests := []struct { - start hexutil.Uint64 - count hexutil.Uint64 - want *engine.EngineAPIError - }{ - // Genesis - { - start: 0, - count: 1, - want: engine.InvalidParams, - }, - // No block requested - { - start: 1, - count: 0, - want: engine.InvalidParams, - }, - // Genesis & no block - { - start: 0, - count: 0, - want: engine.InvalidParams, - }, - // More than 1024 blocks - { - start: 1, - count: 1025, - want: engine.TooLargeRequest, - }, - } - for i, tc := range tests { - result, err := api.GetPayloadBodiesByRangeV1(tc.start, tc.count) - if err == nil { - t.Fatalf("test %d: expected error, got %v", i, result) - } - if have, want := err.Error(), tc.want.Error(); have != want { - t.Fatalf("test %d: have %s, want %s", i, have, want) - } - } -} - -func equalBody(a *types.Body, b *engine.ExecutionPayloadBodyV1) bool { - if a == nil && b == nil { - return true - } else if a == nil || b == nil { - return false - } - if len(a.Transactions) != len(b.TransactionData) { - return false - } - for i, tx := range a.Transactions { - data, _ := tx.MarshalBinary() - if !bytes.Equal(data, b.TransactionData[i]) { - return false - } - } - return reflect.DeepEqual(a.Withdrawals, b.Withdrawals) -} - -func TestBlockToPayloadWithBlobs(t *testing.T) { - header := types.Header{} - var txs []*types.Transaction - - inner := types.BlobTx{ - BlobHashes: make([]common.Hash, 1), - } - - txs = append(txs, types.NewTx(&inner)) - sidecars := []*types.BlobTxSidecar{ - { - Blobs: make([]kzg4844.Blob, 1), - Commitments: make([]kzg4844.Commitment, 1), - Proofs: make([]kzg4844.Proof, 1), - }, - } - - block := types.NewBlock(&header, txs, nil, nil, trie.NewStackTrie(nil)) - envelope := engine.BlockToExecutableData(block, nil, sidecars) - var want int - for _, tx := range txs { - want += len(tx.BlobHashes()) - } - if got := len(envelope.BlobsBundle.Commitments); got != want { - t.Fatalf("invalid number of commitments: got %v, want %v", got, want) - } - if got := len(envelope.BlobsBundle.Proofs); got != want { - t.Fatalf("invalid number of proofs: got %v, want %v", got, want) - } - if got := len(envelope.BlobsBundle.Blobs); got != want { - t.Fatalf("invalid number of blobs: got %v, want %v", got, want) - } - _, err := engine.ExecutableDataToBlock(*envelope.ExecutionPayload, make([]common.Hash, 1), nil) - if err != nil { - t.Error(err) - } -} - -// This checks that beaconRoot is applied to the state from the engine API. -func TestParentBeaconBlockRoot(t *testing.T) { - log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(colorable.NewColorableStderr(), log.LevelTrace, true))) - - genesis, blocks := generateMergeChain(10, true) - - // Set cancun time to last block + 5 seconds - time := blocks[len(blocks)-1].Time() + 5 - genesis.Config.ShanghaiTime = &time - genesis.Config.CancunTime = &time - - n, ethservice := startEthService(t, genesis, blocks) - ethservice.Merger().ReachTTD() - defer n.Close() - - api := NewConsensusAPI(ethservice) - - // 11: Build Shanghai block with no withdrawals. - parent := ethservice.BlockChain().CurrentHeader() - blockParams := engine.PayloadAttributes{ - Timestamp: parent.Time + 5, - Withdrawals: make([]*types.Withdrawal, 0), - BeaconRoot: &common.Hash{42}, - } - fcState := engine.ForkchoiceStateV1{ - HeadBlockHash: parent.Hash(), - } - resp, err := api.ForkchoiceUpdatedV2(fcState, &blockParams) - if err != nil { - t.Fatalf("error preparing payload, err=%v", err.(*engine.EngineAPIError).ErrorData()) - } - if resp.PayloadStatus.Status != engine.VALID { - t.Fatalf("unexpected status (got: %s, want: %s)", resp.PayloadStatus.Status, engine.VALID) - } - - // 11: verify state root is the same as parent - payloadID := (&miner.BuildPayloadArgs{ - Parent: fcState.HeadBlockHash, - Timestamp: blockParams.Timestamp, - FeeRecipient: blockParams.SuggestedFeeRecipient, - Random: blockParams.Random, - Withdrawals: blockParams.Withdrawals, - BeaconRoot: blockParams.BeaconRoot, - }).Id() - execData, err := api.GetPayloadV3(payloadID) - if err != nil { - t.Fatalf("error getting payload, err=%v", err) - } - - // 11: verify locally built block - if status, err := api.NewPayloadV3(*execData.ExecutionPayload, []common.Hash{}, &common.Hash{42}); err != nil { - t.Fatalf("error validating payload: %v", err) - } else if status.Status != engine.VALID { - t.Fatalf("invalid payload") - } - - fcState.HeadBlockHash = execData.ExecutionPayload.BlockHash - resp, err = api.ForkchoiceUpdatedV3(fcState, nil) - if err != nil { - t.Fatalf("error preparing payload, err=%v", err.(*engine.EngineAPIError).ErrorData()) - } - if resp.PayloadStatus.Status != engine.VALID { - t.Fatalf("unexpected status (got: %s, want: %s)", resp.PayloadStatus.Status, engine.VALID) - } - - // 11: verify beacon root was processed. - db, _, err := ethservice.APIBackend.StateAndHeaderByNumber(context.Background(), rpc.BlockNumber(execData.ExecutionPayload.Number)) - if err != nil { - t.Fatalf("unable to load db: %v", err) - } - var ( - timeIdx = common.BigToHash(big.NewInt(int64(execData.ExecutionPayload.Timestamp % 98304))) - rootIdx = common.BigToHash(big.NewInt(int64((execData.ExecutionPayload.Timestamp % 98304) + 98304))) - ) - - if num := db.GetState(params.BeaconRootsStorageAddress, timeIdx); num != timeIdx { - t.Fatalf("incorrect number stored: want %s, got %s", timeIdx, num) - } - if root := db.GetState(params.BeaconRootsStorageAddress, rootIdx); root != *blockParams.BeaconRoot { - t.Fatalf("incorrect root stored: want %s, got %s", *blockParams.BeaconRoot, root) - } -} diff --git a/eth/catalyst/queue.go b/eth/catalyst/queue.go deleted file mode 100644 index 634dc1b2e6..0000000000 --- a/eth/catalyst/queue.go +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright 2022 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 . - -package catalyst - -import ( - "sync" - - "github.com/ethereum/go-ethereum/beacon/engine" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/miner" -) - -// maxTrackedPayloads is the maximum number of prepared payloads the execution -// engine tracks before evicting old ones. Ideally we should only ever track the -// latest one; but have a slight wiggle room for non-ideal conditions. -const maxTrackedPayloads = 10 - -// maxTrackedHeaders is the maximum number of executed payloads the execution -// engine tracks before evicting old ones. These are tracked outside the chain -// during initial sync to allow ForkchoiceUpdate to reference past blocks via -// hashes only. For the sync target it would be enough to track only the latest -// header, but snap sync also needs the latest finalized height for the ancient -// limit. -const maxTrackedHeaders = 96 - -// payloadQueueItem represents an id->payload tuple to store until it's retrieved -// or evicted. -type payloadQueueItem struct { - id engine.PayloadID - payload *miner.Payload -} - -// payloadQueue tracks the latest handful of constructed payloads to be retrieved -// by the beacon chain if block production is requested. -type payloadQueue struct { - payloads []*payloadQueueItem - lock sync.RWMutex -} - -// newPayloadQueue creates a pre-initialized queue with a fixed number of slots -// all containing empty items. -func newPayloadQueue() *payloadQueue { - return &payloadQueue{ - payloads: make([]*payloadQueueItem, maxTrackedPayloads), - } -} - -// put inserts a new payload into the queue at the given id. -func (q *payloadQueue) put(id engine.PayloadID, payload *miner.Payload) { - q.lock.Lock() - defer q.lock.Unlock() - - copy(q.payloads[1:], q.payloads) - q.payloads[0] = &payloadQueueItem{ - id: id, - payload: payload, - } -} - -// get retrieves a previously stored payload item or nil if it does not exist. -func (q *payloadQueue) get(id engine.PayloadID, full bool) *engine.ExecutionPayloadEnvelope { - q.lock.RLock() - defer q.lock.RUnlock() - - for _, item := range q.payloads { - if item == nil { - return nil // no more items - } - if item.id == id { - if !full { - return item.payload.Resolve() - } - return item.payload.ResolveFull() - } - } - return nil -} - -// has checks if a particular payload is already tracked. -func (q *payloadQueue) has(id engine.PayloadID) bool { - q.lock.RLock() - defer q.lock.RUnlock() - - for _, item := range q.payloads { - if item == nil { - return false - } - if item.id == id { - return true - } - } - return false -} - -// headerQueueItem represents an hash->header tuple to store until it's retrieved -// or evicted. -type headerQueueItem struct { - hash common.Hash - header *types.Header -} - -// headerQueue tracks the latest handful of constructed headers to be retrieved -// by the beacon chain if block production is requested. -type headerQueue struct { - headers []*headerQueueItem - lock sync.RWMutex -} - -// newHeaderQueue creates a pre-initialized queue with a fixed number of slots -// all containing empty items. -func newHeaderQueue() *headerQueue { - return &headerQueue{ - headers: make([]*headerQueueItem, maxTrackedHeaders), - } -} - -// put inserts a new header into the queue at the given hash. -func (q *headerQueue) put(hash common.Hash, data *types.Header) { - q.lock.Lock() - defer q.lock.Unlock() - - copy(q.headers[1:], q.headers) - q.headers[0] = &headerQueueItem{ - hash: hash, - header: data, - } -} - -// get retrieves a previously stored header item or nil if it does not exist. -func (q *headerQueue) get(hash common.Hash) *types.Header { - q.lock.RLock() - defer q.lock.RUnlock() - - for _, item := range q.headers { - if item == nil { - return nil // no more items - } - if item.hash == hash { - return item.header - } - } - return nil -} diff --git a/eth/catalyst/simulated_beacon.go b/eth/catalyst/simulated_beacon.go deleted file mode 100644 index d8b8641e6a..0000000000 --- a/eth/catalyst/simulated_beacon.go +++ /dev/null @@ -1,272 +0,0 @@ -// Copyright 2023 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 . - -package catalyst - -import ( - "crypto/rand" - "errors" - "sync" - "time" - - "github.com/ethereum/go-ethereum/beacon/engine" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/eth" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/rpc" -) - -const devEpochLength = 32 - -// withdrawalQueue implements a FIFO queue which holds withdrawals that are -// pending inclusion. -type withdrawalQueue struct { - pending chan *types.Withdrawal -} - -// add queues a withdrawal for future inclusion. -func (w *withdrawalQueue) add(withdrawal *types.Withdrawal) error { - select { - case w.pending <- withdrawal: - break - default: - return errors.New("withdrawal queue full") - } - return nil -} - -// gatherPending returns a number of queued withdrawals up to a maximum count. -func (w *withdrawalQueue) gatherPending(maxCount int) []*types.Withdrawal { - withdrawals := []*types.Withdrawal{} - for { - select { - case withdrawal := <-w.pending: - withdrawals = append(withdrawals, withdrawal) - if len(withdrawals) == maxCount { - break - } - default: - return withdrawals - } - } -} - -type SimulatedBeacon struct { - shutdownCh chan struct{} - eth *eth.Ethereum - period uint64 - withdrawals withdrawalQueue - - feeRecipient common.Address - feeRecipientLock sync.Mutex // lock gates concurrent access to the feeRecipient - - engineAPI *ConsensusAPI - curForkchoiceState engine.ForkchoiceStateV1 - lastBlockTime uint64 -} - -func NewSimulatedBeacon(period uint64, eth *eth.Ethereum) (*SimulatedBeacon, error) { - block := eth.BlockChain().CurrentBlock() - current := engine.ForkchoiceStateV1{ - HeadBlockHash: block.Hash(), - SafeBlockHash: block.Hash(), - FinalizedBlockHash: block.Hash(), - } - engineAPI := newConsensusAPIWithoutHeartbeat(eth) - - // if genesis block, send forkchoiceUpdated to trigger transition to PoS - if block.Number.Sign() == 0 { - if _, err := engineAPI.ForkchoiceUpdatedV2(current, nil); err != nil { - return nil, err - } - } - return &SimulatedBeacon{ - eth: eth, - period: period, - shutdownCh: make(chan struct{}), - engineAPI: engineAPI, - lastBlockTime: block.Time, - curForkchoiceState: current, - withdrawals: withdrawalQueue{make(chan *types.Withdrawal, 20)}, - }, nil -} - -func (c *SimulatedBeacon) setFeeRecipient(feeRecipient common.Address) { - c.feeRecipientLock.Lock() - c.feeRecipient = feeRecipient - c.feeRecipientLock.Unlock() -} - -// Start invokes the SimulatedBeacon life-cycle function in a goroutine. -func (c *SimulatedBeacon) Start() error { - if c.period == 0 { - go c.loopOnDemand() - } else { - go c.loop() - } - return nil -} - -// Stop halts the SimulatedBeacon service. -func (c *SimulatedBeacon) Stop() error { - close(c.shutdownCh) - return nil -} - -// sealBlock initiates payload building for a new block and creates a new block -// with the completed payload. -func (c *SimulatedBeacon) sealBlock(withdrawals []*types.Withdrawal) error { - tstamp := uint64(time.Now().Unix()) - if tstamp <= c.lastBlockTime { - tstamp = c.lastBlockTime + 1 - } - c.feeRecipientLock.Lock() - feeRecipient := c.feeRecipient - c.feeRecipientLock.Unlock() - - // Reset to CurrentBlock in case of the chain was rewound - if header := c.eth.BlockChain().CurrentBlock(); c.curForkchoiceState.HeadBlockHash != header.Hash() { - finalizedHash := c.finalizedBlockHash(header.Number.Uint64()) - c.setCurrentState(header.Hash(), *finalizedHash) - } - - var random [32]byte - rand.Read(random[:]) - fcResponse, err := c.engineAPI.ForkchoiceUpdatedV2(c.curForkchoiceState, &engine.PayloadAttributes{ - Timestamp: tstamp, - SuggestedFeeRecipient: feeRecipient, - Withdrawals: withdrawals, - Random: random, - }) - if err != nil { - return err - } - if fcResponse == engine.STATUS_SYNCING { - return errors.New("chain rewind prevented invocation of payload creation") - } - - envelope, err := c.engineAPI.getPayload(*fcResponse.PayloadID, true) - if err != nil { - return err - } - payload := envelope.ExecutionPayload - - var finalizedHash common.Hash - if payload.Number%devEpochLength == 0 { - finalizedHash = payload.BlockHash - } else { - if fh := c.finalizedBlockHash(payload.Number); fh == nil { - return errors.New("chain rewind interrupted calculation of finalized block hash") - } else { - finalizedHash = *fh - } - } - - // Mark the payload as canon - if _, err = c.engineAPI.NewPayloadV2(*payload); err != nil { - return err - } - c.setCurrentState(payload.BlockHash, finalizedHash) - // Mark the block containing the payload as canonical - if _, err = c.engineAPI.ForkchoiceUpdatedV2(c.curForkchoiceState, nil); err != nil { - return err - } - c.lastBlockTime = payload.Timestamp - return nil -} - -// loopOnDemand runs the block production loop for "on-demand" configuration (period = 0) -func (c *SimulatedBeacon) loopOnDemand() { - var ( - newTxs = make(chan core.NewTxsEvent) - sub = c.eth.TxPool().SubscribeTransactions(newTxs, true) - ) - defer sub.Unsubscribe() - - for { - select { - case <-c.shutdownCh: - return - case w := <-c.withdrawals.pending: - withdrawals := append(c.withdrawals.gatherPending(9), w) - if err := c.sealBlock(withdrawals); err != nil { - log.Warn("Error performing sealing work", "err", err) - } - case <-newTxs: - withdrawals := c.withdrawals.gatherPending(10) - if err := c.sealBlock(withdrawals); err != nil { - log.Warn("Error performing sealing work", "err", err) - } - } - } -} - -// loop runs the block production loop for non-zero period configuration -func (c *SimulatedBeacon) loop() { - timer := time.NewTimer(0) - for { - select { - case <-c.shutdownCh: - return - case <-timer.C: - withdrawals := c.withdrawals.gatherPending(10) - if err := c.sealBlock(withdrawals); err != nil { - log.Warn("Error performing sealing work", "err", err) - } else { - timer.Reset(time.Second * time.Duration(c.period)) - } - } - } -} - -// finalizedBlockHash returns the block hash of the finalized block corresponding to the given number -// or nil if doesn't exist in the chain. -func (c *SimulatedBeacon) finalizedBlockHash(number uint64) *common.Hash { - var finalizedNumber uint64 - if number%devEpochLength == 0 { - finalizedNumber = number - } else { - finalizedNumber = (number - 1) / devEpochLength * devEpochLength - } - - if finalizedBlock := c.eth.BlockChain().GetBlockByNumber(finalizedNumber); finalizedBlock != nil { - fh := finalizedBlock.Hash() - return &fh - } - return nil -} - -// setCurrentState sets the current forkchoice state -func (c *SimulatedBeacon) setCurrentState(headHash, finalizedHash common.Hash) { - c.curForkchoiceState = engine.ForkchoiceStateV1{ - HeadBlockHash: headHash, - SafeBlockHash: headHash, - FinalizedBlockHash: finalizedHash, - } -} - -func RegisterSimulatedBeaconAPIs(stack *node.Node, sim *SimulatedBeacon) { - stack.RegisterAPIs([]rpc.API{ - { - Namespace: "dev", - Service: &api{sim}, - Version: "1.0", - }, - }) -} diff --git a/eth/catalyst/simulated_beacon_api.go b/eth/catalyst/simulated_beacon_api.go deleted file mode 100644 index 93670257f6..0000000000 --- a/eth/catalyst/simulated_beacon_api.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2023 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 . - -package catalyst - -import ( - "context" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" -) - -type api struct { - simBeacon *SimulatedBeacon -} - -func (a *api) AddWithdrawal(ctx context.Context, withdrawal *types.Withdrawal) error { - return a.simBeacon.withdrawals.add(withdrawal) -} - -func (a *api) SetFeeRecipient(ctx context.Context, feeRecipient common.Address) { - a.simBeacon.setFeeRecipient(feeRecipient) -} diff --git a/eth/catalyst/simulated_beacon_test.go b/eth/catalyst/simulated_beacon_test.go deleted file mode 100644 index 6fa97ad87a..0000000000 --- a/eth/catalyst/simulated_beacon_test.go +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright 2023 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 . - -package catalyst - -import ( - "context" - "math/big" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/eth" - "github.com/ethereum/go-ethereum/eth/downloader" - "github.com/ethereum/go-ethereum/eth/ethconfig" - "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/params" -) - -func startSimulatedBeaconEthService(t *testing.T, genesis *core.Genesis) (*node.Node, *eth.Ethereum, *SimulatedBeacon) { - t.Helper() - - n, err := node.New(&node.Config{ - P2P: p2p.Config{ - ListenAddr: "127.0.0.1:8545", - NoDiscovery: true, - MaxPeers: 0, - }, - }) - if err != nil { - t.Fatal("can't create node:", err) - } - - ethcfg := ðconfig.Config{Genesis: genesis, SyncMode: downloader.FullSync, TrieTimeout: time.Minute, TrieDirtyCache: 256, TrieCleanCache: 256} - ethservice, err := eth.New(n, ethcfg) - if err != nil { - t.Fatal("can't create eth service:", err) - } - - simBeacon, err := NewSimulatedBeacon(1, ethservice) - if err != nil { - t.Fatal("can't create simulated beacon:", err) - } - - n.RegisterLifecycle(simBeacon) - - if err := n.Start(); err != nil { - t.Fatal("can't start node:", err) - } - - ethservice.SetSynced() - return n, ethservice, simBeacon -} - -// send 20 transactions, >10 withdrawals and ensure they are included in order -// send enough transactions to fill multiple blocks -func TestSimulatedBeaconSendWithdrawals(t *testing.T) { - var withdrawals []types.Withdrawal - txs := make(map[common.Hash]types.Transaction) - - var ( - // testKey is a private key to use for funding a tester account. - testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") - - // testAddr is the Ethereum address of the tester account. - testAddr = crypto.PubkeyToAddress(testKey.PublicKey) - ) - - // short period (1 second) for testing purposes - var gasLimit uint64 = 10_000_000 - genesis := core.DeveloperGenesisBlock(gasLimit, &testAddr) - node, ethService, mock := startSimulatedBeaconEthService(t, genesis) - _ = mock - defer node.Close() - - chainHeadCh := make(chan core.ChainHeadEvent, 10) - subscription := ethService.BlockChain().SubscribeChainHeadEvent(chainHeadCh) - defer subscription.Unsubscribe() - - // generate some withdrawals - for i := 0; i < 20; i++ { - withdrawals = append(withdrawals, types.Withdrawal{Index: uint64(i)}) - if err := mock.withdrawals.add(&withdrawals[i]); err != nil { - t.Fatal("addWithdrawal failed", err) - } - } - - // generate a bunch of transactions - signer := types.NewEIP155Signer(ethService.BlockChain().Config().ChainID) - for i := 0; i < 20; i++ { - tx, err := types.SignTx(types.NewTransaction(uint64(i), common.Address{}, big.NewInt(1000), params.TxGas, big.NewInt(params.InitialBaseFee), nil), signer, testKey) - if err != nil { - t.Fatalf("error signing transaction, err=%v", err) - } - txs[tx.Hash()] = *tx - - if err := ethService.APIBackend.SendTx(context.Background(), tx); err != nil { - t.Fatal("SendTx failed", err) - } - } - - includedTxs := make(map[common.Hash]struct{}) - var includedWithdrawals []uint64 - - timer := time.NewTimer(12 * time.Second) - for { - select { - case evt := <-chainHeadCh: - for _, includedTx := range evt.Block.Transactions() { - includedTxs[includedTx.Hash()] = struct{}{} - } - for _, includedWithdrawal := range evt.Block.Withdrawals() { - includedWithdrawals = append(includedWithdrawals, includedWithdrawal.Index) - } - - // ensure all withdrawals/txs included. this will take two blocks b/c number of withdrawals > 10 - if len(includedTxs) == len(txs) && len(includedWithdrawals) == len(withdrawals) && evt.Block.Number().Cmp(big.NewInt(2)) == 0 { - return - } - case <-timer.C: - t.Fatal("timed out without including all withdrawals/txs") - } - } -} diff --git a/eth/catalyst/tester.go b/eth/catalyst/tester.go deleted file mode 100644 index 0922ac0ba6..0000000000 --- a/eth/catalyst/tester.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2022 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 . - -package catalyst - -import ( - "sync" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/eth" - "github.com/ethereum/go-ethereum/eth/downloader" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/node" -) - -// FullSyncTester is an auxiliary service that allows Geth to perform full sync -// alone without consensus-layer attached. Users must specify a valid block hash -// as the sync target. -// -// This tester can be applied to different networks, no matter it's pre-merge or -// post-merge, but only for full-sync. -type FullSyncTester struct { - stack *node.Node - backend *eth.Ethereum - target common.Hash - closed chan struct{} - wg sync.WaitGroup -} - -// RegisterFullSyncTester registers the full-sync tester service into the node -// stack for launching and stopping the service controlled by node. -func RegisterFullSyncTester(stack *node.Node, backend *eth.Ethereum, target common.Hash) (*FullSyncTester, error) { - cl := &FullSyncTester{ - stack: stack, - backend: backend, - target: target, - closed: make(chan struct{}), - } - stack.RegisterLifecycle(cl) - return cl, nil -} - -// Start launches the beacon sync with provided sync target. -func (tester *FullSyncTester) Start() error { - tester.wg.Add(1) - go func() { - defer tester.wg.Done() - - // Trigger beacon sync with the provided block hash as trusted - // chain head. - err := tester.backend.Downloader().BeaconDevSync(downloader.FullSync, tester.target, tester.closed) - if err != nil { - log.Info("Failed to trigger beacon sync", "err", err) - } - - ticker := time.NewTicker(time.Second * 5) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - // Stop in case the target block is already stored locally. - if block := tester.backend.BlockChain().GetBlockByHash(tester.target); block != nil { - log.Info("Full-sync target reached", "number", block.NumberU64(), "hash", block.Hash()) - go tester.stack.Close() // async since we need to close ourselves - return - } - - case <-tester.closed: - return - } - } - }() - return nil -} - -// Stop stops the full-sync tester to stop all background activities. -// This function can only be called for one time. -func (tester *FullSyncTester) Stop() error { - close(tester.closed) - tester.wg.Wait() - return nil -} diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go deleted file mode 100644 index ad664afb5b..0000000000 --- a/eth/ethconfig/config.go +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright 2021 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 . - -// Package ethconfig contains the configuration of the ETH and LES protocols. -package ethconfig - -import ( - "errors" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/consensus" - "github.com/ethereum/go-ethereum/consensus/beacon" - "github.com/ethereum/go-ethereum/consensus/clique" - "github.com/ethereum/go-ethereum/consensus/ethash" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/txpool/blobpool" - "github.com/ethereum/go-ethereum/core/txpool/legacypool" - "github.com/ethereum/go-ethereum/eth/downloader" - "github.com/ethereum/go-ethereum/eth/gasprice" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/miner" - "github.com/ethereum/go-ethereum/params" -) - -// FullNodeGPO contains default gasprice oracle settings for full node. -var FullNodeGPO = gasprice.Config{ - Blocks: 20, - Percentile: 60, - MaxHeaderHistory: 1024, - MaxBlockHistory: 1024, - MaxPrice: gasprice.DefaultMaxPrice, - IgnorePrice: gasprice.DefaultIgnorePrice, -} - -// Defaults contains default settings for use on the Ethereum main net. -var Defaults = Config{ - SyncMode: downloader.SnapSync, - NetworkId: 0, // enable auto configuration of networkID == chainID - TxLookupLimit: 2350000, - TransactionHistory: 2350000, - StateHistory: params.FullImmutabilityThreshold, - LightPeers: 100, - DatabaseCache: 512, - TrieCleanCache: 154, - TrieDirtyCache: 256, - TrieTimeout: 60 * time.Minute, - SnapshotCache: 102, - FilterLogCacheSize: 32, - Miner: miner.DefaultConfig, - TxPool: legacypool.DefaultConfig, - BlobPool: blobpool.DefaultConfig, - RPCGasCap: 50000000, - RPCEVMTimeout: 5 * time.Second, - GPO: FullNodeGPO, - RPCTxFeeCap: 1, // 1 ether -} - -//go:generate go run github.com/fjl/gencodec -type Config -formats toml -out gen_config.go - -// Config contains configuration options for ETH and LES protocols. -type Config struct { - // The genesis block, which is inserted if the database is empty. - // If nil, the Ethereum main net block is used. - Genesis *core.Genesis `toml:",omitempty"` - - // Network ID separates blockchains on the peer-to-peer networking level. When left - // zero, the chain ID is used as network ID. - NetworkId uint64 - SyncMode downloader.SyncMode - - // This can be set to list of enrtree:// URLs which will be queried for - // for nodes to connect to. - EthDiscoveryURLs []string - SnapDiscoveryURLs []string - - NoPruning bool // Whether to disable pruning and flush everything to disk - NoPrefetch bool // Whether to disable prefetching and only load state on demand - - // Deprecated, use 'TransactionHistory' instead. - TxLookupLimit uint64 `toml:",omitempty"` // The maximum number of blocks from head whose tx indices are reserved. - TransactionHistory uint64 `toml:",omitempty"` // The maximum number of blocks from head whose tx indices are reserved. - StateHistory uint64 `toml:",omitempty"` // The maximum number of blocks from head whose state histories are reserved. - - // State scheme represents the scheme used to store ethereum states and trie - // nodes on top. It can be 'hash', 'path', or none which means use the scheme - // consistent with persistent state. - StateScheme string `toml:",omitempty"` - - // RequiredBlocks is a set of block number -> hash mappings which must be in the - // canonical chain of all remote peers. Setting the option makes geth verify the - // presence of these blocks for every new peer connection. - RequiredBlocks map[uint64]common.Hash `toml:"-"` - - // Light client options - LightServ int `toml:",omitempty"` // Maximum percentage of time allowed for serving LES requests - LightIngress int `toml:",omitempty"` // Incoming bandwidth limit for light servers - LightEgress int `toml:",omitempty"` // Outgoing bandwidth limit for light servers - LightPeers int `toml:",omitempty"` // Maximum number of LES client peers - LightNoPrune bool `toml:",omitempty"` // Whether to disable light chain pruning - LightNoSyncServe bool `toml:",omitempty"` // Whether to serve light clients before syncing - - // Database options - SkipBcVersionCheck bool `toml:"-"` - DatabaseHandles int `toml:"-"` - DatabaseCache int - DatabaseFreezer string - - TrieCleanCache int - TrieDirtyCache int - TrieTimeout time.Duration - SnapshotCache int - Preimages bool - - // This is the number of blocks for which logs will be cached in the filter system. - FilterLogCacheSize int - - // Mining options - Miner miner.Config - - // Transaction pool options - TxPool legacypool.Config - BlobPool blobpool.Config - - // Gas Price Oracle options - GPO gasprice.Config - - // Enables tracking of SHA3 preimages in the VM - EnablePreimageRecording bool - - // Miscellaneous options - DocRoot string `toml:"-"` - - // RPCGasCap is the global gas cap for eth-call variants. - RPCGasCap uint64 - - // RPCEVMTimeout is the global timeout for eth-call. - RPCEVMTimeout time.Duration - - // RPCTxFeeCap is the global transaction fee(price * gaslimit) cap for - // send-transaction variants. The unit is ether. - RPCTxFeeCap float64 - - // OverrideCancun (TODO: remove after the fork) - OverrideCancun *uint64 `toml:",omitempty"` - - // OverrideVerkle (TODO: remove after the fork) - OverrideVerkle *uint64 `toml:",omitempty"` -} - -// CreateConsensusEngine creates a consensus engine for the given chain config. -// Clique is allowed for now to live standalone, but ethash is forbidden and can -// only exist on already merged networks. -func CreateConsensusEngine(config *params.ChainConfig, db ethdb.Database) (consensus.Engine, error) { - // If proof-of-authority is requested, set it up - if config.Clique != nil { - return beacon.New(clique.New(config.Clique, db)), nil - } - // If defaulting to proof-of-work, enforce an already merged network since - // we cannot run PoW algorithms anymore, so we cannot even follow a chain - // not coordinated by a beacon node. - if !config.TerminalTotalDifficultyPassed { - return nil, errors.New("ethash is only supported as a historical component of already merged networks") - } - return beacon.New(ethash.NewFaker()), nil -} diff --git a/eth/ethconfig/gen_config.go b/eth/ethconfig/gen_config.go deleted file mode 100644 index 2abddc9e0d..0000000000 --- a/eth/ethconfig/gen_config.go +++ /dev/null @@ -1,268 +0,0 @@ -// Code generated by github.com/fjl/gencodec. DO NOT EDIT. - -package ethconfig - -import ( - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/txpool/blobpool" - "github.com/ethereum/go-ethereum/core/txpool/legacypool" - "github.com/ethereum/go-ethereum/eth/downloader" - "github.com/ethereum/go-ethereum/eth/gasprice" - "github.com/ethereum/go-ethereum/miner" -) - -// MarshalTOML marshals as TOML. -func (c Config) MarshalTOML() (interface{}, error) { - type Config struct { - Genesis *core.Genesis `toml:",omitempty"` - NetworkId uint64 - SyncMode downloader.SyncMode - EthDiscoveryURLs []string - SnapDiscoveryURLs []string - NoPruning bool - NoPrefetch bool - TxLookupLimit uint64 `toml:",omitempty"` - TransactionHistory uint64 `toml:",omitempty"` - StateHistory uint64 `toml:",omitempty"` - StateScheme string `toml:",omitempty"` - RequiredBlocks map[uint64]common.Hash `toml:"-"` - LightServ int `toml:",omitempty"` - LightIngress int `toml:",omitempty"` - LightEgress int `toml:",omitempty"` - LightPeers int `toml:",omitempty"` - LightNoPrune bool `toml:",omitempty"` - LightNoSyncServe bool `toml:",omitempty"` - SkipBcVersionCheck bool `toml:"-"` - DatabaseHandles int `toml:"-"` - DatabaseCache int - DatabaseFreezer string - TrieCleanCache int - TrieDirtyCache int - TrieTimeout time.Duration - SnapshotCache int - Preimages bool - FilterLogCacheSize int - Miner miner.Config - TxPool legacypool.Config - BlobPool blobpool.Config - GPO gasprice.Config - EnablePreimageRecording bool - DocRoot string `toml:"-"` - RPCGasCap uint64 - RPCEVMTimeout time.Duration - RPCTxFeeCap float64 - OverrideCancun *uint64 `toml:",omitempty"` - OverrideVerkle *uint64 `toml:",omitempty"` - } - var enc Config - enc.Genesis = c.Genesis - enc.NetworkId = c.NetworkId - enc.SyncMode = c.SyncMode - enc.EthDiscoveryURLs = c.EthDiscoveryURLs - enc.SnapDiscoveryURLs = c.SnapDiscoveryURLs - enc.NoPruning = c.NoPruning - enc.NoPrefetch = c.NoPrefetch - enc.TxLookupLimit = c.TxLookupLimit - enc.TransactionHistory = c.TransactionHistory - enc.StateHistory = c.StateHistory - enc.StateScheme = c.StateScheme - enc.RequiredBlocks = c.RequiredBlocks - enc.LightServ = c.LightServ - enc.LightIngress = c.LightIngress - enc.LightEgress = c.LightEgress - enc.LightPeers = c.LightPeers - enc.LightNoPrune = c.LightNoPrune - enc.LightNoSyncServe = c.LightNoSyncServe - enc.SkipBcVersionCheck = c.SkipBcVersionCheck - enc.DatabaseHandles = c.DatabaseHandles - enc.DatabaseCache = c.DatabaseCache - enc.DatabaseFreezer = c.DatabaseFreezer - enc.TrieCleanCache = c.TrieCleanCache - enc.TrieDirtyCache = c.TrieDirtyCache - enc.TrieTimeout = c.TrieTimeout - enc.SnapshotCache = c.SnapshotCache - enc.Preimages = c.Preimages - enc.FilterLogCacheSize = c.FilterLogCacheSize - enc.Miner = c.Miner - enc.TxPool = c.TxPool - enc.BlobPool = c.BlobPool - enc.GPO = c.GPO - enc.EnablePreimageRecording = c.EnablePreimageRecording - enc.DocRoot = c.DocRoot - enc.RPCGasCap = c.RPCGasCap - enc.RPCEVMTimeout = c.RPCEVMTimeout - enc.RPCTxFeeCap = c.RPCTxFeeCap - enc.OverrideCancun = c.OverrideCancun - enc.OverrideVerkle = c.OverrideVerkle - return &enc, nil -} - -// UnmarshalTOML unmarshals from TOML. -func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { - type Config struct { - Genesis *core.Genesis `toml:",omitempty"` - NetworkId *uint64 - SyncMode *downloader.SyncMode - EthDiscoveryURLs []string - SnapDiscoveryURLs []string - NoPruning *bool - NoPrefetch *bool - TxLookupLimit *uint64 `toml:",omitempty"` - TransactionHistory *uint64 `toml:",omitempty"` - StateHistory *uint64 `toml:",omitempty"` - StateScheme *string `toml:",omitempty"` - RequiredBlocks map[uint64]common.Hash `toml:"-"` - LightServ *int `toml:",omitempty"` - LightIngress *int `toml:",omitempty"` - LightEgress *int `toml:",omitempty"` - LightPeers *int `toml:",omitempty"` - LightNoPrune *bool `toml:",omitempty"` - LightNoSyncServe *bool `toml:",omitempty"` - SkipBcVersionCheck *bool `toml:"-"` - DatabaseHandles *int `toml:"-"` - DatabaseCache *int - DatabaseFreezer *string - TrieCleanCache *int - TrieDirtyCache *int - TrieTimeout *time.Duration - SnapshotCache *int - Preimages *bool - FilterLogCacheSize *int - Miner *miner.Config - TxPool *legacypool.Config - BlobPool *blobpool.Config - GPO *gasprice.Config - EnablePreimageRecording *bool - DocRoot *string `toml:"-"` - RPCGasCap *uint64 - RPCEVMTimeout *time.Duration - RPCTxFeeCap *float64 - OverrideCancun *uint64 `toml:",omitempty"` - OverrideVerkle *uint64 `toml:",omitempty"` - } - var dec Config - if err := unmarshal(&dec); err != nil { - return err - } - if dec.Genesis != nil { - c.Genesis = dec.Genesis - } - if dec.NetworkId != nil { - c.NetworkId = *dec.NetworkId - } - if dec.SyncMode != nil { - c.SyncMode = *dec.SyncMode - } - if dec.EthDiscoveryURLs != nil { - c.EthDiscoveryURLs = dec.EthDiscoveryURLs - } - if dec.SnapDiscoveryURLs != nil { - c.SnapDiscoveryURLs = dec.SnapDiscoveryURLs - } - if dec.NoPruning != nil { - c.NoPruning = *dec.NoPruning - } - if dec.NoPrefetch != nil { - c.NoPrefetch = *dec.NoPrefetch - } - if dec.TxLookupLimit != nil { - c.TxLookupLimit = *dec.TxLookupLimit - } - if dec.TransactionHistory != nil { - c.TransactionHistory = *dec.TransactionHistory - } - if dec.StateHistory != nil { - c.StateHistory = *dec.StateHistory - } - if dec.StateScheme != nil { - c.StateScheme = *dec.StateScheme - } - if dec.RequiredBlocks != nil { - c.RequiredBlocks = dec.RequiredBlocks - } - if dec.LightServ != nil { - c.LightServ = *dec.LightServ - } - if dec.LightIngress != nil { - c.LightIngress = *dec.LightIngress - } - if dec.LightEgress != nil { - c.LightEgress = *dec.LightEgress - } - if dec.LightPeers != nil { - c.LightPeers = *dec.LightPeers - } - if dec.LightNoPrune != nil { - c.LightNoPrune = *dec.LightNoPrune - } - if dec.LightNoSyncServe != nil { - c.LightNoSyncServe = *dec.LightNoSyncServe - } - if dec.SkipBcVersionCheck != nil { - c.SkipBcVersionCheck = *dec.SkipBcVersionCheck - } - if dec.DatabaseHandles != nil { - c.DatabaseHandles = *dec.DatabaseHandles - } - if dec.DatabaseCache != nil { - c.DatabaseCache = *dec.DatabaseCache - } - if dec.DatabaseFreezer != nil { - c.DatabaseFreezer = *dec.DatabaseFreezer - } - if dec.TrieCleanCache != nil { - c.TrieCleanCache = *dec.TrieCleanCache - } - if dec.TrieDirtyCache != nil { - c.TrieDirtyCache = *dec.TrieDirtyCache - } - if dec.TrieTimeout != nil { - c.TrieTimeout = *dec.TrieTimeout - } - if dec.SnapshotCache != nil { - c.SnapshotCache = *dec.SnapshotCache - } - if dec.Preimages != nil { - c.Preimages = *dec.Preimages - } - if dec.FilterLogCacheSize != nil { - c.FilterLogCacheSize = *dec.FilterLogCacheSize - } - if dec.Miner != nil { - c.Miner = *dec.Miner - } - if dec.TxPool != nil { - c.TxPool = *dec.TxPool - } - if dec.BlobPool != nil { - c.BlobPool = *dec.BlobPool - } - if dec.GPO != nil { - c.GPO = *dec.GPO - } - if dec.EnablePreimageRecording != nil { - c.EnablePreimageRecording = *dec.EnablePreimageRecording - } - if dec.DocRoot != nil { - c.DocRoot = *dec.DocRoot - } - if dec.RPCGasCap != nil { - c.RPCGasCap = *dec.RPCGasCap - } - if dec.RPCEVMTimeout != nil { - c.RPCEVMTimeout = *dec.RPCEVMTimeout - } - if dec.RPCTxFeeCap != nil { - c.RPCTxFeeCap = *dec.RPCTxFeeCap - } - if dec.OverrideCancun != nil { - c.OverrideCancun = dec.OverrideCancun - } - if dec.OverrideVerkle != nil { - c.OverrideVerkle = dec.OverrideVerkle - } - return nil -} diff --git a/eth/fetcher/block_fetcher.go b/eth/fetcher/block_fetcher.go deleted file mode 100644 index 126eaaea7f..0000000000 --- a/eth/fetcher/block_fetcher.go +++ /dev/null @@ -1,939 +0,0 @@ -// Copyright 2015 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 . - -// Package fetcher contains the announcement based header, blocks or transaction synchronisation. -package fetcher - -import ( - "errors" - "math/rand" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/prque" - "github.com/ethereum/go-ethereum/consensus" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/eth/protocols/eth" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/metrics" - "github.com/ethereum/go-ethereum/trie" -) - -const ( - lightTimeout = time.Millisecond // Time allowance before an announced header is explicitly requested - arriveTimeout = 500 * time.Millisecond // Time allowance before an announced block/transaction is explicitly requested - gatherSlack = 100 * time.Millisecond // Interval used to collate almost-expired announces with fetches - fetchTimeout = 5 * time.Second // Maximum allotted time to return an explicitly requested block/transaction -) - -const ( - maxUncleDist = 7 // Maximum allowed backward distance from the chain head - maxQueueDist = 32 // Maximum allowed distance from the chain head to queue - hashLimit = 256 // Maximum number of unique blocks or headers a peer may have announced - blockLimit = 64 // Maximum number of unique blocks a peer may have delivered -) - -var ( - blockAnnounceInMeter = metrics.NewRegisteredMeter("eth/fetcher/block/announces/in", nil) - blockAnnounceOutTimer = metrics.NewRegisteredTimer("eth/fetcher/block/announces/out", nil) - blockAnnounceDropMeter = metrics.NewRegisteredMeter("eth/fetcher/block/announces/drop", nil) - blockAnnounceDOSMeter = metrics.NewRegisteredMeter("eth/fetcher/block/announces/dos", nil) - - blockBroadcastInMeter = metrics.NewRegisteredMeter("eth/fetcher/block/broadcasts/in", nil) - blockBroadcastOutTimer = metrics.NewRegisteredTimer("eth/fetcher/block/broadcasts/out", nil) - blockBroadcastDropMeter = metrics.NewRegisteredMeter("eth/fetcher/block/broadcasts/drop", nil) - blockBroadcastDOSMeter = metrics.NewRegisteredMeter("eth/fetcher/block/broadcasts/dos", nil) - - headerFetchMeter = metrics.NewRegisteredMeter("eth/fetcher/block/headers", nil) - bodyFetchMeter = metrics.NewRegisteredMeter("eth/fetcher/block/bodies", nil) - - headerFilterInMeter = metrics.NewRegisteredMeter("eth/fetcher/block/filter/headers/in", nil) - headerFilterOutMeter = metrics.NewRegisteredMeter("eth/fetcher/block/filter/headers/out", nil) - bodyFilterInMeter = metrics.NewRegisteredMeter("eth/fetcher/block/filter/bodies/in", nil) - bodyFilterOutMeter = metrics.NewRegisteredMeter("eth/fetcher/block/filter/bodies/out", nil) -) - -var errTerminated = errors.New("terminated") - -// HeaderRetrievalFn is a callback type for retrieving a header from the local chain. -type HeaderRetrievalFn func(common.Hash) *types.Header - -// blockRetrievalFn is a callback type for retrieving a block from the local chain. -type blockRetrievalFn func(common.Hash) *types.Block - -// headerRequesterFn is a callback type for sending a header retrieval request. -type headerRequesterFn func(common.Hash, chan *eth.Response) (*eth.Request, error) - -// bodyRequesterFn is a callback type for sending a body retrieval request. -type bodyRequesterFn func([]common.Hash, chan *eth.Response) (*eth.Request, error) - -// headerVerifierFn is a callback type to verify a block's header for fast propagation. -type headerVerifierFn func(header *types.Header) error - -// blockBroadcasterFn is a callback type for broadcasting a block to connected peers. -type blockBroadcasterFn func(block *types.Block, propagate bool) - -// chainHeightFn is a callback type to retrieve the current chain height. -type chainHeightFn func() uint64 - -// headersInsertFn is a callback type to insert a batch of headers into the local chain. -type headersInsertFn func(headers []*types.Header) (int, error) - -// chainInsertFn is a callback type to insert a batch of blocks into the local chain. -type chainInsertFn func(types.Blocks) (int, error) - -// peerDropFn is a callback type for dropping a peer detected as malicious. -type peerDropFn func(id string) - -// blockAnnounce is the hash notification of the availability of a new block in the -// network. -type blockAnnounce struct { - hash common.Hash // Hash of the block being announced - number uint64 // Number of the block being announced (0 = unknown | old protocol) - header *types.Header // Header of the block partially reassembled (new protocol) - time time.Time // Timestamp of the announcement - - origin string // Identifier of the peer originating the notification - - fetchHeader headerRequesterFn // Fetcher function to retrieve the header of an announced block - fetchBodies bodyRequesterFn // Fetcher function to retrieve the body of an announced block -} - -// headerFilterTask represents a batch of headers needing fetcher filtering. -type headerFilterTask struct { - peer string // The source peer of block headers - headers []*types.Header // Collection of headers to filter - time time.Time // Arrival time of the headers -} - -// bodyFilterTask represents a batch of block bodies (transactions and uncles) -// needing fetcher filtering. -type bodyFilterTask struct { - peer string // The source peer of block bodies - transactions [][]*types.Transaction // Collection of transactions per block bodies - uncles [][]*types.Header // Collection of uncles per block bodies - time time.Time // Arrival time of the blocks' contents -} - -// blockOrHeaderInject represents a schedules import operation. -type blockOrHeaderInject struct { - origin string - - header *types.Header // Used for light mode fetcher which only cares about header. - block *types.Block // Used for normal mode fetcher which imports full block. -} - -// number returns the block number of the injected object. -func (inject *blockOrHeaderInject) number() uint64 { - if inject.header != nil { - return inject.header.Number.Uint64() - } - return inject.block.NumberU64() -} - -// number returns the block hash of the injected object. -func (inject *blockOrHeaderInject) hash() common.Hash { - if inject.header != nil { - return inject.header.Hash() - } - return inject.block.Hash() -} - -// BlockFetcher is responsible for accumulating block announcements from various peers -// and scheduling them for retrieval. -type BlockFetcher struct { - light bool // The indicator whether it's a light fetcher or normal one. - - // Various event channels - notify chan *blockAnnounce - inject chan *blockOrHeaderInject - - headerFilter chan chan *headerFilterTask - bodyFilter chan chan *bodyFilterTask - - done chan common.Hash - quit chan struct{} - - // Announce states - announces map[string]int // Per peer blockAnnounce counts to prevent memory exhaustion - announced map[common.Hash][]*blockAnnounce // Announced blocks, scheduled for fetching - fetching map[common.Hash]*blockAnnounce // Announced blocks, currently fetching - fetched map[common.Hash][]*blockAnnounce // Blocks with headers fetched, scheduled for body retrieval - completing map[common.Hash]*blockAnnounce // Blocks with headers, currently body-completing - - // Block cache - queue *prque.Prque[int64, *blockOrHeaderInject] // Queue containing the import operations (block number sorted) - queues map[string]int // Per peer block counts to prevent memory exhaustion - queued map[common.Hash]*blockOrHeaderInject // Set of already queued blocks (to dedup imports) - - // Callbacks - getHeader HeaderRetrievalFn // Retrieves a header from the local chain - getBlock blockRetrievalFn // Retrieves a block from the local chain - verifyHeader headerVerifierFn // Checks if a block's headers have a valid proof of work - broadcastBlock blockBroadcasterFn // Broadcasts a block to connected peers - chainHeight chainHeightFn // Retrieves the current chain's height - insertHeaders headersInsertFn // Injects a batch of headers into the chain - insertChain chainInsertFn // Injects a batch of blocks into the chain - dropPeer peerDropFn // Drops a peer for misbehaving - - // Testing hooks - announceChangeHook func(common.Hash, bool) // Method to call upon adding or deleting a hash from the blockAnnounce list - queueChangeHook func(common.Hash, bool) // Method to call upon adding or deleting a block from the import queue - fetchingHook func([]common.Hash) // Method to call upon starting a block (eth/61) or header (eth/62) fetch - completingHook func([]common.Hash) // Method to call upon starting a block body fetch (eth/62) - importedHook func(*types.Header, *types.Block) // Method to call upon successful header or block import (both eth/61 and eth/62) -} - -// NewBlockFetcher creates a block fetcher to retrieve blocks based on hash announcements. -func NewBlockFetcher(light bool, getHeader HeaderRetrievalFn, getBlock blockRetrievalFn, verifyHeader headerVerifierFn, broadcastBlock blockBroadcasterFn, chainHeight chainHeightFn, insertHeaders headersInsertFn, insertChain chainInsertFn, dropPeer peerDropFn) *BlockFetcher { - return &BlockFetcher{ - light: light, - notify: make(chan *blockAnnounce), - inject: make(chan *blockOrHeaderInject), - headerFilter: make(chan chan *headerFilterTask), - bodyFilter: make(chan chan *bodyFilterTask), - done: make(chan common.Hash), - quit: make(chan struct{}), - announces: make(map[string]int), - announced: make(map[common.Hash][]*blockAnnounce), - fetching: make(map[common.Hash]*blockAnnounce), - fetched: make(map[common.Hash][]*blockAnnounce), - completing: make(map[common.Hash]*blockAnnounce), - queue: prque.New[int64, *blockOrHeaderInject](nil), - queues: make(map[string]int), - queued: make(map[common.Hash]*blockOrHeaderInject), - getHeader: getHeader, - getBlock: getBlock, - verifyHeader: verifyHeader, - broadcastBlock: broadcastBlock, - chainHeight: chainHeight, - insertHeaders: insertHeaders, - insertChain: insertChain, - dropPeer: dropPeer, - } -} - -// Start boots up the announcement based synchroniser, accepting and processing -// hash notifications and block fetches until termination requested. -func (f *BlockFetcher) Start() { - go f.loop() -} - -// Stop terminates the announcement based synchroniser, canceling all pending -// operations. -func (f *BlockFetcher) Stop() { - close(f.quit) -} - -// Notify announces the fetcher of the potential availability of a new block in -// the network. -func (f *BlockFetcher) Notify(peer string, hash common.Hash, number uint64, time time.Time, - headerFetcher headerRequesterFn, bodyFetcher bodyRequesterFn) error { - block := &blockAnnounce{ - hash: hash, - number: number, - time: time, - origin: peer, - fetchHeader: headerFetcher, - fetchBodies: bodyFetcher, - } - select { - case f.notify <- block: - return nil - case <-f.quit: - return errTerminated - } -} - -// Enqueue tries to fill gaps the fetcher's future import queue. -func (f *BlockFetcher) Enqueue(peer string, block *types.Block) error { - op := &blockOrHeaderInject{ - origin: peer, - block: block, - } - select { - case f.inject <- op: - return nil - case <-f.quit: - return errTerminated - } -} - -// FilterHeaders extracts all the headers that were explicitly requested by the fetcher, -// returning those that should be handled differently. -func (f *BlockFetcher) FilterHeaders(peer string, headers []*types.Header, time time.Time) []*types.Header { - log.Trace("Filtering headers", "peer", peer, "headers", len(headers)) - - // Send the filter channel to the fetcher - filter := make(chan *headerFilterTask) - - select { - case f.headerFilter <- filter: - case <-f.quit: - return nil - } - // Request the filtering of the header list - select { - case filter <- &headerFilterTask{peer: peer, headers: headers, time: time}: - case <-f.quit: - return nil - } - // Retrieve the headers remaining after filtering - select { - case task := <-filter: - return task.headers - case <-f.quit: - return nil - } -} - -// FilterBodies extracts all the block bodies that were explicitly requested by -// the fetcher, returning those that should be handled differently. -func (f *BlockFetcher) FilterBodies(peer string, transactions [][]*types.Transaction, uncles [][]*types.Header, time time.Time) ([][]*types.Transaction, [][]*types.Header) { - log.Trace("Filtering bodies", "peer", peer, "txs", len(transactions), "uncles", len(uncles)) - - // Send the filter channel to the fetcher - filter := make(chan *bodyFilterTask) - - select { - case f.bodyFilter <- filter: - case <-f.quit: - return nil, nil - } - // Request the filtering of the body list - select { - case filter <- &bodyFilterTask{peer: peer, transactions: transactions, uncles: uncles, time: time}: - case <-f.quit: - return nil, nil - } - // Retrieve the bodies remaining after filtering - select { - case task := <-filter: - return task.transactions, task.uncles - case <-f.quit: - return nil, nil - } -} - -// Loop is the main fetcher loop, checking and processing various notification -// events. -func (f *BlockFetcher) loop() { - // Iterate the block fetching until a quit is requested - var ( - fetchTimer = time.NewTimer(0) - completeTimer = time.NewTimer(0) - ) - <-fetchTimer.C // clear out the channel - <-completeTimer.C - defer fetchTimer.Stop() - defer completeTimer.Stop() - - for { - // Clean up any expired block fetches - for hash, announce := range f.fetching { - if time.Since(announce.time) > fetchTimeout { - f.forgetHash(hash) - } - } - // Import any queued blocks that could potentially fit - height := f.chainHeight() - for !f.queue.Empty() { - op := f.queue.PopItem() - hash := op.hash() - if f.queueChangeHook != nil { - f.queueChangeHook(hash, false) - } - // If too high up the chain or phase, continue later - number := op.number() - if number > height+1 { - f.queue.Push(op, -int64(number)) - if f.queueChangeHook != nil { - f.queueChangeHook(hash, true) - } - break - } - // Otherwise if fresh and still unknown, try and import - if (number+maxUncleDist < height) || (f.light && f.getHeader(hash) != nil) || (!f.light && f.getBlock(hash) != nil) { - f.forgetBlock(hash) - continue - } - if f.light { - f.importHeaders(op.origin, op.header) - } else { - f.importBlocks(op.origin, op.block) - } - } - // Wait for an outside event to occur - select { - case <-f.quit: - // BlockFetcher terminating, abort all operations - return - - case notification := <-f.notify: - // A block was announced, make sure the peer isn't DOSing us - blockAnnounceInMeter.Mark(1) - - count := f.announces[notification.origin] + 1 - if count > hashLimit { - log.Debug("Peer exceeded outstanding announces", "peer", notification.origin, "limit", hashLimit) - blockAnnounceDOSMeter.Mark(1) - break - } - if notification.number == 0 { - break - } - // If we have a valid block number, check that it's potentially useful - if dist := int64(notification.number) - int64(f.chainHeight()); dist < -maxUncleDist || dist > maxQueueDist { - log.Debug("Peer discarded announcement", "peer", notification.origin, "number", notification.number, "hash", notification.hash, "distance", dist) - blockAnnounceDropMeter.Mark(1) - break - } - // All is well, schedule the announce if block's not yet downloading - if _, ok := f.fetching[notification.hash]; ok { - break - } - if _, ok := f.completing[notification.hash]; ok { - break - } - f.announces[notification.origin] = count - f.announced[notification.hash] = append(f.announced[notification.hash], notification) - if f.announceChangeHook != nil && len(f.announced[notification.hash]) == 1 { - f.announceChangeHook(notification.hash, true) - } - if len(f.announced) == 1 { - f.rescheduleFetch(fetchTimer) - } - - case op := <-f.inject: - // A direct block insertion was requested, try and fill any pending gaps - blockBroadcastInMeter.Mark(1) - - // Now only direct block injection is allowed, drop the header injection - // here silently if we receive. - if f.light { - continue - } - f.enqueue(op.origin, nil, op.block) - - case hash := <-f.done: - // A pending import finished, remove all traces of the notification - f.forgetHash(hash) - f.forgetBlock(hash) - - case <-fetchTimer.C: - // At least one block's timer ran out, check for needing retrieval - request := make(map[string][]common.Hash) - - for hash, announces := range f.announced { - // In current LES protocol(les2/les3), only header announce is - // available, no need to wait too much time for header broadcast. - timeout := arriveTimeout - gatherSlack - if f.light { - timeout = 0 - } - if time.Since(announces[0].time) > timeout { - // Pick a random peer to retrieve from, reset all others - announce := announces[rand.Intn(len(announces))] - f.forgetHash(hash) - - // If the block still didn't arrive, queue for fetching - if (f.light && f.getHeader(hash) == nil) || (!f.light && f.getBlock(hash) == nil) { - request[announce.origin] = append(request[announce.origin], hash) - f.fetching[hash] = announce - } - } - } - // Send out all block header requests - for peer, hashes := range request { - log.Trace("Fetching scheduled headers", "peer", peer, "list", hashes) - - // Create a closure of the fetch and schedule in on a new thread - fetchHeader, hashes := f.fetching[hashes[0]].fetchHeader, hashes - go func(peer string) { - if f.fetchingHook != nil { - f.fetchingHook(hashes) - } - for _, hash := range hashes { - headerFetchMeter.Mark(1) - go func(hash common.Hash) { - resCh := make(chan *eth.Response) - - req, err := fetchHeader(hash, resCh) - if err != nil { - return // Legacy code, yolo - } - defer req.Close() - - timeout := time.NewTimer(2 * fetchTimeout) // 2x leeway before dropping the peer - defer timeout.Stop() - - select { - case res := <-resCh: - res.Done <- nil - f.FilterHeaders(peer, *res.Res.(*eth.BlockHeadersRequest), time.Now()) - - case <-timeout.C: - // The peer didn't respond in time. The request - // was already rescheduled at this point, we were - // waiting for a catchup. With an unresponsive - // peer however, it's a protocol violation. - f.dropPeer(peer) - } - }(hash) - } - }(peer) - } - // Schedule the next fetch if blocks are still pending - f.rescheduleFetch(fetchTimer) - - case <-completeTimer.C: - // At least one header's timer ran out, retrieve everything - request := make(map[string][]common.Hash) - - for hash, announces := range f.fetched { - // Pick a random peer to retrieve from, reset all others - announce := announces[rand.Intn(len(announces))] - f.forgetHash(hash) - - // If the block still didn't arrive, queue for completion - if f.getBlock(hash) == nil { - request[announce.origin] = append(request[announce.origin], hash) - f.completing[hash] = announce - } - } - // Send out all block body requests - for peer, hashes := range request { - log.Trace("Fetching scheduled bodies", "peer", peer, "list", hashes) - - // Create a closure of the fetch and schedule in on a new thread - if f.completingHook != nil { - f.completingHook(hashes) - } - fetchBodies := f.completing[hashes[0]].fetchBodies - bodyFetchMeter.Mark(int64(len(hashes))) - - go func(peer string, hashes []common.Hash) { - resCh := make(chan *eth.Response) - - req, err := fetchBodies(hashes, resCh) - if err != nil { - return // Legacy code, yolo - } - defer req.Close() - - timeout := time.NewTimer(2 * fetchTimeout) // 2x leeway before dropping the peer - defer timeout.Stop() - - select { - case res := <-resCh: - res.Done <- nil - // Ignoring withdrawals here, since the block fetcher is not used post-merge. - txs, uncles, _ := res.Res.(*eth.BlockBodiesResponse).Unpack() - f.FilterBodies(peer, txs, uncles, time.Now()) - - case <-timeout.C: - // The peer didn't respond in time. The request - // was already rescheduled at this point, we were - // waiting for a catchup. With an unresponsive - // peer however, it's a protocol violation. - f.dropPeer(peer) - } - }(peer, hashes) - } - // Schedule the next fetch if blocks are still pending - f.rescheduleComplete(completeTimer) - - case filter := <-f.headerFilter: - // Headers arrived from a remote peer. Extract those that were explicitly - // requested by the fetcher, and return everything else so it's delivered - // to other parts of the system. - var task *headerFilterTask - select { - case task = <-filter: - case <-f.quit: - return - } - headerFilterInMeter.Mark(int64(len(task.headers))) - - // Split the batch of headers into unknown ones (to return to the caller), - // known incomplete ones (requiring body retrievals) and completed blocks. - unknown, incomplete, complete, lightHeaders := []*types.Header{}, []*blockAnnounce{}, []*types.Block{}, []*blockAnnounce{} - for _, header := range task.headers { - hash := header.Hash() - - // Filter fetcher-requested headers from other synchronisation algorithms - if announce := f.fetching[hash]; announce != nil && announce.origin == task.peer && f.fetched[hash] == nil && f.completing[hash] == nil && f.queued[hash] == nil { - // If the delivered header does not match the promised number, drop the announcer - if header.Number.Uint64() != announce.number { - log.Trace("Invalid block number fetched", "peer", announce.origin, "hash", header.Hash(), "announced", announce.number, "provided", header.Number) - f.dropPeer(announce.origin) - f.forgetHash(hash) - continue - } - // Collect all headers only if we are running in light - // mode and the headers are not imported by other means. - if f.light { - if f.getHeader(hash) == nil { - announce.header = header - lightHeaders = append(lightHeaders, announce) - } - f.forgetHash(hash) - continue - } - // Only keep if not imported by other means - if f.getBlock(hash) == nil { - announce.header = header - announce.time = task.time - - // If the block is empty (header only), short circuit into the final import queue - if header.TxHash == types.EmptyTxsHash && header.UncleHash == types.EmptyUncleHash { - log.Trace("Block empty, skipping body retrieval", "peer", announce.origin, "number", header.Number, "hash", header.Hash()) - - block := types.NewBlockWithHeader(header) - block.ReceivedAt = task.time - - complete = append(complete, block) - f.completing[hash] = announce - continue - } - // Otherwise add to the list of blocks needing completion - incomplete = append(incomplete, announce) - } else { - log.Trace("Block already imported, discarding header", "peer", announce.origin, "number", header.Number, "hash", header.Hash()) - f.forgetHash(hash) - } - } else { - // BlockFetcher doesn't know about it, add to the return list - unknown = append(unknown, header) - } - } - headerFilterOutMeter.Mark(int64(len(unknown))) - select { - case filter <- &headerFilterTask{headers: unknown, time: task.time}: - case <-f.quit: - return - } - // Schedule the retrieved headers for body completion - for _, announce := range incomplete { - hash := announce.header.Hash() - if _, ok := f.completing[hash]; ok { - continue - } - f.fetched[hash] = append(f.fetched[hash], announce) - if len(f.fetched) == 1 { - f.rescheduleComplete(completeTimer) - } - } - // Schedule the header for light fetcher import - for _, announce := range lightHeaders { - f.enqueue(announce.origin, announce.header, nil) - } - // Schedule the header-only blocks for import - for _, block := range complete { - if announce := f.completing[block.Hash()]; announce != nil { - f.enqueue(announce.origin, nil, block) - } - } - - case filter := <-f.bodyFilter: - // Block bodies arrived, extract any explicitly requested blocks, return the rest - var task *bodyFilterTask - select { - case task = <-filter: - case <-f.quit: - return - } - bodyFilterInMeter.Mark(int64(len(task.transactions))) - blocks := []*types.Block{} - // abort early if there's nothing explicitly requested - if len(f.completing) > 0 { - for i := 0; i < len(task.transactions) && i < len(task.uncles); i++ { - // Match up a body to any possible completion request - var ( - matched = false - uncleHash common.Hash // calculated lazily and reused - txnHash common.Hash // calculated lazily and reused - ) - for hash, announce := range f.completing { - if f.queued[hash] != nil || announce.origin != task.peer { - continue - } - if uncleHash == (common.Hash{}) { - uncleHash = types.CalcUncleHash(task.uncles[i]) - } - if uncleHash != announce.header.UncleHash { - continue - } - if txnHash == (common.Hash{}) { - txnHash = types.DeriveSha(types.Transactions(task.transactions[i]), trie.NewStackTrie(nil)) - } - if txnHash != announce.header.TxHash { - continue - } - // Mark the body matched, reassemble if still unknown - matched = true - if f.getBlock(hash) == nil { - block := types.NewBlockWithHeader(announce.header).WithBody(task.transactions[i], task.uncles[i]) - block.ReceivedAt = task.time - blocks = append(blocks, block) - } else { - f.forgetHash(hash) - } - } - if matched { - task.transactions = append(task.transactions[:i], task.transactions[i+1:]...) - task.uncles = append(task.uncles[:i], task.uncles[i+1:]...) - i-- - continue - } - } - } - bodyFilterOutMeter.Mark(int64(len(task.transactions))) - select { - case filter <- task: - case <-f.quit: - return - } - // Schedule the retrieved blocks for ordered import - for _, block := range blocks { - if announce := f.completing[block.Hash()]; announce != nil { - f.enqueue(announce.origin, nil, block) - } - } - } - } -} - -// rescheduleFetch resets the specified fetch timer to the next blockAnnounce timeout. -func (f *BlockFetcher) rescheduleFetch(fetch *time.Timer) { - // Short circuit if no blocks are announced - if len(f.announced) == 0 { - return - } - // Schedule announcement retrieval quickly for light mode - // since server won't send any headers to client. - if f.light { - fetch.Reset(lightTimeout) - return - } - // Otherwise find the earliest expiring announcement - earliest := time.Now() - for _, announces := range f.announced { - if earliest.After(announces[0].time) { - earliest = announces[0].time - } - } - fetch.Reset(arriveTimeout - time.Since(earliest)) -} - -// rescheduleComplete resets the specified completion timer to the next fetch timeout. -func (f *BlockFetcher) rescheduleComplete(complete *time.Timer) { - // Short circuit if no headers are fetched - if len(f.fetched) == 0 { - return - } - // Otherwise find the earliest expiring announcement - earliest := time.Now() - for _, announces := range f.fetched { - if earliest.After(announces[0].time) { - earliest = announces[0].time - } - } - complete.Reset(gatherSlack - time.Since(earliest)) -} - -// enqueue schedules a new header or block import operation, if the component -// to be imported has not yet been seen. -func (f *BlockFetcher) enqueue(peer string, header *types.Header, block *types.Block) { - var ( - hash common.Hash - number uint64 - ) - if header != nil { - hash, number = header.Hash(), header.Number.Uint64() - } else { - hash, number = block.Hash(), block.NumberU64() - } - // Ensure the peer isn't DOSing us - count := f.queues[peer] + 1 - if count > blockLimit { - log.Debug("Discarded delivered header or block, exceeded allowance", "peer", peer, "number", number, "hash", hash, "limit", blockLimit) - blockBroadcastDOSMeter.Mark(1) - f.forgetHash(hash) - return - } - // Discard any past or too distant blocks - if dist := int64(number) - int64(f.chainHeight()); dist < -maxUncleDist || dist > maxQueueDist { - log.Debug("Discarded delivered header or block, too far away", "peer", peer, "number", number, "hash", hash, "distance", dist) - blockBroadcastDropMeter.Mark(1) - f.forgetHash(hash) - return - } - // Schedule the block for future importing - if _, ok := f.queued[hash]; !ok { - op := &blockOrHeaderInject{origin: peer} - if header != nil { - op.header = header - } else { - op.block = block - } - f.queues[peer] = count - f.queued[hash] = op - f.queue.Push(op, -int64(number)) - if f.queueChangeHook != nil { - f.queueChangeHook(hash, true) - } - log.Debug("Queued delivered header or block", "peer", peer, "number", number, "hash", hash, "queued", f.queue.Size()) - } -} - -// importHeaders spawns a new goroutine to run a header insertion into the chain. -// If the header's number is at the same height as the current import phase, it -// updates the phase states accordingly. -func (f *BlockFetcher) importHeaders(peer string, header *types.Header) { - hash := header.Hash() - log.Debug("Importing propagated header", "peer", peer, "number", header.Number, "hash", hash) - - go func() { - defer func() { f.done <- hash }() - // If the parent's unknown, abort insertion - parent := f.getHeader(header.ParentHash) - if parent == nil { - log.Debug("Unknown parent of propagated header", "peer", peer, "number", header.Number, "hash", hash, "parent", header.ParentHash) - return - } - // Validate the header and if something went wrong, drop the peer - if err := f.verifyHeader(header); err != nil && err != consensus.ErrFutureBlock { - log.Debug("Propagated header verification failed", "peer", peer, "number", header.Number, "hash", hash, "err", err) - f.dropPeer(peer) - return - } - // Run the actual import and log any issues - if _, err := f.insertHeaders([]*types.Header{header}); err != nil { - log.Debug("Propagated header import failed", "peer", peer, "number", header.Number, "hash", hash, "err", err) - return - } - // Invoke the testing hook if needed - if f.importedHook != nil { - f.importedHook(header, nil) - } - }() -} - -// importBlocks spawns a new goroutine to run a block insertion into the chain. If the -// block's number is at the same height as the current import phase, it updates -// the phase states accordingly. -func (f *BlockFetcher) importBlocks(peer string, block *types.Block) { - hash := block.Hash() - - // Run the import on a new thread - log.Debug("Importing propagated block", "peer", peer, "number", block.Number(), "hash", hash) - go func() { - defer func() { f.done <- hash }() - - // If the parent's unknown, abort insertion - parent := f.getBlock(block.ParentHash()) - if parent == nil { - log.Debug("Unknown parent of propagated block", "peer", peer, "number", block.Number(), "hash", hash, "parent", block.ParentHash()) - return - } - // Quickly validate the header and propagate the block if it passes - switch err := f.verifyHeader(block.Header()); err { - case nil: - // All ok, quickly propagate to our peers - blockBroadcastOutTimer.UpdateSince(block.ReceivedAt) - go f.broadcastBlock(block, true) - - case consensus.ErrFutureBlock: - // Weird future block, don't fail, but neither propagate - - default: - // Something went very wrong, drop the peer - log.Debug("Propagated block verification failed", "peer", peer, "number", block.Number(), "hash", hash, "err", err) - f.dropPeer(peer) - return - } - // Run the actual import and log any issues - if _, err := f.insertChain(types.Blocks{block}); err != nil { - log.Debug("Propagated block import failed", "peer", peer, "number", block.Number(), "hash", hash, "err", err) - return - } - // If import succeeded, broadcast the block - blockAnnounceOutTimer.UpdateSince(block.ReceivedAt) - go f.broadcastBlock(block, false) - - // Invoke the testing hook if needed - if f.importedHook != nil { - f.importedHook(nil, block) - } - }() -} - -// forgetHash removes all traces of a block announcement from the fetcher's -// internal state. -func (f *BlockFetcher) forgetHash(hash common.Hash) { - // Remove all pending announces and decrement DOS counters - if announceMap, ok := f.announced[hash]; ok { - for _, announce := range announceMap { - f.announces[announce.origin]-- - if f.announces[announce.origin] <= 0 { - delete(f.announces, announce.origin) - } - } - delete(f.announced, hash) - if f.announceChangeHook != nil { - f.announceChangeHook(hash, false) - } - } - // Remove any pending fetches and decrement the DOS counters - if announce := f.fetching[hash]; announce != nil { - f.announces[announce.origin]-- - if f.announces[announce.origin] <= 0 { - delete(f.announces, announce.origin) - } - delete(f.fetching, hash) - } - - // Remove any pending completion requests and decrement the DOS counters - for _, announce := range f.fetched[hash] { - f.announces[announce.origin]-- - if f.announces[announce.origin] <= 0 { - delete(f.announces, announce.origin) - } - } - delete(f.fetched, hash) - - // Remove any pending completions and decrement the DOS counters - if announce := f.completing[hash]; announce != nil { - f.announces[announce.origin]-- - if f.announces[announce.origin] <= 0 { - delete(f.announces, announce.origin) - } - delete(f.completing, hash) - } -} - -// forgetBlock removes all traces of a queued block from the fetcher's internal -// state. -func (f *BlockFetcher) forgetBlock(hash common.Hash) { - if insert := f.queued[hash]; insert != nil { - f.queues[insert.origin]-- - if f.queues[insert.origin] == 0 { - delete(f.queues, insert.origin) - } - delete(f.queued, hash) - } -} diff --git a/eth/fetcher/block_fetcher_test.go b/eth/fetcher/block_fetcher_test.go deleted file mode 100644 index 6927300b1d..0000000000 --- a/eth/fetcher/block_fetcher_test.go +++ /dev/null @@ -1,948 +0,0 @@ -// Copyright 2015 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 . - -package fetcher - -import ( - "errors" - "math/big" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/consensus/ethash" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/eth/protocols/eth" - "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/trie" -) - -var ( - testdb = rawdb.NewMemoryDatabase() - testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") - testAddress = crypto.PubkeyToAddress(testKey.PublicKey) - gspec = &core.Genesis{ - Config: params.TestChainConfig, - Alloc: core.GenesisAlloc{testAddress: {Balance: big.NewInt(1000000000000000)}}, - BaseFee: big.NewInt(params.InitialBaseFee), - } - genesis = gspec.MustCommit(testdb, trie.NewDatabase(testdb, trie.HashDefaults)) - unknownBlock = types.NewBlock(&types.Header{Root: types.EmptyRootHash, GasLimit: params.GenesisGasLimit, BaseFee: big.NewInt(params.InitialBaseFee)}, nil, nil, nil, trie.NewStackTrie(nil)) -) - -// makeChain creates a chain of n blocks starting at and including parent. -// the returned hash chain is ordered head->parent. In addition, every 3rd block -// contains a transaction and every 5th an uncle to allow testing correct block -// reassembly. -func makeChain(n int, seed byte, parent *types.Block) ([]common.Hash, map[common.Hash]*types.Block) { - blocks, _ := core.GenerateChain(gspec.Config, parent, ethash.NewFaker(), testdb, n, func(i int, block *core.BlockGen) { - block.SetCoinbase(common.Address{seed}) - - // If the block number is multiple of 3, send a bonus transaction to the miner - if parent == genesis && i%3 == 0 { - signer := types.MakeSigner(params.TestChainConfig, block.Number(), block.Timestamp()) - tx, err := types.SignTx(types.NewTransaction(block.TxNonce(testAddress), common.Address{seed}, big.NewInt(1000), params.TxGas, block.BaseFee(), nil), signer, testKey) - if err != nil { - panic(err) - } - block.AddTx(tx) - } - // If the block number is a multiple of 5, add a bonus uncle to the block - if i > 0 && i%5 == 0 { - block.AddUncle(&types.Header{ParentHash: block.PrevBlock(i - 2).Hash(), Number: big.NewInt(int64(i - 1))}) - } - }) - hashes := make([]common.Hash, n+1) - hashes[len(hashes)-1] = parent.Hash() - blockm := make(map[common.Hash]*types.Block, n+1) - blockm[parent.Hash()] = parent - for i, b := range blocks { - hashes[len(hashes)-i-2] = b.Hash() - blockm[b.Hash()] = b - } - return hashes, blockm -} - -// fetcherTester is a test simulator for mocking out local block chain. -type fetcherTester struct { - fetcher *BlockFetcher - - hashes []common.Hash // Hash chain belonging to the tester - headers map[common.Hash]*types.Header // Headers belonging to the tester - blocks map[common.Hash]*types.Block // Blocks belonging to the tester - drops map[string]bool // Map of peers dropped by the fetcher - - lock sync.RWMutex -} - -// newTester creates a new fetcher test mocker. -func newTester(light bool) *fetcherTester { - tester := &fetcherTester{ - hashes: []common.Hash{genesis.Hash()}, - headers: map[common.Hash]*types.Header{genesis.Hash(): genesis.Header()}, - blocks: map[common.Hash]*types.Block{genesis.Hash(): genesis}, - drops: make(map[string]bool), - } - tester.fetcher = NewBlockFetcher(light, tester.getHeader, tester.getBlock, tester.verifyHeader, tester.broadcastBlock, tester.chainHeight, tester.insertHeaders, tester.insertChain, tester.dropPeer) - tester.fetcher.Start() - - return tester -} - -// getHeader retrieves a header from the tester's block chain. -func (f *fetcherTester) getHeader(hash common.Hash) *types.Header { - f.lock.RLock() - defer f.lock.RUnlock() - - return f.headers[hash] -} - -// getBlock retrieves a block from the tester's block chain. -func (f *fetcherTester) getBlock(hash common.Hash) *types.Block { - f.lock.RLock() - defer f.lock.RUnlock() - - return f.blocks[hash] -} - -// verifyHeader is a nop placeholder for the block header verification. -func (f *fetcherTester) verifyHeader(header *types.Header) error { - return nil -} - -// broadcastBlock is a nop placeholder for the block broadcasting. -func (f *fetcherTester) broadcastBlock(block *types.Block, propagate bool) { -} - -// chainHeight retrieves the current height (block number) of the chain. -func (f *fetcherTester) chainHeight() uint64 { - f.lock.RLock() - defer f.lock.RUnlock() - - if f.fetcher.light { - return f.headers[f.hashes[len(f.hashes)-1]].Number.Uint64() - } - return f.blocks[f.hashes[len(f.hashes)-1]].NumberU64() -} - -// insertChain injects a new headers into the simulated chain. -func (f *fetcherTester) insertHeaders(headers []*types.Header) (int, error) { - f.lock.Lock() - defer f.lock.Unlock() - - for i, header := range headers { - // Make sure the parent in known - if _, ok := f.headers[header.ParentHash]; !ok { - return i, errors.New("unknown parent") - } - // Discard any new blocks if the same height already exists - if header.Number.Uint64() <= f.headers[f.hashes[len(f.hashes)-1]].Number.Uint64() { - return i, nil - } - // Otherwise build our current chain - f.hashes = append(f.hashes, header.Hash()) - f.headers[header.Hash()] = header - } - return 0, nil -} - -// insertChain injects a new blocks into the simulated chain. -func (f *fetcherTester) insertChain(blocks types.Blocks) (int, error) { - f.lock.Lock() - defer f.lock.Unlock() - - for i, block := range blocks { - // Make sure the parent in known - if _, ok := f.blocks[block.ParentHash()]; !ok { - return i, errors.New("unknown parent") - } - // Discard any new blocks if the same height already exists - if block.NumberU64() <= f.blocks[f.hashes[len(f.hashes)-1]].NumberU64() { - return i, nil - } - // Otherwise build our current chain - f.hashes = append(f.hashes, block.Hash()) - f.blocks[block.Hash()] = block - } - return 0, nil -} - -// dropPeer is an emulator for the peer removal, simply accumulating the various -// peers dropped by the fetcher. -func (f *fetcherTester) dropPeer(peer string) { - f.lock.Lock() - defer f.lock.Unlock() - - f.drops[peer] = true -} - -// makeHeaderFetcher retrieves a block header fetcher associated with a simulated peer. -func (f *fetcherTester) makeHeaderFetcher(peer string, blocks map[common.Hash]*types.Block, drift time.Duration) headerRequesterFn { - closure := make(map[common.Hash]*types.Block) - for hash, block := range blocks { - closure[hash] = block - } - // Create a function that return a header from the closure - return func(hash common.Hash, sink chan *eth.Response) (*eth.Request, error) { - // Gather the blocks to return - headers := make([]*types.Header, 0, 1) - if block, ok := closure[hash]; ok { - headers = append(headers, block.Header()) - } - // Return on a new thread - req := ð.Request{ - Peer: peer, - } - res := ð.Response{ - Req: req, - Res: (*eth.BlockHeadersRequest)(&headers), - Time: drift, - Done: make(chan error, 1), // Ignore the returned status - } - go func() { - sink <- res - }() - return req, nil - } -} - -// makeBodyFetcher retrieves a block body fetcher associated with a simulated peer. -func (f *fetcherTester) makeBodyFetcher(peer string, blocks map[common.Hash]*types.Block, drift time.Duration) bodyRequesterFn { - closure := make(map[common.Hash]*types.Block) - for hash, block := range blocks { - closure[hash] = block - } - // Create a function that returns blocks from the closure - return func(hashes []common.Hash, sink chan *eth.Response) (*eth.Request, error) { - // Gather the block bodies to return - transactions := make([][]*types.Transaction, 0, len(hashes)) - uncles := make([][]*types.Header, 0, len(hashes)) - - for _, hash := range hashes { - if block, ok := closure[hash]; ok { - transactions = append(transactions, block.Transactions()) - uncles = append(uncles, block.Uncles()) - } - } - // Return on a new thread - bodies := make([]*eth.BlockBody, len(transactions)) - for i, txs := range transactions { - bodies[i] = ð.BlockBody{ - Transactions: txs, - Uncles: uncles[i], - } - } - req := ð.Request{ - Peer: peer, - } - res := ð.Response{ - Req: req, - Res: (*eth.BlockBodiesResponse)(&bodies), - Time: drift, - Done: make(chan error, 1), // Ignore the returned status - } - go func() { - sink <- res - }() - return req, nil - } -} - -// verifyFetchingEvent verifies that one single event arrive on a fetching channel. -func verifyFetchingEvent(t *testing.T, fetching chan []common.Hash, arrive bool) { - t.Helper() - - if arrive { - select { - case <-fetching: - case <-time.After(time.Second): - t.Fatalf("fetching timeout") - } - } else { - select { - case <-fetching: - t.Fatalf("fetching invoked") - case <-time.After(10 * time.Millisecond): - } - } -} - -// verifyCompletingEvent verifies that one single event arrive on an completing channel. -func verifyCompletingEvent(t *testing.T, completing chan []common.Hash, arrive bool) { - t.Helper() - - if arrive { - select { - case <-completing: - case <-time.After(time.Second): - t.Fatalf("completing timeout") - } - } else { - select { - case <-completing: - t.Fatalf("completing invoked") - case <-time.After(10 * time.Millisecond): - } - } -} - -// verifyImportEvent verifies that one single event arrive on an import channel. -func verifyImportEvent(t *testing.T, imported chan interface{}, arrive bool) { - t.Helper() - - if arrive { - select { - case <-imported: - case <-time.After(time.Second): - t.Fatalf("import timeout") - } - } else { - select { - case <-imported: - t.Fatalf("import invoked") - case <-time.After(20 * time.Millisecond): - } - } -} - -// verifyImportCount verifies that exactly count number of events arrive on an -// import hook channel. -func verifyImportCount(t *testing.T, imported chan interface{}, count int) { - t.Helper() - - for i := 0; i < count; i++ { - select { - case <-imported: - case <-time.After(time.Second): - t.Fatalf("block %d: import timeout", i+1) - } - } - verifyImportDone(t, imported) -} - -// verifyImportDone verifies that no more events are arriving on an import channel. -func verifyImportDone(t *testing.T, imported chan interface{}) { - t.Helper() - - select { - case <-imported: - t.Fatalf("extra block imported") - case <-time.After(50 * time.Millisecond): - } -} - -// verifyChainHeight verifies the chain height is as expected. -func verifyChainHeight(t *testing.T, fetcher *fetcherTester, height uint64) { - t.Helper() - - if fetcher.chainHeight() != height { - t.Fatalf("chain height mismatch, got %d, want %d", fetcher.chainHeight(), height) - } -} - -// Tests that a fetcher accepts block/header announcements and initiates retrievals -// for them, successfully importing into the local chain. -func TestFullSequentialAnnouncements(t *testing.T) { testSequentialAnnouncements(t, false) } -func TestLightSequentialAnnouncements(t *testing.T) { testSequentialAnnouncements(t, true) } - -func testSequentialAnnouncements(t *testing.T, light bool) { - // Create a chain of blocks to import - targetBlocks := 4 * hashLimit - hashes, blocks := makeChain(targetBlocks, 0, genesis) - - tester := newTester(light) - defer tester.fetcher.Stop() - headerFetcher := tester.makeHeaderFetcher("valid", blocks, -gatherSlack) - bodyFetcher := tester.makeBodyFetcher("valid", blocks, 0) - - // Iteratively announce blocks until all are imported - imported := make(chan interface{}) - tester.fetcher.importedHook = func(header *types.Header, block *types.Block) { - if light { - if header == nil { - t.Fatalf("Fetcher try to import empty header") - } - imported <- header - } else { - if block == nil { - t.Fatalf("Fetcher try to import empty block") - } - imported <- block - } - } - for i := len(hashes) - 2; i >= 0; i-- { - tester.fetcher.Notify("valid", hashes[i], uint64(len(hashes)-i-1), time.Now().Add(-arriveTimeout), headerFetcher, bodyFetcher) - verifyImportEvent(t, imported, true) - } - verifyImportDone(t, imported) - verifyChainHeight(t, tester, uint64(len(hashes)-1)) -} - -// Tests that if blocks are announced by multiple peers (or even the same buggy -// peer), they will only get downloaded at most once. -func TestFullConcurrentAnnouncements(t *testing.T) { testConcurrentAnnouncements(t, false) } -func TestLightConcurrentAnnouncements(t *testing.T) { testConcurrentAnnouncements(t, true) } - -func testConcurrentAnnouncements(t *testing.T, light bool) { - // Create a chain of blocks to import - targetBlocks := 4 * hashLimit - hashes, blocks := makeChain(targetBlocks, 0, genesis) - - // Assemble a tester with a built in counter for the requests - tester := newTester(light) - firstHeaderFetcher := tester.makeHeaderFetcher("first", blocks, -gatherSlack) - firstBodyFetcher := tester.makeBodyFetcher("first", blocks, 0) - secondHeaderFetcher := tester.makeHeaderFetcher("second", blocks, -gatherSlack) - secondBodyFetcher := tester.makeBodyFetcher("second", blocks, 0) - - var counter atomic.Uint32 - firstHeaderWrapper := func(hash common.Hash, sink chan *eth.Response) (*eth.Request, error) { - counter.Add(1) - return firstHeaderFetcher(hash, sink) - } - secondHeaderWrapper := func(hash common.Hash, sink chan *eth.Response) (*eth.Request, error) { - counter.Add(1) - return secondHeaderFetcher(hash, sink) - } - // Iteratively announce blocks until all are imported - imported := make(chan interface{}) - tester.fetcher.importedHook = func(header *types.Header, block *types.Block) { - if light { - if header == nil { - t.Fatalf("Fetcher try to import empty header") - } - imported <- header - } else { - if block == nil { - t.Fatalf("Fetcher try to import empty block") - } - imported <- block - } - } - for i := len(hashes) - 2; i >= 0; i-- { - tester.fetcher.Notify("first", hashes[i], uint64(len(hashes)-i-1), time.Now().Add(-arriveTimeout), firstHeaderWrapper, firstBodyFetcher) - tester.fetcher.Notify("second", hashes[i], uint64(len(hashes)-i-1), time.Now().Add(-arriveTimeout+time.Millisecond), secondHeaderWrapper, secondBodyFetcher) - tester.fetcher.Notify("second", hashes[i], uint64(len(hashes)-i-1), time.Now().Add(-arriveTimeout-time.Millisecond), secondHeaderWrapper, secondBodyFetcher) - verifyImportEvent(t, imported, true) - } - verifyImportDone(t, imported) - - // Make sure no blocks were retrieved twice - if c := int(counter.Load()); c != targetBlocks { - t.Fatalf("retrieval count mismatch: have %v, want %v", c, targetBlocks) - } - verifyChainHeight(t, tester, uint64(len(hashes)-1)) -} - -// Tests that announcements arriving while a previous is being fetched still -// results in a valid import. -func TestFullOverlappingAnnouncements(t *testing.T) { testOverlappingAnnouncements(t, false) } -func TestLightOverlappingAnnouncements(t *testing.T) { testOverlappingAnnouncements(t, true) } - -func testOverlappingAnnouncements(t *testing.T, light bool) { - // Create a chain of blocks to import - targetBlocks := 4 * hashLimit - hashes, blocks := makeChain(targetBlocks, 0, genesis) - - tester := newTester(light) - headerFetcher := tester.makeHeaderFetcher("valid", blocks, -gatherSlack) - bodyFetcher := tester.makeBodyFetcher("valid", blocks, 0) - - // Iteratively announce blocks, but overlap them continuously - overlap := 16 - imported := make(chan interface{}, len(hashes)-1) - for i := 0; i < overlap; i++ { - imported <- nil - } - tester.fetcher.importedHook = func(header *types.Header, block *types.Block) { - if light { - if header == nil { - t.Fatalf("Fetcher try to import empty header") - } - imported <- header - } else { - if block == nil { - t.Fatalf("Fetcher try to import empty block") - } - imported <- block - } - } - - for i := len(hashes) - 2; i >= 0; i-- { - tester.fetcher.Notify("valid", hashes[i], uint64(len(hashes)-i-1), time.Now().Add(-arriveTimeout), headerFetcher, bodyFetcher) - select { - case <-imported: - case <-time.After(time.Second): - t.Fatalf("block %d: import timeout", len(hashes)-i) - } - } - // Wait for all the imports to complete and check count - verifyImportCount(t, imported, overlap) - verifyChainHeight(t, tester, uint64(len(hashes)-1)) -} - -// Tests that announces already being retrieved will not be duplicated. -func TestFullPendingDeduplication(t *testing.T) { testPendingDeduplication(t, false) } -func TestLightPendingDeduplication(t *testing.T) { testPendingDeduplication(t, true) } - -func testPendingDeduplication(t *testing.T, light bool) { - // Create a hash and corresponding block - hashes, blocks := makeChain(1, 0, genesis) - - // Assemble a tester with a built in counter and delayed fetcher - tester := newTester(light) - headerFetcher := tester.makeHeaderFetcher("repeater", blocks, -gatherSlack) - bodyFetcher := tester.makeBodyFetcher("repeater", blocks, 0) - - delay := 50 * time.Millisecond - var counter atomic.Uint32 - headerWrapper := func(hash common.Hash, sink chan *eth.Response) (*eth.Request, error) { - counter.Add(1) - - // Simulate a long running fetch - resink := make(chan *eth.Response) - req, err := headerFetcher(hash, resink) - if err == nil { - go func() { - res := <-resink - time.Sleep(delay) - sink <- res - }() - } - return req, err - } - checkNonExist := func() bool { - return tester.getBlock(hashes[0]) == nil - } - if light { - checkNonExist = func() bool { - return tester.getHeader(hashes[0]) == nil - } - } - // Announce the same block many times until it's fetched (wait for any pending ops) - for checkNonExist() { - tester.fetcher.Notify("repeater", hashes[0], 1, time.Now().Add(-arriveTimeout), headerWrapper, bodyFetcher) - time.Sleep(time.Millisecond) - } - time.Sleep(delay) - - // Check that all blocks were imported and none fetched twice - if c := counter.Load(); c != 1 { - t.Fatalf("retrieval count mismatch: have %v, want %v", c, 1) - } - verifyChainHeight(t, tester, 1) -} - -// Tests that announcements retrieved in a random order are cached and eventually -// imported when all the gaps are filled in. -func TestFullRandomArrivalImport(t *testing.T) { testRandomArrivalImport(t, false) } -func TestLightRandomArrivalImport(t *testing.T) { testRandomArrivalImport(t, true) } - -func testRandomArrivalImport(t *testing.T, light bool) { - // Create a chain of blocks to import, and choose one to delay - targetBlocks := maxQueueDist - hashes, blocks := makeChain(targetBlocks, 0, genesis) - skip := targetBlocks / 2 - - tester := newTester(light) - headerFetcher := tester.makeHeaderFetcher("valid", blocks, -gatherSlack) - bodyFetcher := tester.makeBodyFetcher("valid", blocks, 0) - - // Iteratively announce blocks, skipping one entry - imported := make(chan interface{}, len(hashes)-1) - tester.fetcher.importedHook = func(header *types.Header, block *types.Block) { - if light { - if header == nil { - t.Fatalf("Fetcher try to import empty header") - } - imported <- header - } else { - if block == nil { - t.Fatalf("Fetcher try to import empty block") - } - imported <- block - } - } - for i := len(hashes) - 1; i >= 0; i-- { - if i != skip { - tester.fetcher.Notify("valid", hashes[i], uint64(len(hashes)-i-1), time.Now().Add(-arriveTimeout), headerFetcher, bodyFetcher) - time.Sleep(time.Millisecond) - } - } - // Finally announce the skipped entry and check full import - tester.fetcher.Notify("valid", hashes[skip], uint64(len(hashes)-skip-1), time.Now().Add(-arriveTimeout), headerFetcher, bodyFetcher) - verifyImportCount(t, imported, len(hashes)-1) - verifyChainHeight(t, tester, uint64(len(hashes)-1)) -} - -// Tests that direct block enqueues (due to block propagation vs. hash announce) -// are correctly schedule, filling and import queue gaps. -func TestQueueGapFill(t *testing.T) { - // Create a chain of blocks to import, and choose one to not announce at all - targetBlocks := maxQueueDist - hashes, blocks := makeChain(targetBlocks, 0, genesis) - skip := targetBlocks / 2 - - tester := newTester(false) - headerFetcher := tester.makeHeaderFetcher("valid", blocks, -gatherSlack) - bodyFetcher := tester.makeBodyFetcher("valid", blocks, 0) - - // Iteratively announce blocks, skipping one entry - imported := make(chan interface{}, len(hashes)-1) - tester.fetcher.importedHook = func(header *types.Header, block *types.Block) { imported <- block } - - for i := len(hashes) - 1; i >= 0; i-- { - if i != skip { - tester.fetcher.Notify("valid", hashes[i], uint64(len(hashes)-i-1), time.Now().Add(-arriveTimeout), headerFetcher, bodyFetcher) - time.Sleep(time.Millisecond) - } - } - // Fill the missing block directly as if propagated - tester.fetcher.Enqueue("valid", blocks[hashes[skip]]) - verifyImportCount(t, imported, len(hashes)-1) - verifyChainHeight(t, tester, uint64(len(hashes)-1)) -} - -// Tests that blocks arriving from various sources (multiple propagations, hash -// announces, etc) do not get scheduled for import multiple times. -func TestImportDeduplication(t *testing.T) { - // Create two blocks to import (one for duplication, the other for stalling) - hashes, blocks := makeChain(2, 0, genesis) - - // Create the tester and wrap the importer with a counter - tester := newTester(false) - headerFetcher := tester.makeHeaderFetcher("valid", blocks, -gatherSlack) - bodyFetcher := tester.makeBodyFetcher("valid", blocks, 0) - - var counter atomic.Uint32 - tester.fetcher.insertChain = func(blocks types.Blocks) (int, error) { - counter.Add(uint32(len(blocks))) - return tester.insertChain(blocks) - } - // Instrument the fetching and imported events - fetching := make(chan []common.Hash) - imported := make(chan interface{}, len(hashes)-1) - tester.fetcher.fetchingHook = func(hashes []common.Hash) { fetching <- hashes } - tester.fetcher.importedHook = func(header *types.Header, block *types.Block) { imported <- block } - - // Announce the duplicating block, wait for retrieval, and also propagate directly - tester.fetcher.Notify("valid", hashes[0], 1, time.Now().Add(-arriveTimeout), headerFetcher, bodyFetcher) - <-fetching - - tester.fetcher.Enqueue("valid", blocks[hashes[0]]) - tester.fetcher.Enqueue("valid", blocks[hashes[0]]) - tester.fetcher.Enqueue("valid", blocks[hashes[0]]) - - // Fill the missing block directly as if propagated, and check import uniqueness - tester.fetcher.Enqueue("valid", blocks[hashes[1]]) - verifyImportCount(t, imported, 2) - - if c := counter.Load(); c != 2 { - t.Fatalf("import invocation count mismatch: have %v, want %v", c, 2) - } -} - -// Tests that blocks with numbers much lower or higher than out current head get -// discarded to prevent wasting resources on useless blocks from faulty peers. -func TestDistantPropagationDiscarding(t *testing.T) { - // Create a long chain to import and define the discard boundaries - hashes, blocks := makeChain(3*maxQueueDist, 0, genesis) - head := hashes[len(hashes)/2] - - low, high := len(hashes)/2+maxUncleDist+1, len(hashes)/2-maxQueueDist-1 - - // Create a tester and simulate a head block being the middle of the above chain - tester := newTester(false) - - tester.lock.Lock() - tester.hashes = []common.Hash{head} - tester.blocks = map[common.Hash]*types.Block{head: blocks[head]} - tester.lock.Unlock() - - // Ensure that a block with a lower number than the threshold is discarded - tester.fetcher.Enqueue("lower", blocks[hashes[low]]) - time.Sleep(10 * time.Millisecond) - if !tester.fetcher.queue.Empty() { - t.Fatalf("fetcher queued stale block") - } - // Ensure that a block with a higher number than the threshold is discarded - tester.fetcher.Enqueue("higher", blocks[hashes[high]]) - time.Sleep(10 * time.Millisecond) - if !tester.fetcher.queue.Empty() { - t.Fatalf("fetcher queued future block") - } -} - -// Tests that announcements with numbers much lower or higher than out current -// head get discarded to prevent wasting resources on useless blocks from faulty -// peers. -func TestFullDistantAnnouncementDiscarding(t *testing.T) { testDistantAnnouncementDiscarding(t, false) } -func TestLightDistantAnnouncementDiscarding(t *testing.T) { testDistantAnnouncementDiscarding(t, true) } - -func testDistantAnnouncementDiscarding(t *testing.T, light bool) { - // Create a long chain to import and define the discard boundaries - hashes, blocks := makeChain(3*maxQueueDist, 0, genesis) - head := hashes[len(hashes)/2] - - low, high := len(hashes)/2+maxUncleDist+1, len(hashes)/2-maxQueueDist-1 - - // Create a tester and simulate a head block being the middle of the above chain - tester := newTester(light) - - tester.lock.Lock() - tester.hashes = []common.Hash{head} - tester.headers = map[common.Hash]*types.Header{head: blocks[head].Header()} - tester.blocks = map[common.Hash]*types.Block{head: blocks[head]} - tester.lock.Unlock() - - headerFetcher := tester.makeHeaderFetcher("lower", blocks, -gatherSlack) - bodyFetcher := tester.makeBodyFetcher("lower", blocks, 0) - - fetching := make(chan struct{}, 2) - tester.fetcher.fetchingHook = func(hashes []common.Hash) { fetching <- struct{}{} } - - // Ensure that a block with a lower number than the threshold is discarded - tester.fetcher.Notify("lower", hashes[low], blocks[hashes[low]].NumberU64(), time.Now().Add(-arriveTimeout), headerFetcher, bodyFetcher) - select { - case <-time.After(50 * time.Millisecond): - case <-fetching: - t.Fatalf("fetcher requested stale header") - } - // Ensure that a block with a higher number than the threshold is discarded - tester.fetcher.Notify("higher", hashes[high], blocks[hashes[high]].NumberU64(), time.Now().Add(-arriveTimeout), headerFetcher, bodyFetcher) - select { - case <-time.After(50 * time.Millisecond): - case <-fetching: - t.Fatalf("fetcher requested future header") - } -} - -// Tests that peers announcing blocks with invalid numbers (i.e. not matching -// the headers provided afterwards) get dropped as malicious. -func TestFullInvalidNumberAnnouncement(t *testing.T) { testInvalidNumberAnnouncement(t, false) } -func TestLightInvalidNumberAnnouncement(t *testing.T) { testInvalidNumberAnnouncement(t, true) } - -func testInvalidNumberAnnouncement(t *testing.T, light bool) { - // Create a single block to import and check numbers against - hashes, blocks := makeChain(1, 0, genesis) - - tester := newTester(light) - badHeaderFetcher := tester.makeHeaderFetcher("bad", blocks, -gatherSlack) - badBodyFetcher := tester.makeBodyFetcher("bad", blocks, 0) - - imported := make(chan interface{}) - announced := make(chan interface{}, 2) - tester.fetcher.importedHook = func(header *types.Header, block *types.Block) { - if light { - if header == nil { - t.Fatalf("Fetcher try to import empty header") - } - imported <- header - } else { - if block == nil { - t.Fatalf("Fetcher try to import empty block") - } - imported <- block - } - } - // Announce a block with a bad number, check for immediate drop - tester.fetcher.announceChangeHook = func(hash common.Hash, b bool) { - announced <- nil - } - tester.fetcher.Notify("bad", hashes[0], 2, time.Now().Add(-arriveTimeout), badHeaderFetcher, badBodyFetcher) - verifyAnnounce := func() { - for i := 0; i < 2; i++ { - select { - case <-announced: - continue - case <-time.After(1 * time.Second): - t.Fatal("announce timeout") - return - } - } - } - verifyAnnounce() - verifyImportEvent(t, imported, false) - tester.lock.RLock() - dropped := tester.drops["bad"] - tester.lock.RUnlock() - - if !dropped { - t.Fatalf("peer with invalid numbered announcement not dropped") - } - goodHeaderFetcher := tester.makeHeaderFetcher("good", blocks, -gatherSlack) - goodBodyFetcher := tester.makeBodyFetcher("good", blocks, 0) - // Make sure a good announcement passes without a drop - tester.fetcher.Notify("good", hashes[0], 1, time.Now().Add(-arriveTimeout), goodHeaderFetcher, goodBodyFetcher) - verifyAnnounce() - verifyImportEvent(t, imported, true) - - tester.lock.RLock() - dropped = tester.drops["good"] - tester.lock.RUnlock() - - if dropped { - t.Fatalf("peer with valid numbered announcement dropped") - } - verifyImportDone(t, imported) -} - -// Tests that if a block is empty (i.e. header only), no body request should be -// made, and instead the header should be assembled into a whole block in itself. -func TestEmptyBlockShortCircuit(t *testing.T) { - // Create a chain of blocks to import - hashes, blocks := makeChain(32, 0, genesis) - - tester := newTester(false) - defer tester.fetcher.Stop() - headerFetcher := tester.makeHeaderFetcher("valid", blocks, -gatherSlack) - bodyFetcher := tester.makeBodyFetcher("valid", blocks, 0) - - // Add a monitoring hook for all internal events - fetching := make(chan []common.Hash) - tester.fetcher.fetchingHook = func(hashes []common.Hash) { fetching <- hashes } - - completing := make(chan []common.Hash) - tester.fetcher.completingHook = func(hashes []common.Hash) { completing <- hashes } - - imported := make(chan interface{}) - tester.fetcher.importedHook = func(header *types.Header, block *types.Block) { - if block == nil { - t.Fatalf("Fetcher try to import empty block") - } - imported <- block - } - // Iteratively announce blocks until all are imported - for i := len(hashes) - 2; i >= 0; i-- { - tester.fetcher.Notify("valid", hashes[i], uint64(len(hashes)-i-1), time.Now().Add(-arriveTimeout), headerFetcher, bodyFetcher) - - // All announces should fetch the header - verifyFetchingEvent(t, fetching, true) - - // Only blocks with data contents should request bodies - verifyCompletingEvent(t, completing, len(blocks[hashes[i]].Transactions()) > 0 || len(blocks[hashes[i]].Uncles()) > 0) - - // Irrelevant of the construct, import should succeed - verifyImportEvent(t, imported, true) - } - verifyImportDone(t, imported) -} - -// Tests that a peer is unable to use unbounded memory with sending infinite -// block announcements to a node, but that even in the face of such an attack, -// the fetcher remains operational. -func TestHashMemoryExhaustionAttack(t *testing.T) { - // Create a tester with instrumented import hooks - tester := newTester(false) - - imported, announces := make(chan interface{}), atomic.Int32{} - tester.fetcher.importedHook = func(header *types.Header, block *types.Block) { imported <- block } - tester.fetcher.announceChangeHook = func(hash common.Hash, added bool) { - if added { - announces.Add(1) - } else { - announces.Add(-1) - } - } - // Create a valid chain and an infinite junk chain - targetBlocks := hashLimit + 2*maxQueueDist - hashes, blocks := makeChain(targetBlocks, 0, genesis) - validHeaderFetcher := tester.makeHeaderFetcher("valid", blocks, -gatherSlack) - validBodyFetcher := tester.makeBodyFetcher("valid", blocks, 0) - - attack, _ := makeChain(targetBlocks, 0, unknownBlock) - attackerHeaderFetcher := tester.makeHeaderFetcher("attacker", nil, -gatherSlack) - attackerBodyFetcher := tester.makeBodyFetcher("attacker", nil, 0) - - // Feed the tester a huge hashset from the attacker, and a limited from the valid peer - for i := 0; i < len(attack); i++ { - if i < maxQueueDist { - tester.fetcher.Notify("valid", hashes[len(hashes)-2-i], uint64(i+1), time.Now(), validHeaderFetcher, validBodyFetcher) - } - tester.fetcher.Notify("attacker", attack[i], 1 /* don't distance drop */, time.Now(), attackerHeaderFetcher, attackerBodyFetcher) - } - if count := announces.Load(); count != hashLimit+maxQueueDist { - t.Fatalf("queued announce count mismatch: have %d, want %d", count, hashLimit+maxQueueDist) - } - // Wait for fetches to complete - verifyImportCount(t, imported, maxQueueDist) - - // Feed the remaining valid hashes to ensure DOS protection state remains clean - for i := len(hashes) - maxQueueDist - 2; i >= 0; i-- { - tester.fetcher.Notify("valid", hashes[i], uint64(len(hashes)-i-1), time.Now().Add(-arriveTimeout), validHeaderFetcher, validBodyFetcher) - verifyImportEvent(t, imported, true) - } - verifyImportDone(t, imported) -} - -// Tests that blocks sent to the fetcher (either through propagation or via hash -// announces and retrievals) don't pile up indefinitely, exhausting available -// system memory. -func TestBlockMemoryExhaustionAttack(t *testing.T) { - // Create a tester with instrumented import hooks - tester := newTester(false) - - imported, enqueued := make(chan interface{}), atomic.Int32{} - tester.fetcher.importedHook = func(header *types.Header, block *types.Block) { imported <- block } - tester.fetcher.queueChangeHook = func(hash common.Hash, added bool) { - if added { - enqueued.Add(1) - } else { - enqueued.Add(-1) - } - } - // Create a valid chain and a batch of dangling (but in range) blocks - targetBlocks := hashLimit + 2*maxQueueDist - hashes, blocks := makeChain(targetBlocks, 0, genesis) - attack := make(map[common.Hash]*types.Block) - for i := byte(0); len(attack) < blockLimit+2*maxQueueDist; i++ { - hashes, blocks := makeChain(maxQueueDist-1, i, unknownBlock) - for _, hash := range hashes[:maxQueueDist-2] { - attack[hash] = blocks[hash] - } - } - // Try to feed all the attacker blocks make sure only a limited batch is accepted - for _, block := range attack { - tester.fetcher.Enqueue("attacker", block) - } - time.Sleep(200 * time.Millisecond) - if queued := enqueued.Load(); queued != blockLimit { - t.Fatalf("queued block count mismatch: have %d, want %d", queued, blockLimit) - } - // Queue up a batch of valid blocks, and check that a new peer is allowed to do so - for i := 0; i < maxQueueDist-1; i++ { - tester.fetcher.Enqueue("valid", blocks[hashes[len(hashes)-3-i]]) - } - time.Sleep(100 * time.Millisecond) - if queued := enqueued.Load(); queued != blockLimit+maxQueueDist-1 { - t.Fatalf("queued block count mismatch: have %d, want %d", queued, blockLimit+maxQueueDist-1) - } - // Insert the missing piece (and sanity check the import) - tester.fetcher.Enqueue("valid", blocks[hashes[len(hashes)-2]]) - verifyImportCount(t, imported, maxQueueDist) - - // Insert the remaining blocks in chunks to ensure clean DOS protection - for i := maxQueueDist; i < len(hashes)-1; i++ { - tester.fetcher.Enqueue("valid", blocks[hashes[len(hashes)-2-i]]) - verifyImportEvent(t, imported, true) - } - verifyImportDone(t, imported) -} diff --git a/eth/fetcher/tx_fetcher.go b/eth/fetcher/tx_fetcher.go deleted file mode 100644 index ea7892d8d8..0000000000 --- a/eth/fetcher/tx_fetcher.go +++ /dev/null @@ -1,1003 +0,0 @@ -// Copyright 2019 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 . - -package fetcher - -import ( - "bytes" - "errors" - "fmt" - "math" - mrand "math/rand" - "sort" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/lru" - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/core/txpool" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/metrics" -) - -const ( - // maxTxAnnounces is the maximum number of unique transaction a peer - // can announce in a short time. - maxTxAnnounces = 4096 - - // maxTxRetrievals is the maximum number of transactions that can be fetched - // in one request. The rationale for picking 256 is to have a reasonabe lower - // bound for the transferred data (don't waste RTTs, transfer more meaningful - // batch sizes), but also have an upper bound on the sequentiality to allow - // using our entire peerset for deliveries. - // - // This number also acts as a failsafe against malicious announces which might - // cause us to request more data than we'd expect. - maxTxRetrievals = 256 - - // maxTxRetrievalSize is the max number of bytes that delivered transactions - // should weigh according to the announcements. The 128KB was chosen to limit - // retrieving a maximum of one blob transaction at a time to minimize hogging - // a connection between two peers. - maxTxRetrievalSize = 128 * 1024 - - // maxTxUnderpricedSetSize is the size of the underpriced transaction set that - // is used to track recent transactions that have been dropped so we don't - // re-request them. - maxTxUnderpricedSetSize = 32768 - - // maxTxUnderpricedTimeout is the max time a transaction should be stuck in the underpriced set. - maxTxUnderpricedTimeout = 5 * time.Minute - - // txArriveTimeout is the time allowance before an announced transaction is - // explicitly requested. - txArriveTimeout = 500 * time.Millisecond - - // txGatherSlack is the interval used to collate almost-expired announces - // with network fetches. - txGatherSlack = 100 * time.Millisecond -) - -var ( - // txFetchTimeout is the maximum allotted time to return an explicitly - // requested transaction. - txFetchTimeout = 5 * time.Second -) - -var ( - txAnnounceInMeter = metrics.NewRegisteredMeter("eth/fetcher/transaction/announces/in", nil) - txAnnounceKnownMeter = metrics.NewRegisteredMeter("eth/fetcher/transaction/announces/known", nil) - txAnnounceUnderpricedMeter = metrics.NewRegisteredMeter("eth/fetcher/transaction/announces/underpriced", nil) - txAnnounceDOSMeter = metrics.NewRegisteredMeter("eth/fetcher/transaction/announces/dos", nil) - - txBroadcastInMeter = metrics.NewRegisteredMeter("eth/fetcher/transaction/broadcasts/in", nil) - txBroadcastKnownMeter = metrics.NewRegisteredMeter("eth/fetcher/transaction/broadcasts/known", nil) - txBroadcastUnderpricedMeter = metrics.NewRegisteredMeter("eth/fetcher/transaction/broadcasts/underpriced", nil) - txBroadcastOtherRejectMeter = metrics.NewRegisteredMeter("eth/fetcher/transaction/broadcasts/otherreject", nil) - - txRequestOutMeter = metrics.NewRegisteredMeter("eth/fetcher/transaction/request/out", nil) - txRequestFailMeter = metrics.NewRegisteredMeter("eth/fetcher/transaction/request/fail", nil) - txRequestDoneMeter = metrics.NewRegisteredMeter("eth/fetcher/transaction/request/done", nil) - txRequestTimeoutMeter = metrics.NewRegisteredMeter("eth/fetcher/transaction/request/timeout", nil) - - txReplyInMeter = metrics.NewRegisteredMeter("eth/fetcher/transaction/replies/in", nil) - txReplyKnownMeter = metrics.NewRegisteredMeter("eth/fetcher/transaction/replies/known", nil) - txReplyUnderpricedMeter = metrics.NewRegisteredMeter("eth/fetcher/transaction/replies/underpriced", nil) - txReplyOtherRejectMeter = metrics.NewRegisteredMeter("eth/fetcher/transaction/replies/otherreject", nil) - - txFetcherWaitingPeers = metrics.NewRegisteredGauge("eth/fetcher/transaction/waiting/peers", nil) - txFetcherWaitingHashes = metrics.NewRegisteredGauge("eth/fetcher/transaction/waiting/hashes", nil) - txFetcherQueueingPeers = metrics.NewRegisteredGauge("eth/fetcher/transaction/queueing/peers", nil) - txFetcherQueueingHashes = metrics.NewRegisteredGauge("eth/fetcher/transaction/queueing/hashes", nil) - txFetcherFetchingPeers = metrics.NewRegisteredGauge("eth/fetcher/transaction/fetching/peers", nil) - txFetcherFetchingHashes = metrics.NewRegisteredGauge("eth/fetcher/transaction/fetching/hashes", nil) -) - -// txAnnounce is the notification of the availability of a batch -// of new transactions in the network. -type txAnnounce struct { - origin string // Identifier of the peer originating the notification - hashes []common.Hash // Batch of transaction hashes being announced - metas []*txMetadata // Batch of metadatas associated with the hashes (nil before eth/68) -} - -// txMetadata is a set of extra data transmitted along the announcement for better -// fetch scheduling. -type txMetadata struct { - kind byte // Transaction consensus type - size uint32 // Transaction size in bytes -} - -// txRequest represents an in-flight transaction retrieval request destined to -// a specific peers. -type txRequest struct { - hashes []common.Hash // Transactions having been requested - stolen map[common.Hash]struct{} // Deliveries by someone else (don't re-request) - time mclock.AbsTime // Timestamp of the request -} - -// txDelivery is the notification that a batch of transactions have been added -// to the pool and should be untracked. -type txDelivery struct { - origin string // Identifier of the peer originating the notification - hashes []common.Hash // Batch of transaction hashes having been delivered - metas []txMetadata // Batch of metadatas associated with the delivered hashes - direct bool // Whether this is a direct reply or a broadcast -} - -// txDrop is the notification that a peer has disconnected. -type txDrop struct { - peer string -} - -// TxFetcher is responsible for retrieving new transaction based on announcements. -// -// The fetcher operates in 3 stages: -// - Transactions that are newly discovered are moved into a wait list. -// - After ~500ms passes, transactions from the wait list that have not been -// broadcast to us in whole are moved into a queueing area. -// - When a connected peer doesn't have in-flight retrieval requests, any -// transaction queued up (and announced by the peer) are allocated to the -// peer and moved into a fetching status until it's fulfilled or fails. -// -// The invariants of the fetcher are: -// - Each tracked transaction (hash) must only be present in one of the -// three stages. This ensures that the fetcher operates akin to a finite -// state automata and there's do data leak. -// - Each peer that announced transactions may be scheduled retrievals, but -// only ever one concurrently. This ensures we can immediately know what is -// missing from a reply and reschedule it. -type TxFetcher struct { - notify chan *txAnnounce - cleanup chan *txDelivery - drop chan *txDrop - quit chan struct{} - - underpriced *lru.Cache[common.Hash, time.Time] // Transactions discarded as too cheap (don't re-fetch) - - // Stage 1: Waiting lists for newly discovered transactions that might be - // broadcast without needing explicit request/reply round trips. - waitlist map[common.Hash]map[string]struct{} // Transactions waiting for an potential broadcast - waittime map[common.Hash]mclock.AbsTime // Timestamps when transactions were added to the waitlist - waitslots map[string]map[common.Hash]*txMetadata // Waiting announcements grouped by peer (DoS protection) - - // Stage 2: Queue of transactions that waiting to be allocated to some peer - // to be retrieved directly. - announces map[string]map[common.Hash]*txMetadata // Set of announced transactions, grouped by origin peer - announced map[common.Hash]map[string]struct{} // Set of download locations, grouped by transaction hash - - // Stage 3: Set of transactions currently being retrieved, some which may be - // fulfilled and some rescheduled. Note, this step shares 'announces' from the - // previous stage to avoid having to duplicate (need it for DoS checks). - fetching map[common.Hash]string // Transaction set currently being retrieved - requests map[string]*txRequest // In-flight transaction retrievals - alternates map[common.Hash]map[string]struct{} // In-flight transaction alternate origins if retrieval fails - - // Callbacks - hasTx func(common.Hash) bool // Retrieves a tx from the local txpool - addTxs func([]*types.Transaction) []error // Insert a batch of transactions into local txpool - fetchTxs func(string, []common.Hash) error // Retrieves a set of txs from a remote peer - dropPeer func(string) // Drops a peer in case of announcement violation - - step chan struct{} // Notification channel when the fetcher loop iterates - clock mclock.Clock // Time wrapper to simulate in tests - rand *mrand.Rand // Randomizer to use in tests instead of map range loops (soft-random) -} - -// NewTxFetcher creates a transaction fetcher to retrieve transaction -// based on hash announcements. -func NewTxFetcher(hasTx func(common.Hash) bool, addTxs func([]*types.Transaction) []error, fetchTxs func(string, []common.Hash) error, dropPeer func(string)) *TxFetcher { - return NewTxFetcherForTests(hasTx, addTxs, fetchTxs, dropPeer, mclock.System{}, nil) -} - -// NewTxFetcherForTests is a testing method to mock out the realtime clock with -// a simulated version and the internal randomness with a deterministic one. -func NewTxFetcherForTests( - hasTx func(common.Hash) bool, addTxs func([]*types.Transaction) []error, fetchTxs func(string, []common.Hash) error, dropPeer func(string), - clock mclock.Clock, rand *mrand.Rand) *TxFetcher { - return &TxFetcher{ - notify: make(chan *txAnnounce), - cleanup: make(chan *txDelivery), - drop: make(chan *txDrop), - quit: make(chan struct{}), - waitlist: make(map[common.Hash]map[string]struct{}), - waittime: make(map[common.Hash]mclock.AbsTime), - waitslots: make(map[string]map[common.Hash]*txMetadata), - announces: make(map[string]map[common.Hash]*txMetadata), - announced: make(map[common.Hash]map[string]struct{}), - fetching: make(map[common.Hash]string), - requests: make(map[string]*txRequest), - alternates: make(map[common.Hash]map[string]struct{}), - underpriced: lru.NewCache[common.Hash, time.Time](maxTxUnderpricedSetSize), - hasTx: hasTx, - addTxs: addTxs, - fetchTxs: fetchTxs, - dropPeer: dropPeer, - clock: clock, - rand: rand, - } -} - -// Notify announces the fetcher of the potential availability of a new batch of -// transactions in the network. -func (f *TxFetcher) Notify(peer string, types []byte, sizes []uint32, hashes []common.Hash) error { - // Keep track of all the announced transactions - txAnnounceInMeter.Mark(int64(len(hashes))) - - // Skip any transaction announcements that we already know of, or that we've - // previously marked as cheap and discarded. This check is of course racy, - // because multiple concurrent notifies will still manage to pass it, but it's - // still valuable to check here because it runs concurrent to the internal - // loop, so anything caught here is time saved internally. - var ( - unknownHashes = make([]common.Hash, 0, len(hashes)) - unknownMetas = make([]*txMetadata, 0, len(hashes)) - - duplicate int64 - underpriced int64 - ) - for i, hash := range hashes { - switch { - case f.hasTx(hash): - duplicate++ - case f.isKnownUnderpriced(hash): - underpriced++ - default: - unknownHashes = append(unknownHashes, hash) - if types == nil { - unknownMetas = append(unknownMetas, nil) - } else { - unknownMetas = append(unknownMetas, &txMetadata{kind: types[i], size: sizes[i]}) - } - } - } - txAnnounceKnownMeter.Mark(duplicate) - txAnnounceUnderpricedMeter.Mark(underpriced) - - // If anything's left to announce, push it into the internal loop - if len(unknownHashes) == 0 { - return nil - } - announce := &txAnnounce{origin: peer, hashes: unknownHashes, metas: unknownMetas} - select { - case f.notify <- announce: - return nil - case <-f.quit: - return errTerminated - } -} - -// isKnownUnderpriced reports whether a transaction hash was recently found to be underpriced. -func (f *TxFetcher) isKnownUnderpriced(hash common.Hash) bool { - prevTime, ok := f.underpriced.Peek(hash) - if ok && prevTime.Before(time.Now().Add(-maxTxUnderpricedTimeout)) { - f.underpriced.Remove(hash) - return false - } - return ok -} - -// Enqueue imports a batch of received transaction into the transaction pool -// and the fetcher. This method may be called by both transaction broadcasts and -// direct request replies. The differentiation is important so the fetcher can -// re-schedule missing transactions as soon as possible. -func (f *TxFetcher) Enqueue(peer string, txs []*types.Transaction, direct bool) error { - var ( - inMeter = txReplyInMeter - knownMeter = txReplyKnownMeter - underpricedMeter = txReplyUnderpricedMeter - otherRejectMeter = txReplyOtherRejectMeter - ) - if !direct { - inMeter = txBroadcastInMeter - knownMeter = txBroadcastKnownMeter - underpricedMeter = txBroadcastUnderpricedMeter - otherRejectMeter = txBroadcastOtherRejectMeter - } - // Keep track of all the propagated transactions - inMeter.Mark(int64(len(txs))) - - // Push all the transactions into the pool, tracking underpriced ones to avoid - // re-requesting them and dropping the peer in case of malicious transfers. - var ( - added = make([]common.Hash, 0, len(txs)) - metas = make([]txMetadata, 0, len(txs)) - ) - // proceed in batches - for i := 0; i < len(txs); i += 128 { - end := i + 128 - if end > len(txs) { - end = len(txs) - } - var ( - duplicate int64 - underpriced int64 - otherreject int64 - ) - batch := txs[i:end] - - for j, err := range f.addTxs(batch) { - // Track the transaction hash if the price is too low for us. - // Avoid re-request this transaction when we receive another - // announcement. - if errors.Is(err, txpool.ErrUnderpriced) || errors.Is(err, txpool.ErrReplaceUnderpriced) { - f.underpriced.Add(batch[j].Hash(), batch[j].Time()) - } - // Track a few interesting failure types - switch { - case err == nil: // Noop, but need to handle to not count these - - case errors.Is(err, txpool.ErrAlreadyKnown): - duplicate++ - - case errors.Is(err, txpool.ErrUnderpriced) || errors.Is(err, txpool.ErrReplaceUnderpriced): - underpriced++ - - default: - otherreject++ - } - added = append(added, batch[j].Hash()) - metas = append(metas, txMetadata{ - kind: batch[j].Type(), - size: uint32(batch[j].Size()), - }) - } - knownMeter.Mark(duplicate) - underpricedMeter.Mark(underpriced) - otherRejectMeter.Mark(otherreject) - - // If 'other reject' is >25% of the deliveries in any batch, sleep a bit. - if otherreject > 128/4 { - time.Sleep(200 * time.Millisecond) - log.Debug("Peer delivering stale transactions", "peer", peer, "rejected", otherreject) - } - } - select { - case f.cleanup <- &txDelivery{origin: peer, hashes: added, metas: metas, direct: direct}: - return nil - case <-f.quit: - return errTerminated - } -} - -// Drop should be called when a peer disconnects. It cleans up all the internal -// data structures of the given node. -func (f *TxFetcher) Drop(peer string) error { - select { - case f.drop <- &txDrop{peer: peer}: - return nil - case <-f.quit: - return errTerminated - } -} - -// Start boots up the announcement based synchroniser, accepting and processing -// hash notifications and block fetches until termination requested. -func (f *TxFetcher) Start() { - go f.loop() -} - -// Stop terminates the announcement based synchroniser, canceling all pending -// operations. -func (f *TxFetcher) Stop() { - close(f.quit) -} - -func (f *TxFetcher) loop() { - var ( - waitTimer = new(mclock.Timer) - timeoutTimer = new(mclock.Timer) - - waitTrigger = make(chan struct{}, 1) - timeoutTrigger = make(chan struct{}, 1) - ) - for { - select { - case ann := <-f.notify: - // Drop part of the new announcements if there are too many accumulated. - // Note, we could but do not filter already known transactions here as - // the probability of something arriving between this call and the pre- - // filter outside is essentially zero. - used := len(f.waitslots[ann.origin]) + len(f.announces[ann.origin]) - if used >= maxTxAnnounces { - // This can happen if a set of transactions are requested but not - // all fulfilled, so the remainder are rescheduled without the cap - // check. Should be fine as the limit is in the thousands and the - // request size in the hundreds. - txAnnounceDOSMeter.Mark(int64(len(ann.hashes))) - break - } - want := used + len(ann.hashes) - if want > maxTxAnnounces { - txAnnounceDOSMeter.Mark(int64(want - maxTxAnnounces)) - - ann.hashes = ann.hashes[:want-maxTxAnnounces] - ann.metas = ann.metas[:want-maxTxAnnounces] - } - // All is well, schedule the remainder of the transactions - idleWait := len(f.waittime) == 0 - _, oldPeer := f.announces[ann.origin] - - for i, hash := range ann.hashes { - // If the transaction is already downloading, add it to the list - // of possible alternates (in case the current retrieval fails) and - // also account it for the peer. - if f.alternates[hash] != nil { - f.alternates[hash][ann.origin] = struct{}{} - - // Stage 2 and 3 share the set of origins per tx - if announces := f.announces[ann.origin]; announces != nil { - announces[hash] = ann.metas[i] - } else { - f.announces[ann.origin] = map[common.Hash]*txMetadata{hash: ann.metas[i]} - } - continue - } - // If the transaction is not downloading, but is already queued - // from a different peer, track it for the new peer too. - if f.announced[hash] != nil { - f.announced[hash][ann.origin] = struct{}{} - - // Stage 2 and 3 share the set of origins per tx - if announces := f.announces[ann.origin]; announces != nil { - announces[hash] = ann.metas[i] - } else { - f.announces[ann.origin] = map[common.Hash]*txMetadata{hash: ann.metas[i]} - } - continue - } - // If the transaction is already known to the fetcher, but not - // yet downloading, add the peer as an alternate origin in the - // waiting list. - if f.waitlist[hash] != nil { - // Ignore double announcements from the same peer. This is - // especially important if metadata is also passed along to - // prevent malicious peers flip-flopping good/bad values. - if _, ok := f.waitlist[hash][ann.origin]; ok { - continue - } - f.waitlist[hash][ann.origin] = struct{}{} - - if waitslots := f.waitslots[ann.origin]; waitslots != nil { - waitslots[hash] = ann.metas[i] - } else { - f.waitslots[ann.origin] = map[common.Hash]*txMetadata{hash: ann.metas[i]} - } - continue - } - // Transaction unknown to the fetcher, insert it into the waiting list - f.waitlist[hash] = map[string]struct{}{ann.origin: {}} - f.waittime[hash] = f.clock.Now() - - if waitslots := f.waitslots[ann.origin]; waitslots != nil { - waitslots[hash] = ann.metas[i] - } else { - f.waitslots[ann.origin] = map[common.Hash]*txMetadata{hash: ann.metas[i]} - } - } - // If a new item was added to the waitlist, schedule it into the fetcher - if idleWait && len(f.waittime) > 0 { - f.rescheduleWait(waitTimer, waitTrigger) - } - // If this peer is new and announced something already queued, maybe - // request transactions from them - if !oldPeer && len(f.announces[ann.origin]) > 0 { - f.scheduleFetches(timeoutTimer, timeoutTrigger, map[string]struct{}{ann.origin: {}}) - } - - case <-waitTrigger: - // At least one transaction's waiting time ran out, push all expired - // ones into the retrieval queues - actives := make(map[string]struct{}) - for hash, instance := range f.waittime { - if time.Duration(f.clock.Now()-instance)+txGatherSlack > txArriveTimeout { - // Transaction expired without propagation, schedule for retrieval - if f.announced[hash] != nil { - panic("announce tracker already contains waitlist item") - } - f.announced[hash] = f.waitlist[hash] - for peer := range f.waitlist[hash] { - if announces := f.announces[peer]; announces != nil { - announces[hash] = f.waitslots[peer][hash] - } else { - f.announces[peer] = map[common.Hash]*txMetadata{hash: f.waitslots[peer][hash]} - } - delete(f.waitslots[peer], hash) - if len(f.waitslots[peer]) == 0 { - delete(f.waitslots, peer) - } - actives[peer] = struct{}{} - } - delete(f.waittime, hash) - delete(f.waitlist, hash) - } - } - // If transactions are still waiting for propagation, reschedule the wait timer - if len(f.waittime) > 0 { - f.rescheduleWait(waitTimer, waitTrigger) - } - // If any peers became active and are idle, request transactions from them - if len(actives) > 0 { - f.scheduleFetches(timeoutTimer, timeoutTrigger, actives) - } - - case <-timeoutTrigger: - // Clean up any expired retrievals and avoid re-requesting them from the - // same peer (either overloaded or malicious, useless in both cases). We - // could also penalize (Drop), but there's nothing to gain, and if could - // possibly further increase the load on it. - for peer, req := range f.requests { - if time.Duration(f.clock.Now()-req.time)+txGatherSlack > txFetchTimeout { - txRequestTimeoutMeter.Mark(int64(len(req.hashes))) - - // Reschedule all the not-yet-delivered fetches to alternate peers - for _, hash := range req.hashes { - // Skip rescheduling hashes already delivered by someone else - if req.stolen != nil { - if _, ok := req.stolen[hash]; ok { - continue - } - } - // Move the delivery back from fetching to queued - if _, ok := f.announced[hash]; ok { - panic("announced tracker already contains alternate item") - } - if f.alternates[hash] != nil { // nil if tx was broadcast during fetch - f.announced[hash] = f.alternates[hash] - } - delete(f.announced[hash], peer) - if len(f.announced[hash]) == 0 { - delete(f.announced, hash) - } - delete(f.announces[peer], hash) - delete(f.alternates, hash) - delete(f.fetching, hash) - } - if len(f.announces[peer]) == 0 { - delete(f.announces, peer) - } - // Keep track of the request as dangling, but never expire - f.requests[peer].hashes = nil - } - } - // Schedule a new transaction retrieval - f.scheduleFetches(timeoutTimer, timeoutTrigger, nil) - - // No idea if we scheduled something or not, trigger the timer if needed - // TODO(karalabe): this is kind of lame, can't we dump it into scheduleFetches somehow? - f.rescheduleTimeout(timeoutTimer, timeoutTrigger) - - case delivery := <-f.cleanup: - // Independent if the delivery was direct or broadcast, remove all - // traces of the hash from internal trackers. That said, compare any - // advertised metadata with the real ones and drop bad peers. - for i, hash := range delivery.hashes { - if _, ok := f.waitlist[hash]; ok { - for peer, txset := range f.waitslots { - if meta := txset[hash]; meta != nil { - if delivery.metas[i].kind != meta.kind { - log.Warn("Announced transaction type mismatch", "peer", peer, "tx", hash, "type", delivery.metas[i].kind, "ann", meta.kind) - f.dropPeer(peer) - } else if delivery.metas[i].size != meta.size { - if math.Abs(float64(delivery.metas[i].size)-float64(meta.size)) > 8 { - log.Warn("Announced transaction size mismatch", "peer", peer, "tx", hash, "size", delivery.metas[i].size, "ann", meta.size) - - // Normally we should drop a peer considering this is a protocol violation. - // However, due to the RLP vs consensus format messyness, allow a few bytes - // wiggle-room where we only warn, but don't drop. - // - // TODO(karalabe): Get rid of this relaxation when clients are proven stable. - f.dropPeer(peer) - } - } - } - delete(txset, hash) - if len(txset) == 0 { - delete(f.waitslots, peer) - } - } - delete(f.waitlist, hash) - delete(f.waittime, hash) - } else { - for peer, txset := range f.announces { - if meta := txset[hash]; meta != nil { - if delivery.metas[i].kind != meta.kind { - log.Warn("Announced transaction type mismatch", "peer", peer, "tx", hash, "type", delivery.metas[i].kind, "ann", meta.kind) - f.dropPeer(peer) - } else if delivery.metas[i].size != meta.size { - if math.Abs(float64(delivery.metas[i].size)-float64(meta.size)) > 8 { - log.Warn("Announced transaction size mismatch", "peer", peer, "tx", hash, "size", delivery.metas[i].size, "ann", meta.size) - - // Normally we should drop a peer considering this is a protocol violation. - // However, due to the RLP vs consensus format messyness, allow a few bytes - // wiggle-room where we only warn, but don't drop. - // - // TODO(karalabe): Get rid of this relaxation when clients are proven stable. - f.dropPeer(peer) - } - } - } - delete(txset, hash) - if len(txset) == 0 { - delete(f.announces, peer) - } - } - delete(f.announced, hash) - delete(f.alternates, hash) - - // If a transaction currently being fetched from a different - // origin was delivered (delivery stolen), mark it so the - // actual delivery won't double schedule it. - if origin, ok := f.fetching[hash]; ok && (origin != delivery.origin || !delivery.direct) { - stolen := f.requests[origin].stolen - if stolen == nil { - f.requests[origin].stolen = make(map[common.Hash]struct{}) - stolen = f.requests[origin].stolen - } - stolen[hash] = struct{}{} - } - delete(f.fetching, hash) - } - } - // In case of a direct delivery, also reschedule anything missing - // from the original query - if delivery.direct { - // Mark the requesting successful (independent of individual status) - txRequestDoneMeter.Mark(int64(len(delivery.hashes))) - - // Make sure something was pending, nuke it - req := f.requests[delivery.origin] - if req == nil { - log.Warn("Unexpected transaction delivery", "peer", delivery.origin) - break - } - delete(f.requests, delivery.origin) - - // Anything not delivered should be re-scheduled (with or without - // this peer, depending on the response cutoff) - delivered := make(map[common.Hash]struct{}) - for _, hash := range delivery.hashes { - delivered[hash] = struct{}{} - } - cutoff := len(req.hashes) // If nothing is delivered, assume everything is missing, don't retry!!! - for i, hash := range req.hashes { - if _, ok := delivered[hash]; ok { - cutoff = i - } - } - // Reschedule missing hashes from alternates, not-fulfilled from alt+self - for i, hash := range req.hashes { - // Skip rescheduling hashes already delivered by someone else - if req.stolen != nil { - if _, ok := req.stolen[hash]; ok { - continue - } - } - if _, ok := delivered[hash]; !ok { - if i < cutoff { - delete(f.alternates[hash], delivery.origin) - delete(f.announces[delivery.origin], hash) - if len(f.announces[delivery.origin]) == 0 { - delete(f.announces, delivery.origin) - } - } - if len(f.alternates[hash]) > 0 { - if _, ok := f.announced[hash]; ok { - panic(fmt.Sprintf("announced tracker already contains alternate item: %v", f.announced[hash])) - } - f.announced[hash] = f.alternates[hash] - } - } - delete(f.alternates, hash) - delete(f.fetching, hash) - } - // Something was delivered, try to reschedule requests - f.scheduleFetches(timeoutTimer, timeoutTrigger, nil) // Partial delivery may enable others to deliver too - } - - case drop := <-f.drop: - // A peer was dropped, remove all traces of it - if _, ok := f.waitslots[drop.peer]; ok { - for hash := range f.waitslots[drop.peer] { - delete(f.waitlist[hash], drop.peer) - if len(f.waitlist[hash]) == 0 { - delete(f.waitlist, hash) - delete(f.waittime, hash) - } - } - delete(f.waitslots, drop.peer) - if len(f.waitlist) > 0 { - f.rescheduleWait(waitTimer, waitTrigger) - } - } - // Clean up any active requests - var request *txRequest - if request = f.requests[drop.peer]; request != nil { - for _, hash := range request.hashes { - // Skip rescheduling hashes already delivered by someone else - if request.stolen != nil { - if _, ok := request.stolen[hash]; ok { - continue - } - } - // Undelivered hash, reschedule if there's an alternative origin available - delete(f.alternates[hash], drop.peer) - if len(f.alternates[hash]) == 0 { - delete(f.alternates, hash) - } else { - f.announced[hash] = f.alternates[hash] - delete(f.alternates, hash) - } - delete(f.fetching, hash) - } - delete(f.requests, drop.peer) - } - // Clean up general announcement tracking - if _, ok := f.announces[drop.peer]; ok { - for hash := range f.announces[drop.peer] { - delete(f.announced[hash], drop.peer) - if len(f.announced[hash]) == 0 { - delete(f.announced, hash) - } - } - delete(f.announces, drop.peer) - } - // If a request was cancelled, check if anything needs to be rescheduled - if request != nil { - f.scheduleFetches(timeoutTimer, timeoutTrigger, nil) - f.rescheduleTimeout(timeoutTimer, timeoutTrigger) - } - - case <-f.quit: - return - } - // No idea what happened, but bump some sanity metrics - txFetcherWaitingPeers.Update(int64(len(f.waitslots))) - txFetcherWaitingHashes.Update(int64(len(f.waitlist))) - txFetcherQueueingPeers.Update(int64(len(f.announces) - len(f.requests))) - txFetcherQueueingHashes.Update(int64(len(f.announced))) - txFetcherFetchingPeers.Update(int64(len(f.requests))) - txFetcherFetchingHashes.Update(int64(len(f.fetching))) - - // Loop did something, ping the step notifier if needed (tests) - if f.step != nil { - f.step <- struct{}{} - } - } -} - -// rescheduleWait iterates over all the transactions currently in the waitlist -// and schedules the movement into the fetcher for the earliest. -// -// The method has a granularity of 'gatherSlack', since there's not much point in -// spinning over all the transactions just to maybe find one that should trigger -// a few ms earlier. -func (f *TxFetcher) rescheduleWait(timer *mclock.Timer, trigger chan struct{}) { - if *timer != nil { - (*timer).Stop() - } - now := f.clock.Now() - - earliest := now - for _, instance := range f.waittime { - if earliest > instance { - earliest = instance - if txArriveTimeout-time.Duration(now-earliest) < gatherSlack { - break - } - } - } - *timer = f.clock.AfterFunc(txArriveTimeout-time.Duration(now-earliest), func() { - trigger <- struct{}{} - }) -} - -// rescheduleTimeout iterates over all the transactions currently in flight and -// schedules a cleanup run when the first would trigger. -// -// The method has a granularity of 'gatherSlack', since there's not much point in -// spinning over all the transactions just to maybe find one that should trigger -// a few ms earlier. -// -// This method is a bit "flaky" "by design". In theory the timeout timer only ever -// should be rescheduled if some request is pending. In practice, a timeout will -// cause the timer to be rescheduled every 5 secs (until the peer comes through or -// disconnects). This is a limitation of the fetcher code because we don't trac -// pending requests and timed out requests separately. Without double tracking, if -// we simply didn't reschedule the timer on all-timeout then the timer would never -// be set again since len(request) > 0 => something's running. -func (f *TxFetcher) rescheduleTimeout(timer *mclock.Timer, trigger chan struct{}) { - if *timer != nil { - (*timer).Stop() - } - now := f.clock.Now() - - earliest := now - for _, req := range f.requests { - // If this request already timed out, skip it altogether - if req.hashes == nil { - continue - } - if earliest > req.time { - earliest = req.time - if txFetchTimeout-time.Duration(now-earliest) < gatherSlack { - break - } - } - } - *timer = f.clock.AfterFunc(txFetchTimeout-time.Duration(now-earliest), func() { - trigger <- struct{}{} - }) -} - -// scheduleFetches starts a batch of retrievals for all available idle peers. -func (f *TxFetcher) scheduleFetches(timer *mclock.Timer, timeout chan struct{}, whitelist map[string]struct{}) { - // Gather the set of peers we want to retrieve from (default to all) - actives := whitelist - if actives == nil { - actives = make(map[string]struct{}) - for peer := range f.announces { - actives[peer] = struct{}{} - } - } - if len(actives) == 0 { - return - } - // For each active peer, try to schedule some transaction fetches - idle := len(f.requests) == 0 - - f.forEachPeer(actives, func(peer string) { - if f.requests[peer] != nil { - return // continue in the for-each - } - if len(f.announces[peer]) == 0 { - return // continue in the for-each - } - var ( - hashes = make([]common.Hash, 0, maxTxRetrievals) - bytes uint64 - ) - f.forEachAnnounce(f.announces[peer], func(hash common.Hash, meta *txMetadata) bool { - // If the transaction is already fetching, skip to the next one - if _, ok := f.fetching[hash]; ok { - return true - } - // Mark the hash as fetching and stash away possible alternates - f.fetching[hash] = peer - - if _, ok := f.alternates[hash]; ok { - panic(fmt.Sprintf("alternate tracker already contains fetching item: %v", f.alternates[hash])) - } - f.alternates[hash] = f.announced[hash] - delete(f.announced, hash) - - // Accumulate the hash and stop if the limit was reached - hashes = append(hashes, hash) - if len(hashes) >= maxTxRetrievals { - return false // break in the for-each - } - if meta != nil { // Only set eth/68 and upwards - bytes += uint64(meta.size) - if bytes >= maxTxRetrievalSize { - return false - } - } - return true // scheduled, try to add more - }) - // If any hashes were allocated, request them from the peer - if len(hashes) > 0 { - f.requests[peer] = &txRequest{hashes: hashes, time: f.clock.Now()} - txRequestOutMeter.Mark(int64(len(hashes))) - - go func(peer string, hashes []common.Hash) { - // Try to fetch the transactions, but in case of a request - // failure (e.g. peer disconnected), reschedule the hashes. - if err := f.fetchTxs(peer, hashes); err != nil { - txRequestFailMeter.Mark(int64(len(hashes))) - f.Drop(peer) - } - }(peer, hashes) - } - }) - // If a new request was fired, schedule a timeout timer - if idle && len(f.requests) > 0 { - f.rescheduleTimeout(timer, timeout) - } -} - -// forEachPeer does a range loop over a map of peers in production, but during -// testing it does a deterministic sorted random to allow reproducing issues. -func (f *TxFetcher) forEachPeer(peers map[string]struct{}, do func(peer string)) { - // If we're running production, use whatever Go's map gives us - if f.rand == nil { - for peer := range peers { - do(peer) - } - return - } - // We're running the test suite, make iteration deterministic - list := make([]string, 0, len(peers)) - for peer := range peers { - list = append(list, peer) - } - sort.Strings(list) - rotateStrings(list, f.rand.Intn(len(list))) - for _, peer := range list { - do(peer) - } -} - -// forEachAnnounce does a range loop over a map of announcements in production, -// but during testing it does a deterministic sorted random to allow reproducing -// issues. -func (f *TxFetcher) forEachAnnounce(announces map[common.Hash]*txMetadata, do func(hash common.Hash, meta *txMetadata) bool) { - // If we're running production, use whatever Go's map gives us - if f.rand == nil { - for hash, meta := range announces { - if !do(hash, meta) { - return - } - } - return - } - // We're running the test suite, make iteration deterministic - list := make([]common.Hash, 0, len(announces)) - for hash := range announces { - list = append(list, hash) - } - sortHashes(list) - rotateHashes(list, f.rand.Intn(len(list))) - for _, hash := range list { - if !do(hash, announces[hash]) { - return - } - } -} - -// rotateStrings rotates the contents of a slice by n steps. This method is only -// used in tests to simulate random map iteration but keep it deterministic. -func rotateStrings(slice []string, n int) { - orig := make([]string, len(slice)) - copy(orig, slice) - - for i := 0; i < len(orig); i++ { - slice[i] = orig[(i+n)%len(orig)] - } -} - -// sortHashes sorts a slice of hashes. This method is only used in tests in order -// to simulate random map iteration but keep it deterministic. -func sortHashes(slice []common.Hash) { - for i := 0; i < len(slice); i++ { - for j := i + 1; j < len(slice); j++ { - if bytes.Compare(slice[i][:], slice[j][:]) > 0 { - slice[i], slice[j] = slice[j], slice[i] - } - } - } -} - -// rotateHashes rotates the contents of a slice by n steps. This method is only -// used in tests to simulate random map iteration but keep it deterministic. -func rotateHashes(slice []common.Hash, n int) { - orig := make([]common.Hash, len(slice)) - copy(orig, slice) - - for i := 0; i < len(orig); i++ { - slice[i] = orig[(i+n)%len(orig)] - } -} diff --git a/eth/fetcher/tx_fetcher_test.go b/eth/fetcher/tx_fetcher_test.go deleted file mode 100644 index 4a62e579b6..0000000000 --- a/eth/fetcher/tx_fetcher_test.go +++ /dev/null @@ -1,2030 +0,0 @@ -// Copyright 2019 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 . - -package fetcher - -import ( - "errors" - "math/big" - "math/rand" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/core/txpool" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/params" -) - -var ( - // testTxs is a set of transactions to use during testing that have meaningful hashes. - testTxs = []*types.Transaction{ - types.NewTransaction(5577006791947779410, common.Address{0x0f}, new(big.Int), 0, new(big.Int), nil), - types.NewTransaction(15352856648520921629, common.Address{0xbb}, new(big.Int), 0, new(big.Int), nil), - types.NewTransaction(3916589616287113937, common.Address{0x86}, new(big.Int), 0, new(big.Int), nil), - types.NewTransaction(9828766684487745566, common.Address{0xac}, new(big.Int), 0, new(big.Int), nil), - } - // testTxsHashes is the hashes of the test transactions above - testTxsHashes = []common.Hash{testTxs[0].Hash(), testTxs[1].Hash(), testTxs[2].Hash(), testTxs[3].Hash()} -) - -type announce struct { - hash common.Hash - kind *byte - size *uint32 -} - -func typeptr(t byte) *byte { return &t } -func sizeptr(n uint32) *uint32 { return &n } - -type doTxNotify struct { - peer string - hashes []common.Hash - types []byte - sizes []uint32 -} -type doTxEnqueue struct { - peer string - txs []*types.Transaction - direct bool -} -type doWait struct { - time time.Duration - step bool -} -type doDrop string -type doFunc func() - -type isWaitingWithMeta map[string][]announce -type isWaiting map[string][]common.Hash - -type isScheduledWithMeta struct { - tracking map[string][]announce - fetching map[string][]common.Hash - dangling map[string][]common.Hash -} -type isScheduled struct { - tracking map[string][]common.Hash - fetching map[string][]common.Hash - dangling map[string][]common.Hash -} -type isUnderpriced int - -// txFetcherTest represents a test scenario that can be executed by the test -// runner. -type txFetcherTest struct { - init func() *TxFetcher - steps []interface{} -} - -// Tests that transaction announcements are added to a waitlist, and none -// of them are scheduled for retrieval until the wait expires. -func TestTransactionFetcherWaiting(t *testing.T) { - testTransactionFetcherParallel(t, txFetcherTest{ - init: func() *TxFetcher { - return NewTxFetcher( - func(common.Hash) bool { return false }, - nil, - func(string, []common.Hash) error { return nil }, - nil, - ) - }, - steps: []interface{}{ - // Initial announcement to get something into the waitlist - doTxNotify{peer: "A", hashes: []common.Hash{{0x01}, {0x02}}}, - isWaiting(map[string][]common.Hash{ - "A": {{0x01}, {0x02}}, - }), - // Announce from a new peer to check that no overwrite happens - doTxNotify{peer: "B", hashes: []common.Hash{{0x03}, {0x04}}}, - isWaiting(map[string][]common.Hash{ - "A": {{0x01}, {0x02}}, - "B": {{0x03}, {0x04}}, - }), - // Announce clashing hashes but unique new peer - doTxNotify{peer: "C", hashes: []common.Hash{{0x01}, {0x04}}}, - isWaiting(map[string][]common.Hash{ - "A": {{0x01}, {0x02}}, - "B": {{0x03}, {0x04}}, - "C": {{0x01}, {0x04}}, - }), - // Announce existing and clashing hashes from existing peer - doTxNotify{peer: "A", hashes: []common.Hash{{0x01}, {0x03}, {0x05}}}, - isWaiting(map[string][]common.Hash{ - "A": {{0x01}, {0x02}, {0x03}, {0x05}}, - "B": {{0x03}, {0x04}}, - "C": {{0x01}, {0x04}}, - }), - isScheduled{tracking: nil, fetching: nil}, - - // Wait for the arrival timeout which should move all expired items - // from the wait list to the scheduler - doWait{time: txArriveTimeout, step: true}, - isWaiting(nil), - isScheduled{ - tracking: map[string][]common.Hash{ - "A": {{0x01}, {0x02}, {0x03}, {0x05}}, - "B": {{0x03}, {0x04}}, - "C": {{0x01}, {0x04}}, - }, - fetching: map[string][]common.Hash{ // Depends on deterministic test randomizer - "A": {{0x02}, {0x03}, {0x05}}, - "C": {{0x01}, {0x04}}, - }, - }, - // Queue up a non-fetchable transaction and then trigger it with a new - // peer (weird case to test 1 line in the fetcher) - doTxNotify{peer: "C", hashes: []common.Hash{{0x06}, {0x07}}}, - isWaiting(map[string][]common.Hash{ - "C": {{0x06}, {0x07}}, - }), - doWait{time: txArriveTimeout, step: true}, - isScheduled{ - tracking: map[string][]common.Hash{ - "A": {{0x01}, {0x02}, {0x03}, {0x05}}, - "B": {{0x03}, {0x04}}, - "C": {{0x01}, {0x04}, {0x06}, {0x07}}, - }, - fetching: map[string][]common.Hash{ - "A": {{0x02}, {0x03}, {0x05}}, - "C": {{0x01}, {0x04}}, - }, - }, - doTxNotify{peer: "D", hashes: []common.Hash{{0x06}, {0x07}}}, - isScheduled{ - tracking: map[string][]common.Hash{ - "A": {{0x01}, {0x02}, {0x03}, {0x05}}, - "B": {{0x03}, {0x04}}, - "C": {{0x01}, {0x04}, {0x06}, {0x07}}, - "D": {{0x06}, {0x07}}, - }, - fetching: map[string][]common.Hash{ - "A": {{0x02}, {0x03}, {0x05}}, - "C": {{0x01}, {0x04}}, - "D": {{0x06}, {0x07}}, - }, - }, - }, - }) -} - -// Tests that transaction announcements with associated metadata are added to a -// waitlist, and none of them are scheduled for retrieval until the wait expires. -// -// This test is an extended version of TestTransactionFetcherWaiting. It's mostly -// to cover the metadata checks without bloating up the basic behavioral tests -// with all the useless extra fields. -func TestTransactionFetcherWaitingWithMeta(t *testing.T) { - testTransactionFetcherParallel(t, txFetcherTest{ - init: func() *TxFetcher { - return NewTxFetcher( - func(common.Hash) bool { return false }, - nil, - func(string, []common.Hash) error { return nil }, - nil, - ) - }, - steps: []interface{}{ - // Initial announcement to get something into the waitlist - doTxNotify{peer: "A", hashes: []common.Hash{{0x01}, {0x02}}, types: []byte{types.LegacyTxType, types.LegacyTxType}, sizes: []uint32{111, 222}}, - isWaitingWithMeta(map[string][]announce{ - "A": { - {common.Hash{0x01}, typeptr(types.LegacyTxType), sizeptr(111)}, - {common.Hash{0x02}, typeptr(types.LegacyTxType), sizeptr(222)}, - }, - }), - // Announce from a new peer to check that no overwrite happens - doTxNotify{peer: "B", hashes: []common.Hash{{0x03}, {0x04}}, types: []byte{types.LegacyTxType, types.LegacyTxType}, sizes: []uint32{333, 444}}, - isWaitingWithMeta(map[string][]announce{ - "A": { - {common.Hash{0x01}, typeptr(types.LegacyTxType), sizeptr(111)}, - {common.Hash{0x02}, typeptr(types.LegacyTxType), sizeptr(222)}, - }, - "B": { - {common.Hash{0x03}, typeptr(types.LegacyTxType), sizeptr(333)}, - {common.Hash{0x04}, typeptr(types.LegacyTxType), sizeptr(444)}, - }, - }), - // Announce clashing hashes but unique new peer - doTxNotify{peer: "C", hashes: []common.Hash{{0x01}, {0x04}}, types: []byte{types.LegacyTxType, types.LegacyTxType}, sizes: []uint32{111, 444}}, - isWaitingWithMeta(map[string][]announce{ - "A": { - {common.Hash{0x01}, typeptr(types.LegacyTxType), sizeptr(111)}, - {common.Hash{0x02}, typeptr(types.LegacyTxType), sizeptr(222)}, - }, - "B": { - {common.Hash{0x03}, typeptr(types.LegacyTxType), sizeptr(333)}, - {common.Hash{0x04}, typeptr(types.LegacyTxType), sizeptr(444)}, - }, - "C": { - {common.Hash{0x01}, typeptr(types.LegacyTxType), sizeptr(111)}, - {common.Hash{0x04}, typeptr(types.LegacyTxType), sizeptr(444)}, - }, - }), - // Announce existing and clashing hashes from existing peer. Clashes - // should not overwrite previous announcements. - doTxNotify{peer: "A", hashes: []common.Hash{{0x01}, {0x03}, {0x05}}, types: []byte{types.LegacyTxType, types.LegacyTxType, types.LegacyTxType}, sizes: []uint32{999, 333, 555}}, - isWaitingWithMeta(map[string][]announce{ - "A": { - {common.Hash{0x01}, typeptr(types.LegacyTxType), sizeptr(111)}, - {common.Hash{0x02}, typeptr(types.LegacyTxType), sizeptr(222)}, - {common.Hash{0x03}, typeptr(types.LegacyTxType), sizeptr(333)}, - {common.Hash{0x05}, typeptr(types.LegacyTxType), sizeptr(555)}, - }, - "B": { - {common.Hash{0x03}, typeptr(types.LegacyTxType), sizeptr(333)}, - {common.Hash{0x04}, typeptr(types.LegacyTxType), sizeptr(444)}, - }, - "C": { - {common.Hash{0x01}, typeptr(types.LegacyTxType), sizeptr(111)}, - {common.Hash{0x04}, typeptr(types.LegacyTxType), sizeptr(444)}, - }, - }), - // Announce clashing hashes with conflicting metadata. Somebody will - // be in the wrong, but we don't know yet who. - doTxNotify{peer: "D", hashes: []common.Hash{{0x01}, {0x02}}, types: []byte{types.LegacyTxType, types.BlobTxType}, sizes: []uint32{999, 222}}, - isWaitingWithMeta(map[string][]announce{ - "A": { - {common.Hash{0x01}, typeptr(types.LegacyTxType), sizeptr(111)}, - {common.Hash{0x02}, typeptr(types.LegacyTxType), sizeptr(222)}, - {common.Hash{0x03}, typeptr(types.LegacyTxType), sizeptr(333)}, - {common.Hash{0x05}, typeptr(types.LegacyTxType), sizeptr(555)}, - }, - "B": { - {common.Hash{0x03}, typeptr(types.LegacyTxType), sizeptr(333)}, - {common.Hash{0x04}, typeptr(types.LegacyTxType), sizeptr(444)}, - }, - "C": { - {common.Hash{0x01}, typeptr(types.LegacyTxType), sizeptr(111)}, - {common.Hash{0x04}, typeptr(types.LegacyTxType), sizeptr(444)}, - }, - "D": { - {common.Hash{0x01}, typeptr(types.LegacyTxType), sizeptr(999)}, - {common.Hash{0x02}, typeptr(types.BlobTxType), sizeptr(222)}, - }, - }), - isScheduled{tracking: nil, fetching: nil}, - - // Wait for the arrival timeout which should move all expired items - // from the wait list to the scheduler - doWait{time: txArriveTimeout, step: true}, - isWaiting(nil), - isScheduledWithMeta{ - tracking: map[string][]announce{ - "A": { - {common.Hash{0x01}, typeptr(types.LegacyTxType), sizeptr(111)}, - {common.Hash{0x02}, typeptr(types.LegacyTxType), sizeptr(222)}, - {common.Hash{0x03}, typeptr(types.LegacyTxType), sizeptr(333)}, - {common.Hash{0x05}, typeptr(types.LegacyTxType), sizeptr(555)}, - }, - "B": { - {common.Hash{0x03}, typeptr(types.LegacyTxType), sizeptr(333)}, - {common.Hash{0x04}, typeptr(types.LegacyTxType), sizeptr(444)}, - }, - "C": { - {common.Hash{0x01}, typeptr(types.LegacyTxType), sizeptr(111)}, - {common.Hash{0x04}, typeptr(types.LegacyTxType), sizeptr(444)}, - }, - "D": { - {common.Hash{0x01}, typeptr(types.LegacyTxType), sizeptr(999)}, - {common.Hash{0x02}, typeptr(types.BlobTxType), sizeptr(222)}, - }, - }, - fetching: map[string][]common.Hash{ // Depends on deterministic test randomizer - "A": {{0x03}, {0x05}}, - "C": {{0x01}, {0x04}}, - "D": {{0x02}}, - }, - }, - // Queue up a non-fetchable transaction and then trigger it with a new - // peer (weird case to test 1 line in the fetcher) - doTxNotify{peer: "C", hashes: []common.Hash{{0x06}, {0x07}}, types: []byte{types.LegacyTxType, types.LegacyTxType}, sizes: []uint32{666, 777}}, - isWaitingWithMeta(map[string][]announce{ - "C": { - {common.Hash{0x06}, typeptr(types.LegacyTxType), sizeptr(666)}, - {common.Hash{0x07}, typeptr(types.LegacyTxType), sizeptr(777)}, - }, - }), - doWait{time: txArriveTimeout, step: true}, - isScheduledWithMeta{ - tracking: map[string][]announce{ - "A": { - {common.Hash{0x01}, typeptr(types.LegacyTxType), sizeptr(111)}, - {common.Hash{0x02}, typeptr(types.LegacyTxType), sizeptr(222)}, - {common.Hash{0x03}, typeptr(types.LegacyTxType), sizeptr(333)}, - {common.Hash{0x05}, typeptr(types.LegacyTxType), sizeptr(555)}, - }, - "B": { - {common.Hash{0x03}, typeptr(types.LegacyTxType), sizeptr(333)}, - {common.Hash{0x04}, typeptr(types.LegacyTxType), sizeptr(444)}, - }, - "C": { - {common.Hash{0x01}, typeptr(types.LegacyTxType), sizeptr(111)}, - {common.Hash{0x04}, typeptr(types.LegacyTxType), sizeptr(444)}, - {common.Hash{0x06}, typeptr(types.LegacyTxType), sizeptr(666)}, - {common.Hash{0x07}, typeptr(types.LegacyTxType), sizeptr(777)}, - }, - "D": { - {common.Hash{0x01}, typeptr(types.LegacyTxType), sizeptr(999)}, - {common.Hash{0x02}, typeptr(types.BlobTxType), sizeptr(222)}, - }, - }, - fetching: map[string][]common.Hash{ - "A": {{0x03}, {0x05}}, - "C": {{0x01}, {0x04}}, - "D": {{0x02}}, - }, - }, - doTxNotify{peer: "E", hashes: []common.Hash{{0x06}, {0x07}}, types: []byte{types.LegacyTxType, types.LegacyTxType}, sizes: []uint32{666, 777}}, - isScheduledWithMeta{ - tracking: map[string][]announce{ - "A": { - {common.Hash{0x01}, typeptr(types.LegacyTxType), sizeptr(111)}, - {common.Hash{0x02}, typeptr(types.LegacyTxType), sizeptr(222)}, - {common.Hash{0x03}, typeptr(types.LegacyTxType), sizeptr(333)}, - {common.Hash{0x05}, typeptr(types.LegacyTxType), sizeptr(555)}, - }, - "B": { - {common.Hash{0x03}, typeptr(types.LegacyTxType), sizeptr(333)}, - {common.Hash{0x04}, typeptr(types.LegacyTxType), sizeptr(444)}, - }, - "C": { - {common.Hash{0x01}, typeptr(types.LegacyTxType), sizeptr(111)}, - {common.Hash{0x04}, typeptr(types.LegacyTxType), sizeptr(444)}, - {common.Hash{0x06}, typeptr(types.LegacyTxType), sizeptr(666)}, - {common.Hash{0x07}, typeptr(types.LegacyTxType), sizeptr(777)}, - }, - "D": { - {common.Hash{0x01}, typeptr(types.LegacyTxType), sizeptr(999)}, - {common.Hash{0x02}, typeptr(types.BlobTxType), sizeptr(222)}, - }, - "E": { - {common.Hash{0x06}, typeptr(types.LegacyTxType), sizeptr(666)}, - {common.Hash{0x07}, typeptr(types.LegacyTxType), sizeptr(777)}, - }, - }, - fetching: map[string][]common.Hash{ - "A": {{0x03}, {0x05}}, - "C": {{0x01}, {0x04}}, - "D": {{0x02}}, - "E": {{0x06}, {0x07}}, - }, - }, - }, - }) -} - -// Tests that transaction announcements skip the waiting list if they are -// already scheduled. -func TestTransactionFetcherSkipWaiting(t *testing.T) { - testTransactionFetcherParallel(t, txFetcherTest{ - init: func() *TxFetcher { - return NewTxFetcher( - func(common.Hash) bool { return false }, - nil, - func(string, []common.Hash) error { return nil }, - nil, - ) - }, - steps: []interface{}{ - // Push an initial announcement through to the scheduled stage - doTxNotify{peer: "A", hashes: []common.Hash{{0x01}, {0x02}}}, - isWaiting(map[string][]common.Hash{ - "A": {{0x01}, {0x02}}, - }), - isScheduled{tracking: nil, fetching: nil}, - - doWait{time: txArriveTimeout, step: true}, - isWaiting(nil), - isScheduled{ - tracking: map[string][]common.Hash{ - "A": {{0x01}, {0x02}}, - }, - fetching: map[string][]common.Hash{ - "A": {{0x01}, {0x02}}, - }, - }, - // Announce overlaps from the same peer, ensure the new ones end up - // in stage one, and clashing ones don't get double tracked - doTxNotify{peer: "A", hashes: []common.Hash{{0x02}, {0x03}}}, - isWaiting(map[string][]common.Hash{ - "A": {{0x03}}, - }), - isScheduled{ - tracking: map[string][]common.Hash{ - "A": {{0x01}, {0x02}}, - }, - fetching: map[string][]common.Hash{ - "A": {{0x01}, {0x02}}, - }, - }, - // Announce overlaps from a new peer, ensure new transactions end up - // in stage one and clashing ones get tracked for the new peer - doTxNotify{peer: "B", hashes: []common.Hash{{0x02}, {0x03}, {0x04}}}, - isWaiting(map[string][]common.Hash{ - "A": {{0x03}}, - "B": {{0x03}, {0x04}}, - }), - isScheduled{ - tracking: map[string][]common.Hash{ - "A": {{0x01}, {0x02}}, - "B": {{0x02}}, - }, - fetching: map[string][]common.Hash{ - "A": {{0x01}, {0x02}}, - }, - }, - }, - }) -} - -// Tests that only a single transaction request gets scheduled to a peer -// and subsequent announces block or get allotted to someone else. -func TestTransactionFetcherSingletonRequesting(t *testing.T) { - testTransactionFetcherParallel(t, txFetcherTest{ - init: func() *TxFetcher { - return NewTxFetcher( - func(common.Hash) bool { return false }, - nil, - func(string, []common.Hash) error { return nil }, - nil, - ) - }, - steps: []interface{}{ - // Push an initial announcement through to the scheduled stage - doTxNotify{peer: "A", hashes: []common.Hash{{0x01}, {0x02}}}, - isWaiting(map[string][]common.Hash{ - "A": {{0x01}, {0x02}}, - }), - isScheduled{tracking: nil, fetching: nil}, - - doWait{time: txArriveTimeout, step: true}, - isWaiting(nil), - isScheduled{ - tracking: map[string][]common.Hash{ - "A": {{0x01}, {0x02}}, - }, - fetching: map[string][]common.Hash{ - "A": {{0x01}, {0x02}}, - }, - }, - // Announce a new set of transactions from the same peer and ensure - // they do not start fetching since the peer is already busy - doTxNotify{peer: "A", hashes: []common.Hash{{0x03}, {0x04}}}, - isWaiting(map[string][]common.Hash{ - "A": {{0x03}, {0x04}}, - }), - isScheduled{ - tracking: map[string][]common.Hash{ - "A": {{0x01}, {0x02}}, - }, - fetching: map[string][]common.Hash{ - "A": {{0x01}, {0x02}}, - }, - }, - doWait{time: txArriveTimeout, step: true}, - isWaiting(nil), - isScheduled{ - tracking: map[string][]common.Hash{ - "A": {{0x01}, {0x02}, {0x03}, {0x04}}, - }, - fetching: map[string][]common.Hash{ - "A": {{0x01}, {0x02}}, - }, - }, - // Announce a duplicate set of transactions from a new peer and ensure - // uniquely new ones start downloading, even if clashing. - doTxNotify{peer: "B", hashes: []common.Hash{{0x02}, {0x03}, {0x05}, {0x06}}}, - isWaiting(map[string][]common.Hash{ - "B": {{0x05}, {0x06}}, - }), - isScheduled{ - tracking: map[string][]common.Hash{ - "A": {{0x01}, {0x02}, {0x03}, {0x04}}, - "B": {{0x02}, {0x03}}, - }, - fetching: map[string][]common.Hash{ - "A": {{0x01}, {0x02}}, - "B": {{0x03}}, - }, - }, - }, - }) -} - -// Tests that if a transaction retrieval fails, all the transactions get -// instantly schedule back to someone else or the announcements dropped -// if no alternate source is available. -func TestTransactionFetcherFailedRescheduling(t *testing.T) { - // Create a channel to control when tx requests can fail - proceed := make(chan struct{}) - testTransactionFetcherParallel(t, txFetcherTest{ - init: func() *TxFetcher { - return NewTxFetcher( - func(common.Hash) bool { return false }, - nil, - func(origin string, hashes []common.Hash) error { - <-proceed - return errors.New("peer disconnected") - }, - nil, - ) - }, - steps: []interface{}{ - // Push an initial announcement through to the scheduled stage - doTxNotify{peer: "A", hashes: []common.Hash{{0x01}, {0x02}}}, - isWaiting(map[string][]common.Hash{ - "A": {{0x01}, {0x02}}, - }), - isScheduled{tracking: nil, fetching: nil}, - - doWait{time: txArriveTimeout, step: true}, - isWaiting(nil), - isScheduled{ - tracking: map[string][]common.Hash{ - "A": {{0x01}, {0x02}}, - }, - fetching: map[string][]common.Hash{ - "A": {{0x01}, {0x02}}, - }, - }, - // While the original peer is stuck in the request, push in an second - // data source. - doTxNotify{peer: "B", hashes: []common.Hash{{0x02}}}, - isWaiting(nil), - isScheduled{ - tracking: map[string][]common.Hash{ - "A": {{0x01}, {0x02}}, - "B": {{0x02}}, - }, - fetching: map[string][]common.Hash{ - "A": {{0x01}, {0x02}}, - }, - }, - // Wait until the original request fails and check that transactions - // are either rescheduled or dropped - doFunc(func() { - proceed <- struct{}{} // Allow peer A to return the failure - }), - doWait{time: 0, step: true}, - isWaiting(nil), - isScheduled{ - tracking: map[string][]common.Hash{ - "B": {{0x02}}, - }, - fetching: map[string][]common.Hash{ - "B": {{0x02}}, - }, - }, - doFunc(func() { - proceed <- struct{}{} // Allow peer B to return the failure - }), - doWait{time: 0, step: true}, - isWaiting(nil), - isScheduled{nil, nil, nil}, - }, - }) -} - -// Tests that if a transaction retrieval succeeds, all alternate origins -// are cleaned up. -func TestTransactionFetcherCleanup(t *testing.T) { - testTransactionFetcherParallel(t, txFetcherTest{ - init: func() *TxFetcher { - return NewTxFetcher( - func(common.Hash) bool { return false }, - func(txs []*types.Transaction) []error { - return make([]error, len(txs)) - }, - func(string, []common.Hash) error { return nil }, - nil, - ) - }, - steps: []interface{}{ - // Push an initial announcement through to the scheduled stage - doTxNotify{peer: "A", hashes: []common.Hash{testTxsHashes[0]}}, - isWaiting(map[string][]common.Hash{ - "A": {testTxsHashes[0]}, - }), - isScheduled{tracking: nil, fetching: nil}, - - doWait{time: txArriveTimeout, step: true}, - isWaiting(nil), - isScheduled{ - tracking: map[string][]common.Hash{ - "A": {testTxsHashes[0]}, - }, - fetching: map[string][]common.Hash{ - "A": {testTxsHashes[0]}, - }, - }, - // Request should be delivered - doTxEnqueue{peer: "A", txs: []*types.Transaction{testTxs[0]}, direct: true}, - isScheduled{nil, nil, nil}, - }, - }) -} - -// Tests that if a transaction retrieval succeeds, but the response is empty (no -// transactions available, then all are nuked instead of being rescheduled (yes, -// this was a bug)). -func TestTransactionFetcherCleanupEmpty(t *testing.T) { - testTransactionFetcherParallel(t, txFetcherTest{ - init: func() *TxFetcher { - return NewTxFetcher( - func(common.Hash) bool { return false }, - func(txs []*types.Transaction) []error { - return make([]error, len(txs)) - }, - func(string, []common.Hash) error { return nil }, - nil, - ) - }, - steps: []interface{}{ - // Push an initial announcement through to the scheduled stage - doTxNotify{peer: "A", hashes: []common.Hash{testTxsHashes[0]}}, - isWaiting(map[string][]common.Hash{ - "A": {testTxsHashes[0]}, - }), - isScheduled{tracking: nil, fetching: nil}, - - doWait{time: txArriveTimeout, step: true}, - isWaiting(nil), - isScheduled{ - tracking: map[string][]common.Hash{ - "A": {testTxsHashes[0]}, - }, - fetching: map[string][]common.Hash{ - "A": {testTxsHashes[0]}, - }, - }, - // Deliver an empty response and ensure the transaction is cleared, not rescheduled - doTxEnqueue{peer: "A", txs: []*types.Transaction{}, direct: true}, - isScheduled{nil, nil, nil}, - }, - }) -} - -// Tests that non-returned transactions are either re-scheduled from a -// different peer, or self if they are after the cutoff point. -func TestTransactionFetcherMissingRescheduling(t *testing.T) { - testTransactionFetcherParallel(t, txFetcherTest{ - init: func() *TxFetcher { - return NewTxFetcher( - func(common.Hash) bool { return false }, - func(txs []*types.Transaction) []error { - return make([]error, len(txs)) - }, - func(string, []common.Hash) error { return nil }, - nil, - ) - }, - steps: []interface{}{ - // Push an initial announcement through to the scheduled stage - doTxNotify{peer: "A", hashes: []common.Hash{testTxsHashes[0], testTxsHashes[1], testTxsHashes[2]}}, - isWaiting(map[string][]common.Hash{ - "A": {testTxsHashes[0], testTxsHashes[1], testTxsHashes[2]}, - }), - isScheduled{tracking: nil, fetching: nil}, - - doWait{time: txArriveTimeout, step: true}, - isWaiting(nil), - isScheduled{ - tracking: map[string][]common.Hash{ - "A": {testTxsHashes[0], testTxsHashes[1], testTxsHashes[2]}, - }, - fetching: map[string][]common.Hash{ - "A": {testTxsHashes[0], testTxsHashes[1], testTxsHashes[2]}, - }, - }, - // Deliver the middle transaction requested, the one before which - // should be dropped and the one after re-requested. - doTxEnqueue{peer: "A", txs: []*types.Transaction{testTxs[0]}, direct: true}, // This depends on the deterministic random - isScheduled{ - tracking: map[string][]common.Hash{ - "A": {testTxsHashes[2]}, - }, - fetching: map[string][]common.Hash{ - "A": {testTxsHashes[2]}, - }, - }, - }, - }) -} - -// Tests that out of two transactions, if one is missing and the last is -// delivered, the peer gets properly cleaned out from the internal state. -func TestTransactionFetcherMissingCleanup(t *testing.T) { - testTransactionFetcherParallel(t, txFetcherTest{ - init: func() *TxFetcher { - return NewTxFetcher( - func(common.Hash) bool { return false }, - func(txs []*types.Transaction) []error { - return make([]error, len(txs)) - }, - func(string, []common.Hash) error { return nil }, - nil, - ) - }, - steps: []interface{}{ - // Push an initial announcement through to the scheduled stage - doTxNotify{peer: "A", hashes: []common.Hash{testTxsHashes[0], testTxsHashes[1]}}, - isWaiting(map[string][]common.Hash{ - "A": {testTxsHashes[0], testTxsHashes[1]}, - }), - isScheduled{tracking: nil, fetching: nil}, - - doWait{time: txArriveTimeout, step: true}, - isWaiting(nil), - isScheduled{ - tracking: map[string][]common.Hash{ - "A": {testTxsHashes[0], testTxsHashes[1]}, - }, - fetching: map[string][]common.Hash{ - "A": {testTxsHashes[0], testTxsHashes[1]}, - }, - }, - // Deliver the middle transaction requested, the one before which - // should be dropped and the one after re-requested. - doTxEnqueue{peer: "A", txs: []*types.Transaction{testTxs[1]}, direct: true}, // This depends on the deterministic random - isScheduled{nil, nil, nil}, - }, - }) -} - -// Tests that transaction broadcasts properly clean up announcements. -func TestTransactionFetcherBroadcasts(t *testing.T) { - testTransactionFetcherParallel(t, txFetcherTest{ - init: func() *TxFetcher { - return NewTxFetcher( - func(common.Hash) bool { return false }, - func(txs []*types.Transaction) []error { - return make([]error, len(txs)) - }, - func(string, []common.Hash) error { return nil }, - nil, - ) - }, - steps: []interface{}{ - // Set up three transactions to be in different stats, waiting, queued and fetching - doTxNotify{peer: "A", hashes: []common.Hash{testTxsHashes[0]}}, - doWait{time: txArriveTimeout, step: true}, - doTxNotify{peer: "A", hashes: []common.Hash{testTxsHashes[1]}}, - doWait{time: txArriveTimeout, step: true}, - doTxNotify{peer: "A", hashes: []common.Hash{testTxsHashes[2]}}, - - isWaiting(map[string][]common.Hash{ - "A": {testTxsHashes[2]}, - }), - isScheduled{ - tracking: map[string][]common.Hash{ - "A": {testTxsHashes[0], testTxsHashes[1]}, - }, - fetching: map[string][]common.Hash{ - "A": {testTxsHashes[0]}, - }, - }, - // Broadcast all the transactions and ensure everything gets cleaned - // up, but the dangling request is left alone to avoid doing multiple - // concurrent requests. - doTxEnqueue{peer: "A", txs: []*types.Transaction{testTxs[0], testTxs[1], testTxs[2]}, direct: false}, - isWaiting(nil), - isScheduled{ - tracking: nil, - fetching: nil, - dangling: map[string][]common.Hash{ - "A": {testTxsHashes[0]}, - }, - }, - // Deliver the requested hashes - doTxEnqueue{peer: "A", txs: []*types.Transaction{testTxs[0], testTxs[1], testTxs[2]}, direct: true}, - isScheduled{nil, nil, nil}, - }, - }) -} - -// Tests that the waiting list timers properly reset and reschedule. -func TestTransactionFetcherWaitTimerResets(t *testing.T) { - testTransactionFetcherParallel(t, txFetcherTest{ - init: func() *TxFetcher { - return NewTxFetcher( - func(common.Hash) bool { return false }, - nil, - func(string, []common.Hash) error { return nil }, - nil, - ) - }, - steps: []interface{}{ - doTxNotify{peer: "A", hashes: []common.Hash{{0x01}}}, - isWaiting(map[string][]common.Hash{ - "A": {{0x01}}, - }), - isScheduled{nil, nil, nil}, - doWait{time: txArriveTimeout / 2, step: false}, - isWaiting(map[string][]common.Hash{ - "A": {{0x01}}, - }), - isScheduled{nil, nil, nil}, - - doTxNotify{peer: "A", hashes: []common.Hash{{0x02}}}, - isWaiting(map[string][]common.Hash{ - "A": {{0x01}, {0x02}}, - }), - isScheduled{nil, nil, nil}, - doWait{time: txArriveTimeout / 2, step: true}, - isWaiting(map[string][]common.Hash{ - "A": {{0x02}}, - }), - isScheduled{ - tracking: map[string][]common.Hash{ - "A": {{0x01}}, - }, - fetching: map[string][]common.Hash{ - "A": {{0x01}}, - }, - }, - - doWait{time: txArriveTimeout / 2, step: true}, - isWaiting(nil), - isScheduled{ - tracking: map[string][]common.Hash{ - "A": {{0x01}, {0x02}}, - }, - fetching: map[string][]common.Hash{ - "A": {{0x01}}, - }, - }, - }, - }) -} - -// Tests that if a transaction request is not replied to, it will time -// out and be re-scheduled for someone else. -func TestTransactionFetcherTimeoutRescheduling(t *testing.T) { - testTransactionFetcherParallel(t, txFetcherTest{ - init: func() *TxFetcher { - return NewTxFetcher( - func(common.Hash) bool { return false }, - func(txs []*types.Transaction) []error { - return make([]error, len(txs)) - }, - func(string, []common.Hash) error { return nil }, - nil, - ) - }, - steps: []interface{}{ - // Push an initial announcement through to the scheduled stage - doTxNotify{peer: "A", hashes: []common.Hash{testTxsHashes[0]}}, - isWaiting(map[string][]common.Hash{ - "A": {testTxsHashes[0]}, - }), - isScheduled{tracking: nil, fetching: nil}, - - doWait{time: txArriveTimeout, step: true}, - isWaiting(nil), - isScheduled{ - tracking: map[string][]common.Hash{ - "A": {testTxsHashes[0]}, - }, - fetching: map[string][]common.Hash{ - "A": {testTxsHashes[0]}, - }, - }, - // Wait until the delivery times out, everything should be cleaned up - doWait{time: txFetchTimeout, step: true}, - isWaiting(nil), - isScheduled{ - tracking: nil, - fetching: nil, - dangling: map[string][]common.Hash{ - "A": {}, - }, - }, - // Ensure that followup announcements don't get scheduled - doTxNotify{peer: "A", hashes: []common.Hash{testTxsHashes[1]}}, - doWait{time: txArriveTimeout, step: true}, - isScheduled{ - tracking: map[string][]common.Hash{ - "A": {testTxsHashes[1]}, - }, - fetching: nil, - dangling: map[string][]common.Hash{ - "A": {}, - }, - }, - // If the dangling request arrives a bit later, do not choke - doTxEnqueue{peer: "A", txs: []*types.Transaction{testTxs[0]}, direct: true}, - isWaiting(nil), - isScheduled{ - tracking: map[string][]common.Hash{ - "A": {testTxsHashes[1]}, - }, - fetching: map[string][]common.Hash{ - "A": {testTxsHashes[1]}, - }, - }, - }, - }) -} - -// Tests that the fetching timeout timers properly reset and reschedule. -func TestTransactionFetcherTimeoutTimerResets(t *testing.T) { - testTransactionFetcherParallel(t, txFetcherTest{ - init: func() *TxFetcher { - return NewTxFetcher( - func(common.Hash) bool { return false }, - nil, - func(string, []common.Hash) error { return nil }, - nil, - ) - }, - steps: []interface{}{ - doTxNotify{peer: "A", hashes: []common.Hash{{0x01}}}, - doWait{time: txArriveTimeout, step: true}, - doTxNotify{peer: "B", hashes: []common.Hash{{0x02}}}, - doWait{time: txArriveTimeout, step: true}, - - isWaiting(nil), - isScheduled{ - tracking: map[string][]common.Hash{ - "A": {{0x01}}, - "B": {{0x02}}, - }, - fetching: map[string][]common.Hash{ - "A": {{0x01}}, - "B": {{0x02}}, - }, - }, - doWait{time: txFetchTimeout - txArriveTimeout, step: true}, - isScheduled{ - tracking: map[string][]common.Hash{ - "B": {{0x02}}, - }, - fetching: map[string][]common.Hash{ - "B": {{0x02}}, - }, - dangling: map[string][]common.Hash{ - "A": {}, - }, - }, - doWait{time: txArriveTimeout, step: true}, - isScheduled{ - tracking: nil, - fetching: nil, - dangling: map[string][]common.Hash{ - "A": {}, - "B": {}, - }, - }, - }, - }) -} - -// Tests that if thousands of transactions are announced, only a small -// number of them will be requested at a time. -func TestTransactionFetcherRateLimiting(t *testing.T) { - // Create a slew of transactions and announce them - var hashes []common.Hash - for i := 0; i < maxTxAnnounces; i++ { - hashes = append(hashes, common.Hash{byte(i / 256), byte(i % 256)}) - } - testTransactionFetcherParallel(t, txFetcherTest{ - init: func() *TxFetcher { - return NewTxFetcher( - func(common.Hash) bool { return false }, - nil, - func(string, []common.Hash) error { return nil }, - nil, - ) - }, - steps: []interface{}{ - // Announce all the transactions, wait a bit and ensure only a small - // percentage gets requested - doTxNotify{peer: "A", hashes: hashes}, - doWait{time: txArriveTimeout, step: true}, - isWaiting(nil), - isScheduled{ - tracking: map[string][]common.Hash{ - "A": hashes, - }, - fetching: map[string][]common.Hash{ - "A": hashes[1643 : 1643+maxTxRetrievals], - }, - }, - }, - }) -} - -// Tests that if huge transactions are announced, only a small number of them will -// be requested at a time, to keep the responses below a reasonable level. -func TestTransactionFetcherBandwidthLimiting(t *testing.T) { - testTransactionFetcherParallel(t, txFetcherTest{ - init: func() *TxFetcher { - return NewTxFetcher( - func(common.Hash) bool { return false }, - nil, - func(string, []common.Hash) error { return nil }, - nil, - ) - }, - steps: []interface{}{ - // Announce mid size transactions from A to verify that multiple - // ones can be piled into a single request. - doTxNotify{peer: "A", - hashes: []common.Hash{{0x01}, {0x02}, {0x03}, {0x04}}, - types: []byte{types.LegacyTxType, types.LegacyTxType, types.LegacyTxType, types.LegacyTxType}, - sizes: []uint32{48 * 1024, 48 * 1024, 48 * 1024, 48 * 1024}, - }, - // Announce exactly on the limit transactions to see that only one - // gets requested - doTxNotify{peer: "B", - hashes: []common.Hash{{0x05}, {0x06}}, - types: []byte{types.LegacyTxType, types.LegacyTxType}, - sizes: []uint32{maxTxRetrievalSize, maxTxRetrievalSize}, - }, - // Announce oversized blob transactions to see that overflows are ok - doTxNotify{peer: "C", - hashes: []common.Hash{{0x07}, {0x08}}, - types: []byte{types.BlobTxType, types.BlobTxType}, - sizes: []uint32{params.MaxBlobGasPerBlock, params.MaxBlobGasPerBlock}, - }, - doWait{time: txArriveTimeout, step: true}, - isWaiting(nil), - isScheduledWithMeta{ - tracking: map[string][]announce{ - "A": { - {common.Hash{0x01}, typeptr(types.LegacyTxType), sizeptr(48 * 1024)}, - {common.Hash{0x02}, typeptr(types.LegacyTxType), sizeptr(48 * 1024)}, - {common.Hash{0x03}, typeptr(types.LegacyTxType), sizeptr(48 * 1024)}, - {common.Hash{0x04}, typeptr(types.LegacyTxType), sizeptr(48 * 1024)}, - }, - "B": { - {common.Hash{0x05}, typeptr(types.LegacyTxType), sizeptr(maxTxRetrievalSize)}, - {common.Hash{0x06}, typeptr(types.LegacyTxType), sizeptr(maxTxRetrievalSize)}, - }, - "C": { - {common.Hash{0x07}, typeptr(types.BlobTxType), sizeptr(params.MaxBlobGasPerBlock)}, - {common.Hash{0x08}, typeptr(types.BlobTxType), sizeptr(params.MaxBlobGasPerBlock)}, - }, - }, - fetching: map[string][]common.Hash{ - "A": {{0x02}, {0x03}, {0x04}}, - "B": {{0x06}}, - "C": {{0x08}}, - }, - }, - }, - }) -} - -// Tests that then number of transactions a peer is allowed to announce and/or -// request at the same time is hard capped. -func TestTransactionFetcherDoSProtection(t *testing.T) { - // Create a slew of transactions and to announce them - var hashesA []common.Hash - for i := 0; i < maxTxAnnounces+1; i++ { - hashesA = append(hashesA, common.Hash{0x01, byte(i / 256), byte(i % 256)}) - } - var hashesB []common.Hash - for i := 0; i < maxTxAnnounces+1; i++ { - hashesB = append(hashesB, common.Hash{0x02, byte(i / 256), byte(i % 256)}) - } - testTransactionFetcherParallel(t, txFetcherTest{ - init: func() *TxFetcher { - return NewTxFetcher( - func(common.Hash) bool { return false }, - nil, - func(string, []common.Hash) error { return nil }, - nil, - ) - }, - steps: []interface{}{ - // Announce half of the transaction and wait for them to be scheduled - doTxNotify{peer: "A", hashes: hashesA[:maxTxAnnounces/2]}, - doTxNotify{peer: "B", hashes: hashesB[:maxTxAnnounces/2-1]}, - doWait{time: txArriveTimeout, step: true}, - - // Announce the second half and keep them in the wait list - doTxNotify{peer: "A", hashes: hashesA[maxTxAnnounces/2 : maxTxAnnounces]}, - doTxNotify{peer: "B", hashes: hashesB[maxTxAnnounces/2-1 : maxTxAnnounces-1]}, - - // Ensure the hashes are split half and half - isWaiting(map[string][]common.Hash{ - "A": hashesA[maxTxAnnounces/2 : maxTxAnnounces], - "B": hashesB[maxTxAnnounces/2-1 : maxTxAnnounces-1], - }), - isScheduled{ - tracking: map[string][]common.Hash{ - "A": hashesA[:maxTxAnnounces/2], - "B": hashesB[:maxTxAnnounces/2-1], - }, - fetching: map[string][]common.Hash{ - "A": hashesA[1643 : 1643+maxTxRetrievals], - "B": append(append([]common.Hash{}, hashesB[maxTxAnnounces/2-3:maxTxAnnounces/2-1]...), hashesB[:maxTxRetrievals-2]...), - }, - }, - // Ensure that adding even one more hash results in dropping the hash - doTxNotify{peer: "A", hashes: []common.Hash{hashesA[maxTxAnnounces]}}, - doTxNotify{peer: "B", hashes: hashesB[maxTxAnnounces-1 : maxTxAnnounces+1]}, - - isWaiting(map[string][]common.Hash{ - "A": hashesA[maxTxAnnounces/2 : maxTxAnnounces], - "B": hashesB[maxTxAnnounces/2-1 : maxTxAnnounces], - }), - isScheduled{ - tracking: map[string][]common.Hash{ - "A": hashesA[:maxTxAnnounces/2], - "B": hashesB[:maxTxAnnounces/2-1], - }, - fetching: map[string][]common.Hash{ - "A": hashesA[1643 : 1643+maxTxRetrievals], - "B": append(append([]common.Hash{}, hashesB[maxTxAnnounces/2-3:maxTxAnnounces/2-1]...), hashesB[:maxTxRetrievals-2]...), - }, - }, - }, - }) -} - -// Tests that underpriced transactions don't get rescheduled after being rejected. -func TestTransactionFetcherUnderpricedDedup(t *testing.T) { - testTransactionFetcherParallel(t, txFetcherTest{ - init: func() *TxFetcher { - return NewTxFetcher( - func(common.Hash) bool { return false }, - func(txs []*types.Transaction) []error { - errs := make([]error, len(txs)) - for i := 0; i < len(errs); i++ { - if i%2 == 0 { - errs[i] = txpool.ErrUnderpriced - } else { - errs[i] = txpool.ErrReplaceUnderpriced - } - } - return errs - }, - func(string, []common.Hash) error { return nil }, - nil, - ) - }, - steps: []interface{}{ - // Deliver a transaction through the fetcher, but reject as underpriced - doTxNotify{peer: "A", hashes: []common.Hash{testTxsHashes[0], testTxsHashes[1]}}, - doWait{time: txArriveTimeout, step: true}, - doTxEnqueue{peer: "A", txs: []*types.Transaction{testTxs[0], testTxs[1]}, direct: true}, - isScheduled{nil, nil, nil}, - - // Try to announce the transaction again, ensure it's not scheduled back - doTxNotify{peer: "A", hashes: []common.Hash{testTxsHashes[0], testTxsHashes[1], testTxsHashes[2]}}, // [2] is needed to force a step in the fetcher - isWaiting(map[string][]common.Hash{ - "A": {testTxsHashes[2]}, - }), - isScheduled{nil, nil, nil}, - }, - }) -} - -// Tests that underpriced transactions don't get rescheduled after being rejected, -// but at the same time there's a hard cap on the number of transactions that are -// tracked. -func TestTransactionFetcherUnderpricedDoSProtection(t *testing.T) { - // Temporarily disable fetch timeouts as they massively mess up the simulated clock - defer func(timeout time.Duration) { txFetchTimeout = timeout }(txFetchTimeout) - txFetchTimeout = 24 * time.Hour - - // Create a slew of transactions to max out the underpriced set - var txs []*types.Transaction - for i := 0; i < maxTxUnderpricedSetSize+1; i++ { - txs = append(txs, types.NewTransaction(rand.Uint64(), common.Address{byte(rand.Intn(256))}, new(big.Int), 0, new(big.Int), nil)) - } - hashes := make([]common.Hash, len(txs)) - for i, tx := range txs { - hashes[i] = tx.Hash() - } - // Generate a set of steps to announce and deliver the entire set of transactions - var steps []interface{} - for i := 0; i < maxTxUnderpricedSetSize/maxTxRetrievals; i++ { - steps = append(steps, doTxNotify{peer: "A", hashes: hashes[i*maxTxRetrievals : (i+1)*maxTxRetrievals]}) - steps = append(steps, isWaiting(map[string][]common.Hash{ - "A": hashes[i*maxTxRetrievals : (i+1)*maxTxRetrievals], - })) - steps = append(steps, doWait{time: txArriveTimeout, step: true}) - steps = append(steps, isScheduled{ - tracking: map[string][]common.Hash{ - "A": hashes[i*maxTxRetrievals : (i+1)*maxTxRetrievals], - }, - fetching: map[string][]common.Hash{ - "A": hashes[i*maxTxRetrievals : (i+1)*maxTxRetrievals], - }, - }) - steps = append(steps, doTxEnqueue{peer: "A", txs: txs[i*maxTxRetrievals : (i+1)*maxTxRetrievals], direct: true}) - steps = append(steps, isWaiting(nil)) - steps = append(steps, isScheduled{nil, nil, nil}) - steps = append(steps, isUnderpriced((i+1)*maxTxRetrievals)) - } - testTransactionFetcher(t, txFetcherTest{ - init: func() *TxFetcher { - return NewTxFetcher( - func(common.Hash) bool { return false }, - func(txs []*types.Transaction) []error { - errs := make([]error, len(txs)) - for i := 0; i < len(errs); i++ { - errs[i] = txpool.ErrUnderpriced - } - return errs - }, - func(string, []common.Hash) error { return nil }, - nil, - ) - }, - steps: append(steps, []interface{}{ - // The preparation of the test has already been done in `steps`, add the last check - doTxNotify{peer: "A", hashes: []common.Hash{hashes[maxTxUnderpricedSetSize]}}, - doWait{time: txArriveTimeout, step: true}, - doTxEnqueue{peer: "A", txs: []*types.Transaction{txs[maxTxUnderpricedSetSize]}, direct: true}, - isUnderpriced(maxTxUnderpricedSetSize), - }...), - }) -} - -// Tests that unexpected deliveries don't corrupt the internal state. -func TestTransactionFetcherOutOfBoundDeliveries(t *testing.T) { - testTransactionFetcherParallel(t, txFetcherTest{ - init: func() *TxFetcher { - return NewTxFetcher( - func(common.Hash) bool { return false }, - func(txs []*types.Transaction) []error { - return make([]error, len(txs)) - }, - func(string, []common.Hash) error { return nil }, - nil, - ) - }, - steps: []interface{}{ - // Deliver something out of the blue - isWaiting(nil), - isScheduled{nil, nil, nil}, - doTxEnqueue{peer: "A", txs: []*types.Transaction{testTxs[0]}, direct: false}, - isWaiting(nil), - isScheduled{nil, nil, nil}, - - // Set up a few hashes into various stages - doTxNotify{peer: "A", hashes: []common.Hash{testTxsHashes[0]}}, - doWait{time: txArriveTimeout, step: true}, - doTxNotify{peer: "A", hashes: []common.Hash{testTxsHashes[1]}}, - doWait{time: txArriveTimeout, step: true}, - doTxNotify{peer: "A", hashes: []common.Hash{testTxsHashes[2]}}, - - isWaiting(map[string][]common.Hash{ - "A": {testTxsHashes[2]}, - }), - isScheduled{ - tracking: map[string][]common.Hash{ - "A": {testTxsHashes[0], testTxsHashes[1]}, - }, - fetching: map[string][]common.Hash{ - "A": {testTxsHashes[0]}, - }, - }, - // Deliver everything and more out of the blue - doTxEnqueue{peer: "B", txs: []*types.Transaction{testTxs[0], testTxs[1], testTxs[2], testTxs[3]}, direct: true}, - isWaiting(nil), - isScheduled{ - tracking: nil, - fetching: nil, - dangling: map[string][]common.Hash{ - "A": {testTxsHashes[0]}, - }, - }, - }, - }) -} - -// Tests that dropping a peer cleans out all internal data structures in all the -// live or dangling stages. -func TestTransactionFetcherDrop(t *testing.T) { - testTransactionFetcherParallel(t, txFetcherTest{ - init: func() *TxFetcher { - return NewTxFetcher( - func(common.Hash) bool { return false }, - func(txs []*types.Transaction) []error { - return make([]error, len(txs)) - }, - func(string, []common.Hash) error { return nil }, - nil, - ) - }, - steps: []interface{}{ - // Set up a few hashes into various stages - doTxNotify{peer: "A", hashes: []common.Hash{{0x01}}}, - doWait{time: txArriveTimeout, step: true}, - doTxNotify{peer: "A", hashes: []common.Hash{{0x02}}}, - doWait{time: txArriveTimeout, step: true}, - doTxNotify{peer: "A", hashes: []common.Hash{{0x03}}}, - - isWaiting(map[string][]common.Hash{ - "A": {{0x03}}, - }), - isScheduled{ - tracking: map[string][]common.Hash{ - "A": {{0x01}, {0x02}}, - }, - fetching: map[string][]common.Hash{ - "A": {{0x01}}, - }, - }, - // Drop the peer and ensure everything's cleaned out - doDrop("A"), - isWaiting(nil), - isScheduled{nil, nil, nil}, - - // Push the node into a dangling (timeout) state - doTxNotify{peer: "A", hashes: []common.Hash{testTxsHashes[0]}}, - doWait{time: txArriveTimeout, step: true}, - isWaiting(nil), - isScheduled{ - tracking: map[string][]common.Hash{ - "A": {testTxsHashes[0]}, - }, - fetching: map[string][]common.Hash{ - "A": {testTxsHashes[0]}, - }, - }, - doWait{time: txFetchTimeout, step: true}, - isWaiting(nil), - isScheduled{ - tracking: nil, - fetching: nil, - dangling: map[string][]common.Hash{ - "A": {}, - }, - }, - // Drop the peer and ensure everything's cleaned out - doDrop("A"), - isWaiting(nil), - isScheduled{nil, nil, nil}, - }, - }) -} - -// Tests that dropping a peer instantly reschedules failed announcements to any -// available peer. -func TestTransactionFetcherDropRescheduling(t *testing.T) { - testTransactionFetcherParallel(t, txFetcherTest{ - init: func() *TxFetcher { - return NewTxFetcher( - func(common.Hash) bool { return false }, - func(txs []*types.Transaction) []error { - return make([]error, len(txs)) - }, - func(string, []common.Hash) error { return nil }, - nil, - ) - }, - steps: []interface{}{ - // Set up a few hashes into various stages - doTxNotify{peer: "A", hashes: []common.Hash{{0x01}}}, - doWait{time: txArriveTimeout, step: true}, - doTxNotify{peer: "B", hashes: []common.Hash{{0x01}}}, - - isWaiting(nil), - isScheduled{ - tracking: map[string][]common.Hash{ - "A": {{0x01}}, - "B": {{0x01}}, - }, - fetching: map[string][]common.Hash{ - "A": {{0x01}}, - }, - }, - // Drop the peer and ensure everything's cleaned out - doDrop("A"), - isWaiting(nil), - isScheduled{ - tracking: map[string][]common.Hash{ - "B": {{0x01}}, - }, - fetching: map[string][]common.Hash{ - "B": {{0x01}}, - }, - }, - }, - }) -} - -// Tests that announced transactions with the wrong transaction type or size will -// result in a dropped peer. -func TestInvalidAnnounceMetadata(t *testing.T) { - drop := make(chan string, 2) - testTransactionFetcherParallel(t, txFetcherTest{ - init: func() *TxFetcher { - return NewTxFetcher( - func(common.Hash) bool { return false }, - func(txs []*types.Transaction) []error { - return make([]error, len(txs)) - }, - func(string, []common.Hash) error { return nil }, - func(peer string) { drop <- peer }, - ) - }, - steps: []interface{}{ - // Initial announcement to get something into the waitlist - doTxNotify{peer: "A", hashes: []common.Hash{testTxsHashes[0], testTxsHashes[1]}, types: []byte{testTxs[0].Type(), testTxs[1].Type()}, sizes: []uint32{uint32(testTxs[0].Size()), uint32(testTxs[1].Size())}}, - isWaitingWithMeta(map[string][]announce{ - "A": { - {testTxsHashes[0], typeptr(testTxs[0].Type()), sizeptr(uint32(testTxs[0].Size()))}, - {testTxsHashes[1], typeptr(testTxs[1].Type()), sizeptr(uint32(testTxs[1].Size()))}, - }, - }), - // Announce from new peers conflicting transactions - doTxNotify{peer: "B", hashes: []common.Hash{testTxsHashes[0]}, types: []byte{testTxs[0].Type()}, sizes: []uint32{1024 + uint32(testTxs[0].Size())}}, - doTxNotify{peer: "C", hashes: []common.Hash{testTxsHashes[1]}, types: []byte{1 + testTxs[1].Type()}, sizes: []uint32{uint32(testTxs[1].Size())}}, - isWaitingWithMeta(map[string][]announce{ - "A": { - {testTxsHashes[0], typeptr(testTxs[0].Type()), sizeptr(uint32(testTxs[0].Size()))}, - {testTxsHashes[1], typeptr(testTxs[1].Type()), sizeptr(uint32(testTxs[1].Size()))}, - }, - "B": { - {testTxsHashes[0], typeptr(testTxs[0].Type()), sizeptr(1024 + uint32(testTxs[0].Size()))}, - }, - "C": { - {testTxsHashes[1], typeptr(1 + testTxs[1].Type()), sizeptr(uint32(testTxs[1].Size()))}, - }, - }), - // Schedule all the transactions for retrieval - doWait{time: txArriveTimeout, step: true}, - isWaitingWithMeta(nil), - isScheduledWithMeta{ - tracking: map[string][]announce{ - "A": { - {testTxsHashes[0], typeptr(testTxs[0].Type()), sizeptr(uint32(testTxs[0].Size()))}, - {testTxsHashes[1], typeptr(testTxs[1].Type()), sizeptr(uint32(testTxs[1].Size()))}, - }, - "B": { - {testTxsHashes[0], typeptr(testTxs[0].Type()), sizeptr(1024 + uint32(testTxs[0].Size()))}, - }, - "C": { - {testTxsHashes[1], typeptr(1 + testTxs[1].Type()), sizeptr(uint32(testTxs[1].Size()))}, - }, - }, - fetching: map[string][]common.Hash{ - "A": {testTxsHashes[0]}, - "C": {testTxsHashes[1]}, - }, - }, - // Deliver the transactions and wait for B to be dropped - doTxEnqueue{peer: "A", txs: []*types.Transaction{testTxs[0], testTxs[1]}}, - doFunc(func() { <-drop }), - doFunc(func() { <-drop }), - }, - }) -} - -// This test reproduces a crash caught by the fuzzer. The root cause was a -// dangling transaction timing out and clashing on re-add with a concurrently -// announced one. -func TestTransactionFetcherFuzzCrash01(t *testing.T) { - testTransactionFetcherParallel(t, txFetcherTest{ - init: func() *TxFetcher { - return NewTxFetcher( - func(common.Hash) bool { return false }, - func(txs []*types.Transaction) []error { - return make([]error, len(txs)) - }, - func(string, []common.Hash) error { return nil }, - nil, - ) - }, - steps: []interface{}{ - // Get a transaction into fetching mode and make it dangling with a broadcast - doTxNotify{peer: "A", hashes: []common.Hash{testTxsHashes[0]}}, - doWait{time: txArriveTimeout, step: true}, - doTxEnqueue{peer: "A", txs: []*types.Transaction{testTxs[0]}}, - - // Notify the dangling transaction once more and crash via a timeout - doTxNotify{peer: "A", hashes: []common.Hash{testTxsHashes[0]}}, - doWait{time: txFetchTimeout, step: true}, - }, - }) -} - -// This test reproduces a crash caught by the fuzzer. The root cause was a -// dangling transaction getting peer-dropped and clashing on re-add with a -// concurrently announced one. -func TestTransactionFetcherFuzzCrash02(t *testing.T) { - testTransactionFetcherParallel(t, txFetcherTest{ - init: func() *TxFetcher { - return NewTxFetcher( - func(common.Hash) bool { return false }, - func(txs []*types.Transaction) []error { - return make([]error, len(txs)) - }, - func(string, []common.Hash) error { return nil }, - nil, - ) - }, - steps: []interface{}{ - // Get a transaction into fetching mode and make it dangling with a broadcast - doTxNotify{peer: "A", hashes: []common.Hash{testTxsHashes[0]}}, - doWait{time: txArriveTimeout, step: true}, - doTxEnqueue{peer: "A", txs: []*types.Transaction{testTxs[0]}}, - - // Notify the dangling transaction once more, re-fetch, and crash via a drop and timeout - doTxNotify{peer: "B", hashes: []common.Hash{testTxsHashes[0]}}, - doWait{time: txArriveTimeout, step: true}, - doDrop("A"), - doWait{time: txFetchTimeout, step: true}, - }, - }) -} - -// This test reproduces a crash caught by the fuzzer. The root cause was a -// dangling transaction getting rescheduled via a partial delivery, clashing -// with a concurrent notify. -func TestTransactionFetcherFuzzCrash03(t *testing.T) { - testTransactionFetcherParallel(t, txFetcherTest{ - init: func() *TxFetcher { - return NewTxFetcher( - func(common.Hash) bool { return false }, - func(txs []*types.Transaction) []error { - return make([]error, len(txs)) - }, - func(string, []common.Hash) error { return nil }, - nil, - ) - }, - steps: []interface{}{ - // Get a transaction into fetching mode and make it dangling with a broadcast - doTxNotify{peer: "A", hashes: []common.Hash{testTxsHashes[0], testTxsHashes[1]}}, - doWait{time: txFetchTimeout, step: true}, - doTxEnqueue{peer: "A", txs: []*types.Transaction{testTxs[0], testTxs[1]}}, - - // Notify the dangling transaction once more, partially deliver, clash&crash with a timeout - doTxNotify{peer: "B", hashes: []common.Hash{testTxsHashes[0]}}, - doWait{time: txArriveTimeout, step: true}, - - doTxEnqueue{peer: "A", txs: []*types.Transaction{testTxs[1]}, direct: true}, - doWait{time: txFetchTimeout, step: true}, - }, - }) -} - -// This test reproduces a crash caught by the fuzzer. The root cause was a -// dangling transaction getting rescheduled via a disconnect, clashing with -// a concurrent notify. -func TestTransactionFetcherFuzzCrash04(t *testing.T) { - // Create a channel to control when tx requests can fail - proceed := make(chan struct{}) - - testTransactionFetcherParallel(t, txFetcherTest{ - init: func() *TxFetcher { - return NewTxFetcher( - func(common.Hash) bool { return false }, - func(txs []*types.Transaction) []error { - return make([]error, len(txs)) - }, - func(string, []common.Hash) error { - <-proceed - return errors.New("peer disconnected") - }, - nil, - ) - }, - steps: []interface{}{ - // Get a transaction into fetching mode and make it dangling with a broadcast - doTxNotify{peer: "A", hashes: []common.Hash{testTxsHashes[0]}}, - doWait{time: txArriveTimeout, step: true}, - doTxEnqueue{peer: "A", txs: []*types.Transaction{testTxs[0]}}, - - // Notify the dangling transaction once more, re-fetch, and crash via an in-flight disconnect - doTxNotify{peer: "B", hashes: []common.Hash{testTxsHashes[0]}}, - doWait{time: txArriveTimeout, step: true}, - doFunc(func() { - proceed <- struct{}{} // Allow peer A to return the failure - }), - doWait{time: 0, step: true}, - doWait{time: txFetchTimeout, step: true}, - }, - }) -} - -func testTransactionFetcherParallel(t *testing.T, tt txFetcherTest) { - t.Parallel() - testTransactionFetcher(t, tt) -} - -func testTransactionFetcher(t *testing.T, tt txFetcherTest) { - // Create a fetcher and hook into it's simulated fields - clock := new(mclock.Simulated) - wait := make(chan struct{}) - - fetcher := tt.init() - fetcher.clock = clock - fetcher.step = wait - fetcher.rand = rand.New(rand.NewSource(0x3a29)) - - fetcher.Start() - defer fetcher.Stop() - - defer func() { // drain the wait chan on exit - for { - select { - case <-wait: - default: - return - } - } - }() - - // Crunch through all the test steps and execute them - for i, step := range tt.steps { - // Auto-expand certain steps to ones with metadata - switch old := step.(type) { - case isWaiting: - new := make(isWaitingWithMeta) - for peer, hashes := range old { - for _, hash := range hashes { - new[peer] = append(new[peer], announce{hash, nil, nil}) - } - } - step = new - - case isScheduled: - new := isScheduledWithMeta{ - tracking: make(map[string][]announce), - fetching: old.fetching, - dangling: old.dangling, - } - for peer, hashes := range old.tracking { - for _, hash := range hashes { - new.tracking[peer] = append(new.tracking[peer], announce{hash, nil, nil}) - } - } - step = new - } - // Process the original or expanded steps - switch step := step.(type) { - case doTxNotify: - if err := fetcher.Notify(step.peer, step.types, step.sizes, step.hashes); err != nil { - t.Errorf("step %d: %v", i, err) - } - <-wait // Fetcher needs to process this, wait until it's done - select { - case <-wait: - panic("wtf") - case <-time.After(time.Millisecond): - } - - case doTxEnqueue: - if err := fetcher.Enqueue(step.peer, step.txs, step.direct); err != nil { - t.Errorf("step %d: %v", i, err) - } - <-wait // Fetcher needs to process this, wait until it's done - - case doWait: - clock.Run(step.time) - if step.step { - <-wait // Fetcher supposed to do something, wait until it's done - } - - case doDrop: - if err := fetcher.Drop(string(step)); err != nil { - t.Errorf("step %d: %v", i, err) - } - <-wait // Fetcher needs to process this, wait until it's done - - case doFunc: - step() - - case isWaitingWithMeta: - // We need to check that the waiting list (stage 1) internals - // match with the expected set. Check the peer->hash mappings - // first. - for peer, announces := range step { - waiting := fetcher.waitslots[peer] - if waiting == nil { - t.Errorf("step %d: peer %s missing from waitslots", i, peer) - continue - } - for _, ann := range announces { - if meta, ok := waiting[ann.hash]; !ok { - t.Errorf("step %d, peer %s: hash %x missing from waitslots", i, peer, ann.hash) - } else { - if (meta == nil && (ann.kind != nil || ann.size != nil)) || - (meta != nil && (ann.kind == nil || ann.size == nil)) || - (meta != nil && (meta.kind != *ann.kind || meta.size != *ann.size)) { - t.Errorf("step %d, peer %s, hash %x: waitslot metadata mismatch: want %v, have %v/%v", i, peer, ann.hash, meta, *ann.kind, *ann.size) - } - } - } - for hash, meta := range waiting { - ann := announce{hash: hash} - if meta != nil { - ann.kind, ann.size = &meta.kind, &meta.size - } - if !containsAnnounce(announces, ann) { - t.Errorf("step %d, peer %s: announce %v extra in waitslots", i, peer, ann) - } - } - } - for peer := range fetcher.waitslots { - if _, ok := step[peer]; !ok { - t.Errorf("step %d: peer %s extra in waitslots", i, peer) - } - } - // Peer->hash sets correct, check the hash->peer and timeout sets - for peer, announces := range step { - for _, ann := range announces { - if _, ok := fetcher.waitlist[ann.hash][peer]; !ok { - t.Errorf("step %d, hash %x: peer %s missing from waitlist", i, ann.hash, peer) - } - if _, ok := fetcher.waittime[ann.hash]; !ok { - t.Errorf("step %d: hash %x missing from waittime", i, ann.hash) - } - } - } - for hash, peers := range fetcher.waitlist { - if len(peers) == 0 { - t.Errorf("step %d, hash %x: empty peerset in waitlist", i, hash) - } - for peer := range peers { - if !containsHashInAnnounces(step[peer], hash) { - t.Errorf("step %d, hash %x: peer %s extra in waitlist", i, hash, peer) - } - } - } - for hash := range fetcher.waittime { - var found bool - for _, announces := range step { - if containsHashInAnnounces(announces, hash) { - found = true - break - } - } - if !found { - t.Errorf("step %d,: hash %x extra in waittime", i, hash) - } - } - - case isScheduledWithMeta: - // Check that all scheduled announces are accounted for and no - // extra ones are present. - for peer, announces := range step.tracking { - scheduled := fetcher.announces[peer] - if scheduled == nil { - t.Errorf("step %d: peer %s missing from announces", i, peer) - continue - } - for _, ann := range announces { - if meta, ok := scheduled[ann.hash]; !ok { - t.Errorf("step %d, peer %s: hash %x missing from announces", i, peer, ann.hash) - } else { - if (meta == nil && (ann.kind != nil || ann.size != nil)) || - (meta != nil && (ann.kind == nil || ann.size == nil)) || - (meta != nil && (meta.kind != *ann.kind || meta.size != *ann.size)) { - t.Errorf("step %d, peer %s, hash %x: announce metadata mismatch: want %v, have %v/%v", i, peer, ann.hash, meta, *ann.kind, *ann.size) - } - } - } - for hash, meta := range scheduled { - ann := announce{hash: hash} - if meta != nil { - ann.kind, ann.size = &meta.kind, &meta.size - } - if !containsAnnounce(announces, ann) { - t.Errorf("step %d, peer %s: announce %x extra in announces", i, peer, hash) - } - } - } - for peer := range fetcher.announces { - if _, ok := step.tracking[peer]; !ok { - t.Errorf("step %d: peer %s extra in announces", i, peer) - } - } - // Check that all announces required to be fetching are in the - // appropriate sets - for peer, hashes := range step.fetching { - request := fetcher.requests[peer] - if request == nil { - t.Errorf("step %d: peer %s missing from requests", i, peer) - continue - } - for _, hash := range hashes { - if !containsHash(request.hashes, hash) { - t.Errorf("step %d, peer %s: hash %x missing from requests", i, peer, hash) - } - } - for _, hash := range request.hashes { - if !containsHash(hashes, hash) { - t.Errorf("step %d, peer %s: hash %x extra in requests", i, peer, hash) - } - } - } - for peer := range fetcher.requests { - if _, ok := step.fetching[peer]; !ok { - if _, ok := step.dangling[peer]; !ok { - t.Errorf("step %d: peer %s extra in requests", i, peer) - } - } - } - for peer, hashes := range step.fetching { - for _, hash := range hashes { - if _, ok := fetcher.fetching[hash]; !ok { - t.Errorf("step %d, peer %s: hash %x missing from fetching", i, peer, hash) - } - } - } - for hash := range fetcher.fetching { - var found bool - for _, req := range fetcher.requests { - if containsHash(req.hashes, hash) { - found = true - break - } - } - if !found { - t.Errorf("step %d: hash %x extra in fetching", i, hash) - } - } - for _, hashes := range step.fetching { - for _, hash := range hashes { - alternates := fetcher.alternates[hash] - if alternates == nil { - t.Errorf("step %d: hash %x missing from alternates", i, hash) - continue - } - for peer := range alternates { - if _, ok := fetcher.announces[peer]; !ok { - t.Errorf("step %d: peer %s extra in alternates", i, peer) - continue - } - if _, ok := fetcher.announces[peer][hash]; !ok { - t.Errorf("step %d, peer %s: hash %x extra in alternates", i, hash, peer) - continue - } - } - for p := range fetcher.announced[hash] { - if _, ok := alternates[p]; !ok { - t.Errorf("step %d, hash %x: peer %s missing from alternates", i, hash, p) - continue - } - } - } - } - for peer, hashes := range step.dangling { - request := fetcher.requests[peer] - if request == nil { - t.Errorf("step %d: peer %s missing from requests", i, peer) - continue - } - for _, hash := range hashes { - if !containsHash(request.hashes, hash) { - t.Errorf("step %d, peer %s: hash %x missing from requests", i, peer, hash) - } - } - for _, hash := range request.hashes { - if !containsHash(hashes, hash) { - t.Errorf("step %d, peer %s: hash %x extra in requests", i, peer, hash) - } - } - } - // Check that all transaction announces that are scheduled for - // retrieval but not actively being downloaded are tracked only - // in the stage 2 `announced` map. - var queued []common.Hash - for _, announces := range step.tracking { - for _, ann := range announces { - var found bool - for _, hs := range step.fetching { - if containsHash(hs, ann.hash) { - found = true - break - } - } - if !found { - queued = append(queued, ann.hash) - } - } - } - for _, hash := range queued { - if _, ok := fetcher.announced[hash]; !ok { - t.Errorf("step %d: hash %x missing from announced", i, hash) - } - } - for hash := range fetcher.announced { - if !containsHash(queued, hash) { - t.Errorf("step %d: hash %x extra in announced", i, hash) - } - } - - case isUnderpriced: - if fetcher.underpriced.Len() != int(step) { - t.Errorf("step %d: underpriced set size mismatch: have %d, want %d", i, fetcher.underpriced.Len(), step) - } - - default: - t.Fatalf("step %d: unknown step type %T", i, step) - } - // After every step, cross validate the internal uniqueness invariants - // between stage one and stage two. - for hash := range fetcher.waittime { - if _, ok := fetcher.announced[hash]; ok { - t.Errorf("step %d: hash %s present in both stage 1 and 2", i, hash) - } - } - } -} - -// containsAnnounce returns whether an announcement is contained within a slice -// of announcements. -func containsAnnounce(slice []announce, ann announce) bool { - for _, have := range slice { - if have.hash == ann.hash { - if have.kind == nil || ann.kind == nil { - if have.kind != ann.kind { - return false - } - } else if *have.kind != *ann.kind { - return false - } - if have.size == nil || ann.size == nil { - if have.size != ann.size { - return false - } - } else if *have.size != *ann.size { - return false - } - return true - } - } - return false -} - -// containsHashInAnnounces returns whether a hash is contained within a slice -// of announcements. -func containsHashInAnnounces(slice []announce, hash common.Hash) bool { - for _, have := range slice { - if have.hash == hash { - return true - } - } - return false -} - -// containsHash returns whether a hash is contained within a hash slice. -func containsHash(slice []common.Hash, hash common.Hash) bool { - for _, have := range slice { - if have == hash { - return true - } - } - return false -} - -// Tests that a transaction is forgotten after the timeout. -func TestTransactionForgotten(t *testing.T) { - fetcher := NewTxFetcher( - func(common.Hash) bool { return false }, - func(txs []*types.Transaction) []error { - errs := make([]error, len(txs)) - for i := 0; i < len(errs); i++ { - errs[i] = txpool.ErrUnderpriced - } - return errs - }, - func(string, []common.Hash) error { return nil }, - func(string) {}, - ) - fetcher.Start() - defer fetcher.Stop() - // Create one TX which is 5 minutes old, and one which is recent - tx1 := types.NewTx(&types.LegacyTx{Nonce: 0}) - tx1.SetTime(time.Now().Add(-maxTxUnderpricedTimeout - 1*time.Second)) - tx2 := types.NewTx(&types.LegacyTx{Nonce: 1}) - - // Enqueue both in the fetcher. They will be immediately tagged as underpriced - if err := fetcher.Enqueue("asdf", []*types.Transaction{tx1, tx2}, false); err != nil { - t.Fatal(err) - } - // isKnownUnderpriced should trigger removal of the first tx (no longer be known underpriced) - if fetcher.isKnownUnderpriced(tx1.Hash()) { - t.Fatal("transaction should be forgotten by now") - } - // isKnownUnderpriced should not trigger removal of the second - if !fetcher.isKnownUnderpriced(tx2.Hash()) { - t.Fatal("transaction should be known underpriced") - } -} diff --git a/eth/gasestimator/gasestimator.go b/eth/gasestimator/gasestimator.go deleted file mode 100644 index a36c670747..0000000000 --- a/eth/gasestimator/gasestimator.go +++ /dev/null @@ -1,235 +0,0 @@ -// Copyright 2023 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 . - -package gasestimator - -import ( - "context" - "errors" - "fmt" - "math" - "math/big" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/state" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/vm" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/params" -) - -// Options are the contextual parameters to execute the requested call. -// -// Whilst it would be possible to pass a blockchain object that aggregates all -// these together, it would be excessively hard to test. Splitting the parts out -// allows testing without needing a proper live chain. -type Options struct { - Config *params.ChainConfig // Chain configuration for hard fork selection - Chain core.ChainContext // Chain context to access past block hashes - Header *types.Header // Header defining the block context to execute in - State *state.StateDB // Pre-state on top of which to estimate the gas - - ErrorRatio float64 // Allowed overestimation ratio for faster estimation termination -} - -// Estimate returns the lowest possible gas limit that allows the transaction to -// run successfully with the provided context options. It returns an error if the -// transaction would always revert, or if there are unexpected failures. -func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uint64) (uint64, []byte, error) { - // Binary search the gas limit, as it may need to be higher than the amount used - var ( - lo uint64 // lowest-known gas limit where tx execution fails - hi uint64 // lowest-known gas limit where tx execution succeeds - ) - // Determine the highest gas limit can be used during the estimation. - hi = opts.Header.GasLimit - if call.GasLimit >= params.TxGas { - hi = call.GasLimit - } - // Normalize the max fee per gas the call is willing to spend. - var feeCap *big.Int - if call.GasFeeCap != nil { - feeCap = call.GasFeeCap - } else if call.GasPrice != nil { - feeCap = call.GasPrice - } else { - feeCap = common.Big0 - } - // Recap the highest gas limit with account's available balance. - if feeCap.BitLen() != 0 { - balance := opts.State.GetBalance(call.From) - - available := new(big.Int).Set(balance) - if call.Value != nil { - if call.Value.Cmp(available) >= 0 { - return 0, nil, core.ErrInsufficientFundsForTransfer - } - available.Sub(available, call.Value) - } - allowance := new(big.Int).Div(available, feeCap) - - // If the allowance is larger than maximum uint64, skip checking - if allowance.IsUint64() && hi > allowance.Uint64() { - transfer := call.Value - if transfer == nil { - transfer = new(big.Int) - } - log.Debug("Gas estimation capped by limited funds", "original", hi, "balance", balance, - "sent", transfer, "maxFeePerGas", feeCap, "fundable", allowance) - hi = allowance.Uint64() - } - } - // Recap the highest gas allowance with specified gascap. - if gasCap != 0 && hi > gasCap { - log.Debug("Caller gas above allowance, capping", "requested", hi, "cap", gasCap) - hi = gasCap - } - // If the transaction is a plain value transfer, short circuit estimation and - // directly try 21000. Returning 21000 without any execution is dangerous as - // some tx field combos might bump the price up even for plain transfers (e.g. - // unused access list items). Ever so slightly wasteful, but safer overall. - if len(call.Data) == 0 { - if call.To != nil && opts.State.GetCodeSize(*call.To) == 0 { - failed, _, err := execute(ctx, call, opts, params.TxGas) - if !failed && err == nil { - return params.TxGas, nil, nil - } - } - } - // We first execute the transaction at the highest allowable gas limit, since if this fails we - // can return error immediately. - failed, result, err := execute(ctx, call, opts, hi) - if err != nil { - return 0, nil, err - } - if failed { - if result != nil && !errors.Is(result.Err, vm.ErrOutOfGas) { - return 0, result.Revert(), result.Err - } - return 0, nil, fmt.Errorf("gas required exceeds allowance (%d)", hi) - } - // For almost any transaction, the gas consumed by the unconstrained execution - // above lower-bounds the gas limit required for it to succeed. One exception - // is those that explicitly check gas remaining in order to execute within a - // given limit, but we probably don't want to return the lowest possible gas - // limit for these cases anyway. - lo = result.UsedGas - 1 - - // There's a fairly high chance for the transaction to execute successfully - // with gasLimit set to the first execution's usedGas + gasRefund. Explicitly - // check that gas amount and use as a limit for the binary search. - optimisticGasLimit := (result.UsedGas + result.RefundedGas + params.CallStipend) * 64 / 63 - if optimisticGasLimit < hi { - failed, _, err = execute(ctx, call, opts, optimisticGasLimit) - if err != nil { - // This should not happen under normal conditions since if we make it this far the - // transaction had run without error at least once before. - log.Error("Execution error in estimate gas", "err", err) - return 0, nil, err - } - if failed { - lo = optimisticGasLimit - } else { - hi = optimisticGasLimit - } - } - // Binary search for the smallest gas limit that allows the tx to execute successfully. - for lo+1 < hi { - if opts.ErrorRatio > 0 { - // It is a bit pointless to return a perfect estimation, as changing - // network conditions require the caller to bump it up anyway. Since - // wallets tend to use 20-25% bump, allowing a small approximation - // error is fine (as long as it's upwards). - if float64(hi-lo)/float64(hi) < opts.ErrorRatio { - break - } - } - mid := (hi + lo) / 2 - if mid > lo*2 { - // Most txs don't need much higher gas limit than their gas used, and most txs don't - // require near the full block limit of gas, so the selection of where to bisect the - // range here is skewed to favor the low side. - mid = lo * 2 - } - failed, _, err = execute(ctx, call, opts, mid) - if err != nil { - // This should not happen under normal conditions since if we make it this far the - // transaction had run without error at least once before. - log.Error("Execution error in estimate gas", "err", err) - return 0, nil, err - } - if failed { - lo = mid - } else { - hi = mid - } - } - return hi, nil, nil -} - -// execute is a helper that executes the transaction under a given gas limit and -// returns true if the transaction fails for a reason that might be related to -// not enough gas. A non-nil error means execution failed due to reasons unrelated -// to the gas limit. -func execute(ctx context.Context, call *core.Message, opts *Options, gasLimit uint64) (bool, *core.ExecutionResult, error) { - // Configure the call for this specific execution (and revert the change after) - defer func(gas uint64) { call.GasLimit = gas }(call.GasLimit) - call.GasLimit = gasLimit - - // Execute the call and separate execution faults caused by a lack of gas or - // other non-fixable conditions - result, err := run(ctx, call, opts) - if err != nil { - if errors.Is(err, core.ErrIntrinsicGas) { - return true, nil, nil // Special case, raise gas limit - } - return true, nil, err // Bail out - } - return result.Failed(), result, nil -} - -// run assembles the EVM as defined by the consensus rules and runs the requested -// call invocation. -func run(ctx context.Context, call *core.Message, opts *Options) (*core.ExecutionResult, error) { - // Assemble the call and the call context - var ( - msgContext = core.NewEVMTxContext(call) - evmContext = core.NewEVMBlockContext(opts.Header, opts.Chain, nil) - - dirtyState = opts.State.Copy() - evm = vm.NewEVM(evmContext, msgContext, dirtyState, opts.Config, vm.Config{NoBaseFee: true}) - ) - // Monitor the outer context and interrupt the EVM upon cancellation. To avoid - // a dangling goroutine until the outer estimation finishes, create an internal - // context for the lifetime of this method call. - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - go func() { - <-ctx.Done() - evm.Cancel() - }() - // Execute the call, returning a wrapped error or the result - result, err := core.ApplyMessage(evm, call, new(core.GasPool).AddGas(math.MaxUint64)) - if vmerr := dirtyState.Error(); vmerr != nil { - return nil, vmerr - } - if err != nil { - return result, fmt.Errorf("failed with %d gas: %w", call.GasLimit, err) - } - return result, nil -} diff --git a/eth/handler.go b/eth/handler.go deleted file mode 100644 index a327af6113..0000000000 --- a/eth/handler.go +++ /dev/null @@ -1,692 +0,0 @@ -// Copyright 2015 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 . - -package eth - -import ( - "errors" - "math" - "math/big" - "sync" - "sync/atomic" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/consensus" - "github.com/ethereum/go-ethereum/consensus/beacon" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/forkid" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/txpool" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/eth/downloader" - "github.com/ethereum/go-ethereum/eth/fetcher" - "github.com/ethereum/go-ethereum/eth/protocols/eth" - "github.com/ethereum/go-ethereum/eth/protocols/snap" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/event" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/metrics" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/trie/triedb/pathdb" -) - -const ( - // txChanSize is the size of channel listening to NewTxsEvent. - // The number is referenced from the size of tx pool. - txChanSize = 4096 - - // txMaxBroadcastSize is the max size of a transaction that will be broadcasted. - // All transactions with a higher size will be announced and need to be fetched - // by the peer. - txMaxBroadcastSize = 4096 -) - -var syncChallengeTimeout = 15 * time.Second // Time allowance for a node to reply to the sync progress challenge - -// txPool defines the methods needed from a transaction pool implementation to -// support all the operations needed by the Ethereum chain protocols. -type txPool interface { - // Has returns an indicator whether txpool has a transaction - // cached with the given hash. - Has(hash common.Hash) bool - - // Get retrieves the transaction from local txpool with given - // tx hash. - Get(hash common.Hash) *types.Transaction - - // Add should add the given transactions to the pool. - Add(txs []*types.Transaction, local bool, sync bool) []error - - // Pending should return pending transactions. - // The slice should be modifiable by the caller. - Pending(enforceTips bool) map[common.Address][]*txpool.LazyTransaction - - // SubscribeTransactions subscribes to new transaction events. The subscriber - // can decide whether to receive notifications only for newly seen transactions - // or also for reorged out ones. - SubscribeTransactions(ch chan<- core.NewTxsEvent, reorgs bool) event.Subscription -} - -// handlerConfig is the collection of initialization parameters to create a full -// node network handler. -type handlerConfig struct { - Database ethdb.Database // Database for direct sync insertions - Chain *core.BlockChain // Blockchain to serve data from - TxPool txPool // Transaction pool to propagate from - Merger *consensus.Merger // The manager for eth1/2 transition - Network uint64 // Network identifier to advertise - Sync downloader.SyncMode // Whether to snap or full sync - BloomCache uint64 // Megabytes to alloc for snap sync bloom - EventMux *event.TypeMux // Legacy event mux, deprecate for `feed` - RequiredBlocks map[uint64]common.Hash // Hard coded map of required block hashes for sync challenges -} - -type handler struct { - networkID uint64 - forkFilter forkid.Filter // Fork ID filter, constant across the lifetime of the node - - snapSync atomic.Bool // Flag whether snap sync is enabled (gets disabled if we already have blocks) - synced atomic.Bool // Flag whether we're considered synchronised (enables transaction processing) - - database ethdb.Database - txpool txPool - chain *core.BlockChain - maxPeers int - - downloader *downloader.Downloader - blockFetcher *fetcher.BlockFetcher - txFetcher *fetcher.TxFetcher - peers *peerSet - merger *consensus.Merger - - eventMux *event.TypeMux - txsCh chan core.NewTxsEvent - txsSub event.Subscription - minedBlockSub *event.TypeMuxSubscription - - requiredBlocks map[uint64]common.Hash - - // channels for fetcher, syncer, txsyncLoop - quitSync chan struct{} - - chainSync *chainSyncer - wg sync.WaitGroup - - handlerStartCh chan struct{} - handlerDoneCh chan struct{} -} - -// newHandler returns a handler for all Ethereum chain management protocol. -func newHandler(config *handlerConfig) (*handler, error) { - // Create the protocol manager with the base fields - if config.EventMux == nil { - config.EventMux = new(event.TypeMux) // Nicety initialization for tests - } - h := &handler{ - networkID: config.Network, - forkFilter: forkid.NewFilter(config.Chain), - eventMux: config.EventMux, - database: config.Database, - txpool: config.TxPool, - chain: config.Chain, - peers: newPeerSet(), - merger: config.Merger, - requiredBlocks: config.RequiredBlocks, - quitSync: make(chan struct{}), - handlerDoneCh: make(chan struct{}), - handlerStartCh: make(chan struct{}), - } - if config.Sync == downloader.FullSync { - // The database seems empty as the current block is the genesis. Yet the snap - // block is ahead, so snap sync was enabled for this node at a certain point. - // The scenarios where this can happen is - // * if the user manually (or via a bad block) rolled back a snap sync node - // below the sync point. - // * the last snap sync is not finished while user specifies a full sync this - // time. But we don't have any recent state for full sync. - // In these cases however it's safe to reenable snap sync. - fullBlock, snapBlock := h.chain.CurrentBlock(), h.chain.CurrentSnapBlock() - if fullBlock.Number.Uint64() == 0 && snapBlock.Number.Uint64() > 0 { - h.snapSync.Store(true) - log.Warn("Switch sync mode from full sync to snap sync", "reason", "snap sync incomplete") - } else if !h.chain.HasState(fullBlock.Root) { - h.snapSync.Store(true) - log.Warn("Switch sync mode from full sync to snap sync", "reason", "head state missing") - } - } else { - head := h.chain.CurrentBlock() - if head.Number.Uint64() > 0 && h.chain.HasState(head.Root) { - // Print warning log if database is not empty to run snap sync. - log.Warn("Switch sync mode from snap sync to full sync", "reason", "snap sync complete") - } else { - // If snap sync was requested and our database is empty, grant it - h.snapSync.Store(true) - log.Info("Enabled snap sync", "head", head.Number, "hash", head.Hash()) - } - } - // If snap sync is requested but snapshots are disabled, fail loudly - if h.snapSync.Load() && config.Chain.Snapshots() == nil { - return nil, errors.New("snap sync not supported with snapshots disabled") - } - // Construct the downloader (long sync) - h.downloader = downloader.New(config.Database, h.eventMux, h.chain, nil, h.removePeer, h.enableSyncedFeatures) - if ttd := h.chain.Config().TerminalTotalDifficulty; ttd != nil { - if h.chain.Config().TerminalTotalDifficultyPassed { - log.Info("Chain post-merge, sync via beacon client") - } else { - head := h.chain.CurrentBlock() - if td := h.chain.GetTd(head.Hash(), head.Number.Uint64()); td.Cmp(ttd) >= 0 { - log.Info("Chain post-TTD, sync via beacon client") - } else { - log.Warn("Chain pre-merge, sync via PoW (ensure beacon client is ready)") - } - } - } else if h.chain.Config().TerminalTotalDifficultyPassed { - log.Error("Chain configured post-merge, but without TTD. Are you debugging sync?") - } - // Construct the fetcher (short sync) - validator := func(header *types.Header) error { - // All the block fetcher activities should be disabled - // after the transition. Print the warning log. - if h.merger.PoSFinalized() { - log.Warn("Unexpected validation activity", "hash", header.Hash(), "number", header.Number) - return errors.New("unexpected behavior after transition") - } - // Reject all the PoS style headers in the first place. No matter - // the chain has finished the transition or not, the PoS headers - // should only come from the trusted consensus layer instead of - // p2p network. - if beacon, ok := h.chain.Engine().(*beacon.Beacon); ok { - if beacon.IsPoSHeader(header) { - return errors.New("unexpected post-merge header") - } - } - return h.chain.Engine().VerifyHeader(h.chain, header) - } - heighter := func() uint64 { - return h.chain.CurrentBlock().Number.Uint64() - } - inserter := func(blocks types.Blocks) (int, error) { - // All the block fetcher activities should be disabled - // after the transition. Print the warning log. - if h.merger.PoSFinalized() { - var ctx []interface{} - ctx = append(ctx, "blocks", len(blocks)) - if len(blocks) > 0 { - ctx = append(ctx, "firsthash", blocks[0].Hash()) - ctx = append(ctx, "firstnumber", blocks[0].Number()) - ctx = append(ctx, "lasthash", blocks[len(blocks)-1].Hash()) - ctx = append(ctx, "lastnumber", blocks[len(blocks)-1].Number()) - } - log.Warn("Unexpected insertion activity", ctx...) - return 0, errors.New("unexpected behavior after transition") - } - // If snap sync is running, deny importing weird blocks. This is a problematic - // clause when starting up a new network, because snap-syncing miners might not - // accept each others' blocks until a restart. Unfortunately we haven't figured - // out a way yet where nodes can decide unilaterally whether the network is new - // or not. This should be fixed if we figure out a solution. - if !h.synced.Load() { - log.Warn("Syncing, discarded propagated block", "number", blocks[0].Number(), "hash", blocks[0].Hash()) - return 0, nil - } - if h.merger.TDDReached() { - // The blocks from the p2p network is regarded as untrusted - // after the transition. In theory block gossip should be disabled - // entirely whenever the transition is started. But in order to - // handle the transition boundary reorg in the consensus-layer, - // the legacy blocks are still accepted, but only for the terminal - // pow blocks. Spec: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-3675.md#halt-the-importing-of-pow-blocks - for i, block := range blocks { - ptd := h.chain.GetTd(block.ParentHash(), block.NumberU64()-1) - if ptd == nil { - return 0, nil - } - td := new(big.Int).Add(ptd, block.Difficulty()) - if !h.chain.Config().IsTerminalPoWBlock(ptd, td) { - log.Info("Filtered out non-terminal pow block", "number", block.NumberU64(), "hash", block.Hash()) - return 0, nil - } - if err := h.chain.InsertBlockWithoutSetHead(block); err != nil { - return i, err - } - } - return 0, nil - } - return h.chain.InsertChain(blocks) - } - h.blockFetcher = fetcher.NewBlockFetcher(false, nil, h.chain.GetBlockByHash, validator, h.BroadcastBlock, heighter, nil, inserter, h.removePeer) - - fetchTx := func(peer string, hashes []common.Hash) error { - p := h.peers.peer(peer) - if p == nil { - return errors.New("unknown peer") - } - return p.RequestTxs(hashes) - } - addTxs := func(txs []*types.Transaction) []error { - return h.txpool.Add(txs, false, false) - } - h.txFetcher = fetcher.NewTxFetcher(h.txpool.Has, addTxs, fetchTx, h.removePeer) - h.chainSync = newChainSyncer(h) - return h, nil -} - -// protoTracker tracks the number of active protocol handlers. -func (h *handler) protoTracker() { - defer h.wg.Done() - var active int - for { - select { - case <-h.handlerStartCh: - active++ - case <-h.handlerDoneCh: - active-- - case <-h.quitSync: - // Wait for all active handlers to finish. - for ; active > 0; active-- { - <-h.handlerDoneCh - } - return - } - } -} - -// incHandlers signals to increment the number of active handlers if not -// quitting. -func (h *handler) incHandlers() bool { - select { - case h.handlerStartCh <- struct{}{}: - return true - case <-h.quitSync: - return false - } -} - -// decHandlers signals to decrement the number of active handlers. -func (h *handler) decHandlers() { - h.handlerDoneCh <- struct{}{} -} - -// runEthPeer registers an eth peer into the joint eth/snap peerset, adds it to -// various subsystems and starts handling messages. -func (h *handler) runEthPeer(peer *eth.Peer, handler eth.Handler) error { - if !h.incHandlers() { - return p2p.DiscQuitting - } - defer h.decHandlers() - - // If the peer has a `snap` extension, wait for it to connect so we can have - // a uniform initialization/teardown mechanism - snap, err := h.peers.waitSnapExtension(peer) - if err != nil { - peer.Log().Error("Snapshot extension barrier failed", "err", err) - return err - } - - // Execute the Ethereum handshake - var ( - genesis = h.chain.Genesis() - head = h.chain.CurrentHeader() - hash = head.Hash() - number = head.Number.Uint64() - td = h.chain.GetTd(hash, number) - ) - forkID := forkid.NewID(h.chain.Config(), genesis, number, head.Time) - if err := peer.Handshake(h.networkID, td, hash, genesis.Hash(), forkID, h.forkFilter); err != nil { - peer.Log().Debug("Ethereum handshake failed", "err", err) - return err - } - reject := false // reserved peer slots - if h.snapSync.Load() { - if snap == nil { - // If we are running snap-sync, we want to reserve roughly half the peer - // slots for peers supporting the snap protocol. - // The logic here is; we only allow up to 5 more non-snap peers than snap-peers. - if all, snp := h.peers.len(), h.peers.snapLen(); all-snp > snp+5 { - reject = true - } - } - } - // Ignore maxPeers if this is a trusted peer - if !peer.Peer.Info().Network.Trusted { - if reject || h.peers.len() >= h.maxPeers { - return p2p.DiscTooManyPeers - } - } - peer.Log().Debug("Ethereum peer connected", "name", peer.Name()) - - // Register the peer locally - if err := h.peers.registerPeer(peer, snap); err != nil { - peer.Log().Error("Ethereum peer registration failed", "err", err) - return err - } - defer h.unregisterPeer(peer.ID()) - - p := h.peers.peer(peer.ID()) - if p == nil { - return errors.New("peer dropped during handling") - } - // Register the peer in the downloader. If the downloader considers it banned, we disconnect - if err := h.downloader.RegisterPeer(peer.ID(), peer.Version(), peer); err != nil { - peer.Log().Error("Failed to register peer in eth syncer", "err", err) - return err - } - if snap != nil { - if err := h.downloader.SnapSyncer.Register(snap); err != nil { - peer.Log().Error("Failed to register peer in snap syncer", "err", err) - return err - } - } - h.chainSync.handlePeerEvent() - - // Propagate existing transactions. new transactions appearing - // after this will be sent via broadcasts. - h.syncTransactions(peer) - - // Create a notification channel for pending requests if the peer goes down - dead := make(chan struct{}) - defer close(dead) - - // If we have any explicit peer required block hashes, request them - for number, hash := range h.requiredBlocks { - resCh := make(chan *eth.Response) - - req, err := peer.RequestHeadersByNumber(number, 1, 0, false, resCh) - if err != nil { - return err - } - go func(number uint64, hash common.Hash, req *eth.Request) { - // Ensure the request gets cancelled in case of error/drop - defer req.Close() - - timeout := time.NewTimer(syncChallengeTimeout) - defer timeout.Stop() - - select { - case res := <-resCh: - headers := ([]*types.Header)(*res.Res.(*eth.BlockHeadersRequest)) - if len(headers) == 0 { - // Required blocks are allowed to be missing if the remote - // node is not yet synced - res.Done <- nil - return - } - // Validate the header and either drop the peer or continue - if len(headers) > 1 { - res.Done <- errors.New("too many headers in required block response") - return - } - if headers[0].Number.Uint64() != number || headers[0].Hash() != hash { - peer.Log().Info("Required block mismatch, dropping peer", "number", number, "hash", headers[0].Hash(), "want", hash) - res.Done <- errors.New("required block mismatch") - return - } - peer.Log().Debug("Peer required block verified", "number", number, "hash", hash) - res.Done <- nil - case <-timeout.C: - peer.Log().Warn("Required block challenge timed out, dropping", "addr", peer.RemoteAddr(), "type", peer.Name()) - h.removePeer(peer.ID()) - } - }(number, hash, req) - } - // Handle incoming messages until the connection is torn down - return handler(peer) -} - -// runSnapExtension registers a `snap` peer into the joint eth/snap peerset and -// starts handling inbound messages. As `snap` is only a satellite protocol to -// `eth`, all subsystem registrations and lifecycle management will be done by -// the main `eth` handler to prevent strange races. -func (h *handler) runSnapExtension(peer *snap.Peer, handler snap.Handler) error { - if !h.incHandlers() { - return p2p.DiscQuitting - } - defer h.decHandlers() - - if err := h.peers.registerSnapExtension(peer); err != nil { - if metrics.Enabled { - if peer.Inbound() { - snap.IngressRegistrationErrorMeter.Mark(1) - } else { - snap.EgressRegistrationErrorMeter.Mark(1) - } - } - peer.Log().Debug("Snapshot extension registration failed", "err", err) - return err - } - return handler(peer) -} - -// removePeer requests disconnection of a peer. -func (h *handler) removePeer(id string) { - peer := h.peers.peer(id) - if peer != nil { - peer.Peer.Disconnect(p2p.DiscUselessPeer) - } -} - -// unregisterPeer removes a peer from the downloader, fetchers and main peer set. -func (h *handler) unregisterPeer(id string) { - // Create a custom logger to avoid printing the entire id - var logger log.Logger - if len(id) < 16 { - // Tests use short IDs, don't choke on them - logger = log.New("peer", id) - } else { - logger = log.New("peer", id[:8]) - } - // Abort if the peer does not exist - peer := h.peers.peer(id) - if peer == nil { - logger.Error("Ethereum peer removal failed", "err", errPeerNotRegistered) - return - } - // Remove the `eth` peer if it exists - logger.Debug("Removing Ethereum peer", "snap", peer.snapExt != nil) - - // Remove the `snap` extension if it exists - if peer.snapExt != nil { - h.downloader.SnapSyncer.Unregister(id) - } - h.downloader.UnregisterPeer(id) - h.txFetcher.Drop(id) - - if err := h.peers.unregisterPeer(id); err != nil { - logger.Error("Ethereum peer removal failed", "err", err) - } -} - -func (h *handler) Start(maxPeers int) { - h.maxPeers = maxPeers - - // broadcast and announce transactions (only new ones, not resurrected ones) - h.wg.Add(1) - h.txsCh = make(chan core.NewTxsEvent, txChanSize) - h.txsSub = h.txpool.SubscribeTransactions(h.txsCh, false) - go h.txBroadcastLoop() - - // broadcast mined blocks - h.wg.Add(1) - h.minedBlockSub = h.eventMux.Subscribe(core.NewMinedBlockEvent{}) - go h.minedBroadcastLoop() - - // start sync handlers - h.wg.Add(1) - go h.chainSync.loop() - - // start peer handler tracker - h.wg.Add(1) - go h.protoTracker() -} - -func (h *handler) Stop() { - h.txsSub.Unsubscribe() // quits txBroadcastLoop - h.minedBlockSub.Unsubscribe() // quits blockBroadcastLoop - - // Quit chainSync and txsync64. - // After this is done, no new peers will be accepted. - close(h.quitSync) - - // Disconnect existing sessions. - // This also closes the gate for any new registrations on the peer set. - // sessions which are already established but not added to h.peers yet - // will exit when they try to register. - h.peers.close() - h.wg.Wait() - - log.Info("Ethereum protocol stopped") -} - -// BroadcastBlock will either propagate a block to a subset of its peers, or -// will only announce its availability (depending what's requested). -func (h *handler) BroadcastBlock(block *types.Block, propagate bool) { - // Disable the block propagation if the chain has already entered the PoS - // stage. The block propagation is delegated to the consensus layer. - if h.merger.PoSFinalized() { - return - } - // Disable the block propagation if it's the post-merge block. - if beacon, ok := h.chain.Engine().(*beacon.Beacon); ok { - if beacon.IsPoSHeader(block.Header()) { - return - } - } - hash := block.Hash() - peers := h.peers.peersWithoutBlock(hash) - - // If propagation is requested, send to a subset of the peer - if propagate { - // Calculate the TD of the block (it's not imported yet, so block.Td is not valid) - var td *big.Int - if parent := h.chain.GetBlock(block.ParentHash(), block.NumberU64()-1); parent != nil { - td = new(big.Int).Add(block.Difficulty(), h.chain.GetTd(block.ParentHash(), block.NumberU64()-1)) - } else { - log.Error("Propagating dangling block", "number", block.Number(), "hash", hash) - return - } - // Send the block to a subset of our peers - transfer := peers[:int(math.Sqrt(float64(len(peers))))] - for _, peer := range transfer { - peer.AsyncSendNewBlock(block, td) - } - log.Trace("Propagated block", "hash", hash, "recipients", len(transfer), "duration", common.PrettyDuration(time.Since(block.ReceivedAt))) - return - } - // Otherwise if the block is indeed in out own chain, announce it - if h.chain.HasBlock(hash, block.NumberU64()) { - for _, peer := range peers { - peer.AsyncSendNewBlockHash(block) - } - log.Trace("Announced block", "hash", hash, "recipients", len(peers), "duration", common.PrettyDuration(time.Since(block.ReceivedAt))) - } -} - -// BroadcastTransactions will propagate a batch of transactions -// - To a square root of all peers for non-blob transactions -// - And, separately, as announcements to all peers which are not known to -// already have the given transaction. -func (h *handler) BroadcastTransactions(txs types.Transactions) { - var ( - blobTxs int // Number of blob transactions to announce only - largeTxs int // Number of large transactions to announce only - - directCount int // Number of transactions sent directly to peers (duplicates included) - directPeers int // Number of peers that were sent transactions directly - annCount int // Number of transactions announced across all peers (duplicates included) - annPeers int // Number of peers announced about transactions - - txset = make(map[*ethPeer][]common.Hash) // Set peer->hash to transfer directly - annos = make(map[*ethPeer][]common.Hash) // Set peer->hash to announce - ) - // Broadcast transactions to a batch of peers not knowing about it - for _, tx := range txs { - peers := h.peers.peersWithoutTransaction(tx.Hash()) - - var numDirect int - switch { - case tx.Type() == types.BlobTxType: - blobTxs++ - case tx.Size() > txMaxBroadcastSize: - largeTxs++ - default: - numDirect = int(math.Sqrt(float64(len(peers)))) - } - // Send the tx unconditionally to a subset of our peers - for _, peer := range peers[:numDirect] { - txset[peer] = append(txset[peer], tx.Hash()) - } - // For the remaining peers, send announcement only - for _, peer := range peers[numDirect:] { - annos[peer] = append(annos[peer], tx.Hash()) - } - } - for peer, hashes := range txset { - directPeers++ - directCount += len(hashes) - peer.AsyncSendTransactions(hashes) - } - for peer, hashes := range annos { - annPeers++ - annCount += len(hashes) - peer.AsyncSendPooledTransactionHashes(hashes) - } - log.Debug("Distributed transactions", "plaintxs", len(txs)-blobTxs-largeTxs, "blobtxs", blobTxs, "largetxs", largeTxs, - "bcastpeers", directPeers, "bcastcount", directCount, "annpeers", annPeers, "anncount", annCount) -} - -// minedBroadcastLoop sends mined blocks to connected peers. -func (h *handler) minedBroadcastLoop() { - defer h.wg.Done() - - for obj := range h.minedBlockSub.Chan() { - if ev, ok := obj.Data.(core.NewMinedBlockEvent); ok { - h.BroadcastBlock(ev.Block, true) // First propagate block to peers - h.BroadcastBlock(ev.Block, false) // Only then announce to the rest - } - } -} - -// txBroadcastLoop announces new transactions to connected peers. -func (h *handler) txBroadcastLoop() { - defer h.wg.Done() - for { - select { - case event := <-h.txsCh: - h.BroadcastTransactions(event.Txs) - case <-h.txsSub.Err(): - return - } - } -} - -// enableSyncedFeatures enables the post-sync functionalities when the initial -// sync is finished. -func (h *handler) enableSyncedFeatures() { - // Mark the local node as synced. - h.synced.Store(true) - - // If we were running snap sync and it finished, disable doing another - // round on next sync cycle - if h.snapSync.Load() { - log.Info("Snap sync complete, auto disabling") - h.snapSync.Store(false) - } - if h.chain.TrieDB().Scheme() == rawdb.PathScheme { - h.chain.TrieDB().SetBufferSize(pathdb.DefaultBufferSize) - } -} diff --git a/eth/handler_eth_test.go b/eth/handler_eth_test.go deleted file mode 100644 index bb342acc18..0000000000 --- a/eth/handler_eth_test.go +++ /dev/null @@ -1,608 +0,0 @@ -// Copyright 2020 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 . - -package eth - -import ( - "fmt" - "math/big" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/consensus" - "github.com/ethereum/go-ethereum/consensus/ethash" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/forkid" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/vm" - "github.com/ethereum/go-ethereum/eth/downloader" - "github.com/ethereum/go-ethereum/eth/protocols/eth" - "github.com/ethereum/go-ethereum/event" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/params" -) - -// testEthHandler is a mock event handler to listen for inbound network requests -// on the `eth` protocol and convert them into a more easily testable form. -type testEthHandler struct { - blockBroadcasts event.Feed - txAnnounces event.Feed - txBroadcasts event.Feed -} - -func (h *testEthHandler) Chain() *core.BlockChain { panic("no backing chain") } -func (h *testEthHandler) TxPool() eth.TxPool { panic("no backing tx pool") } -func (h *testEthHandler) AcceptTxs() bool { return true } -func (h *testEthHandler) RunPeer(*eth.Peer, eth.Handler) error { panic("not used in tests") } -func (h *testEthHandler) PeerInfo(enode.ID) interface{} { panic("not used in tests") } - -func (h *testEthHandler) Handle(peer *eth.Peer, packet eth.Packet) error { - switch packet := packet.(type) { - case *eth.NewBlockPacket: - h.blockBroadcasts.Send(packet.Block) - return nil - - case *eth.NewPooledTransactionHashesPacket67: - h.txAnnounces.Send(([]common.Hash)(*packet)) - return nil - - case *eth.NewPooledTransactionHashesPacket68: - h.txAnnounces.Send(packet.Hashes) - return nil - - case *eth.TransactionsPacket: - h.txBroadcasts.Send(([]*types.Transaction)(*packet)) - return nil - - case *eth.PooledTransactionsResponse: - h.txBroadcasts.Send(([]*types.Transaction)(*packet)) - return nil - - default: - panic(fmt.Sprintf("unexpected eth packet type in tests: %T", packet)) - } -} - -// Tests that peers are correctly accepted (or rejected) based on the advertised -// fork IDs in the protocol handshake. -func TestForkIDSplit67(t *testing.T) { testForkIDSplit(t, eth.ETH67) } -func TestForkIDSplit68(t *testing.T) { testForkIDSplit(t, eth.ETH68) } - -func testForkIDSplit(t *testing.T, protocol uint) { - t.Parallel() - - var ( - engine = ethash.NewFaker() - - configNoFork = ¶ms.ChainConfig{HomesteadBlock: big.NewInt(1)} - configProFork = ¶ms.ChainConfig{ - HomesteadBlock: big.NewInt(1), - EIP150Block: big.NewInt(2), - EIP155Block: big.NewInt(2), - EIP158Block: big.NewInt(2), - ByzantiumBlock: big.NewInt(3), - } - dbNoFork = rawdb.NewMemoryDatabase() - dbProFork = rawdb.NewMemoryDatabase() - - gspecNoFork = &core.Genesis{Config: configNoFork} - gspecProFork = &core.Genesis{Config: configProFork} - - chainNoFork, _ = core.NewBlockChain(dbNoFork, nil, gspecNoFork, nil, engine, vm.Config{}, nil, nil) - chainProFork, _ = core.NewBlockChain(dbProFork, nil, gspecProFork, nil, engine, vm.Config{}, nil, nil) - - _, blocksNoFork, _ = core.GenerateChainWithGenesis(gspecNoFork, engine, 2, nil) - _, blocksProFork, _ = core.GenerateChainWithGenesis(gspecProFork, engine, 2, nil) - - ethNoFork, _ = newHandler(&handlerConfig{ - Database: dbNoFork, - Chain: chainNoFork, - TxPool: newTestTxPool(), - Merger: consensus.NewMerger(rawdb.NewMemoryDatabase()), - Network: 1, - Sync: downloader.FullSync, - BloomCache: 1, - }) - ethProFork, _ = newHandler(&handlerConfig{ - Database: dbProFork, - Chain: chainProFork, - TxPool: newTestTxPool(), - Merger: consensus.NewMerger(rawdb.NewMemoryDatabase()), - Network: 1, - Sync: downloader.FullSync, - BloomCache: 1, - }) - ) - ethNoFork.Start(1000) - ethProFork.Start(1000) - - // Clean up everything after ourselves - defer chainNoFork.Stop() - defer chainProFork.Stop() - - defer ethNoFork.Stop() - defer ethProFork.Stop() - - // Both nodes should allow the other to connect (same genesis, next fork is the same) - p2pNoFork, p2pProFork := p2p.MsgPipe() - defer p2pNoFork.Close() - defer p2pProFork.Close() - - peerNoFork := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{1}, "", nil, p2pNoFork), p2pNoFork, nil) - peerProFork := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{2}, "", nil, p2pProFork), p2pProFork, nil) - defer peerNoFork.Close() - defer peerProFork.Close() - - errc := make(chan error, 2) - go func(errc chan error) { - errc <- ethNoFork.runEthPeer(peerProFork, func(peer *eth.Peer) error { return nil }) - }(errc) - go func(errc chan error) { - errc <- ethProFork.runEthPeer(peerNoFork, func(peer *eth.Peer) error { return nil }) - }(errc) - - for i := 0; i < 2; i++ { - select { - case err := <-errc: - if err != nil { - t.Fatalf("frontier nofork <-> profork failed: %v", err) - } - case <-time.After(250 * time.Millisecond): - t.Fatalf("frontier nofork <-> profork handler timeout") - } - } - // Progress into Homestead. Fork's match, so we don't care what the future holds - chainNoFork.InsertChain(blocksNoFork[:1]) - chainProFork.InsertChain(blocksProFork[:1]) - - p2pNoFork, p2pProFork = p2p.MsgPipe() - defer p2pNoFork.Close() - defer p2pProFork.Close() - - peerNoFork = eth.NewPeer(protocol, p2p.NewPeer(enode.ID{1}, "", nil), p2pNoFork, nil) - peerProFork = eth.NewPeer(protocol, p2p.NewPeer(enode.ID{2}, "", nil), p2pProFork, nil) - defer peerNoFork.Close() - defer peerProFork.Close() - - errc = make(chan error, 2) - go func(errc chan error) { - errc <- ethNoFork.runEthPeer(peerProFork, func(peer *eth.Peer) error { return nil }) - }(errc) - go func(errc chan error) { - errc <- ethProFork.runEthPeer(peerNoFork, func(peer *eth.Peer) error { return nil }) - }(errc) - - for i := 0; i < 2; i++ { - select { - case err := <-errc: - if err != nil { - t.Fatalf("homestead nofork <-> profork failed: %v", err) - } - case <-time.After(250 * time.Millisecond): - t.Fatalf("homestead nofork <-> profork handler timeout") - } - } - // Progress into Spurious. Forks mismatch, signalling differing chains, reject - chainNoFork.InsertChain(blocksNoFork[1:2]) - chainProFork.InsertChain(blocksProFork[1:2]) - - p2pNoFork, p2pProFork = p2p.MsgPipe() - defer p2pNoFork.Close() - defer p2pProFork.Close() - - peerNoFork = eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{1}, "", nil, p2pNoFork), p2pNoFork, nil) - peerProFork = eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{2}, "", nil, p2pProFork), p2pProFork, nil) - defer peerNoFork.Close() - defer peerProFork.Close() - - errc = make(chan error, 2) - go func(errc chan error) { - errc <- ethNoFork.runEthPeer(peerProFork, func(peer *eth.Peer) error { return nil }) - }(errc) - go func(errc chan error) { - errc <- ethProFork.runEthPeer(peerNoFork, func(peer *eth.Peer) error { return nil }) - }(errc) - - var successes int - for i := 0; i < 2; i++ { - select { - case err := <-errc: - if err == nil { - successes++ - if successes == 2 { // Only one side disconnects - t.Fatalf("fork ID rejection didn't happen") - } - } - case <-time.After(250 * time.Millisecond): - t.Fatalf("split peers not rejected") - } - } -} - -// Tests that received transactions are added to the local pool. -func TestRecvTransactions67(t *testing.T) { testRecvTransactions(t, eth.ETH67) } -func TestRecvTransactions68(t *testing.T) { testRecvTransactions(t, eth.ETH68) } - -func testRecvTransactions(t *testing.T, protocol uint) { - t.Parallel() - - // Create a message handler, configure it to accept transactions and watch them - handler := newTestHandler() - defer handler.close() - - handler.handler.synced.Store(true) // mark synced to accept transactions - - txs := make(chan core.NewTxsEvent) - sub := handler.txpool.SubscribeTransactions(txs, false) - defer sub.Unsubscribe() - - // Create a source peer to send messages through and a sink handler to receive them - p2pSrc, p2pSink := p2p.MsgPipe() - defer p2pSrc.Close() - defer p2pSink.Close() - - src := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{1}, "", nil, p2pSrc), p2pSrc, handler.txpool) - sink := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{2}, "", nil, p2pSink), p2pSink, handler.txpool) - defer src.Close() - defer sink.Close() - - go handler.handler.runEthPeer(sink, func(peer *eth.Peer) error { - return eth.Handle((*ethHandler)(handler.handler), peer) - }) - // Run the handshake locally to avoid spinning up a source handler - var ( - genesis = handler.chain.Genesis() - head = handler.chain.CurrentBlock() - td = handler.chain.GetTd(head.Hash(), head.Number.Uint64()) - ) - if err := src.Handshake(1, td, head.Hash(), genesis.Hash(), forkid.NewIDWithChain(handler.chain), forkid.NewFilter(handler.chain)); err != nil { - t.Fatalf("failed to run protocol handshake") - } - // Send the transaction to the sink and verify that it's added to the tx pool - tx := types.NewTransaction(0, common.Address{}, big.NewInt(0), 100000, big.NewInt(0), nil) - tx, _ = types.SignTx(tx, types.HomesteadSigner{}, testKey) - - if err := src.SendTransactions([]*types.Transaction{tx}); err != nil { - t.Fatalf("failed to send transaction: %v", err) - } - select { - case event := <-txs: - if len(event.Txs) != 1 { - t.Errorf("wrong number of added transactions: got %d, want 1", len(event.Txs)) - } else if event.Txs[0].Hash() != tx.Hash() { - t.Errorf("added wrong tx hash: got %v, want %v", event.Txs[0].Hash(), tx.Hash()) - } - case <-time.After(2 * time.Second): - t.Errorf("no NewTxsEvent received within 2 seconds") - } -} - -// This test checks that pending transactions are sent. -func TestSendTransactions67(t *testing.T) { testSendTransactions(t, eth.ETH67) } -func TestSendTransactions68(t *testing.T) { testSendTransactions(t, eth.ETH68) } - -func testSendTransactions(t *testing.T, protocol uint) { - t.Parallel() - - // Create a message handler and fill the pool with big transactions - handler := newTestHandler() - defer handler.close() - - insert := make([]*types.Transaction, 100) - for nonce := range insert { - tx := types.NewTransaction(uint64(nonce), common.Address{}, big.NewInt(0), 100000, big.NewInt(0), make([]byte, 10240)) - tx, _ = types.SignTx(tx, types.HomesteadSigner{}, testKey) - insert[nonce] = tx - } - go handler.txpool.Add(insert, false, false) // Need goroutine to not block on feed - time.Sleep(250 * time.Millisecond) // Wait until tx events get out of the system (can't use events, tx broadcaster races with peer join) - - // Create a source handler to send messages through and a sink peer to receive them - p2pSrc, p2pSink := p2p.MsgPipe() - defer p2pSrc.Close() - defer p2pSink.Close() - - src := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{1}, "", nil, p2pSrc), p2pSrc, handler.txpool) - sink := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{2}, "", nil, p2pSink), p2pSink, handler.txpool) - defer src.Close() - defer sink.Close() - - go handler.handler.runEthPeer(src, func(peer *eth.Peer) error { - return eth.Handle((*ethHandler)(handler.handler), peer) - }) - // Run the handshake locally to avoid spinning up a source handler - var ( - genesis = handler.chain.Genesis() - head = handler.chain.CurrentBlock() - td = handler.chain.GetTd(head.Hash(), head.Number.Uint64()) - ) - if err := sink.Handshake(1, td, head.Hash(), genesis.Hash(), forkid.NewIDWithChain(handler.chain), forkid.NewFilter(handler.chain)); err != nil { - t.Fatalf("failed to run protocol handshake") - } - // After the handshake completes, the source handler should stream the sink - // the transactions, subscribe to all inbound network events - backend := new(testEthHandler) - - anns := make(chan []common.Hash) - annSub := backend.txAnnounces.Subscribe(anns) - defer annSub.Unsubscribe() - - bcasts := make(chan []*types.Transaction) - bcastSub := backend.txBroadcasts.Subscribe(bcasts) - defer bcastSub.Unsubscribe() - - go eth.Handle(backend, sink) - - // Make sure we get all the transactions on the correct channels - seen := make(map[common.Hash]struct{}) - for len(seen) < len(insert) { - switch protocol { - case 67, 68: - select { - case hashes := <-anns: - for _, hash := range hashes { - if _, ok := seen[hash]; ok { - t.Errorf("duplicate transaction announced: %x", hash) - } - seen[hash] = struct{}{} - } - case <-bcasts: - t.Errorf("initial tx broadcast received on post eth/66") - } - - default: - panic("unsupported protocol, please extend test") - } - } - for _, tx := range insert { - if _, ok := seen[tx.Hash()]; !ok { - t.Errorf("missing transaction: %x", tx.Hash()) - } - } -} - -// Tests that transactions get propagated to all attached peers, either via direct -// broadcasts or via announcements/retrievals. -func TestTransactionPropagation67(t *testing.T) { testTransactionPropagation(t, eth.ETH67) } -func TestTransactionPropagation68(t *testing.T) { testTransactionPropagation(t, eth.ETH68) } - -func testTransactionPropagation(t *testing.T, protocol uint) { - t.Parallel() - - // Create a source handler to send transactions from and a number of sinks - // to receive them. We need multiple sinks since a one-to-one peering would - // broadcast all transactions without announcement. - source := newTestHandler() - source.handler.snapSync.Store(false) // Avoid requiring snap, otherwise some will be dropped below - defer source.close() - - sinks := make([]*testHandler, 10) - for i := 0; i < len(sinks); i++ { - sinks[i] = newTestHandler() - defer sinks[i].close() - - sinks[i].handler.synced.Store(true) // mark synced to accept transactions - } - // Interconnect all the sink handlers with the source handler - for i, sink := range sinks { - sink := sink // Closure for gorotuine below - - sourcePipe, sinkPipe := p2p.MsgPipe() - defer sourcePipe.Close() - defer sinkPipe.Close() - - sourcePeer := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{byte(i + 1)}, "", nil, sourcePipe), sourcePipe, source.txpool) - sinkPeer := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{0}, "", nil, sinkPipe), sinkPipe, sink.txpool) - defer sourcePeer.Close() - defer sinkPeer.Close() - - go source.handler.runEthPeer(sourcePeer, func(peer *eth.Peer) error { - return eth.Handle((*ethHandler)(source.handler), peer) - }) - go sink.handler.runEthPeer(sinkPeer, func(peer *eth.Peer) error { - return eth.Handle((*ethHandler)(sink.handler), peer) - }) - } - // Subscribe to all the transaction pools - txChs := make([]chan core.NewTxsEvent, len(sinks)) - for i := 0; i < len(sinks); i++ { - txChs[i] = make(chan core.NewTxsEvent, 1024) - - sub := sinks[i].txpool.SubscribeTransactions(txChs[i], false) - defer sub.Unsubscribe() - } - // Fill the source pool with transactions and wait for them at the sinks - txs := make([]*types.Transaction, 1024) - for nonce := range txs { - tx := types.NewTransaction(uint64(nonce), common.Address{}, big.NewInt(0), 100000, big.NewInt(0), nil) - tx, _ = types.SignTx(tx, types.HomesteadSigner{}, testKey) - txs[nonce] = tx - } - source.txpool.Add(txs, false, false) - - // Iterate through all the sinks and ensure they all got the transactions - for i := range sinks { - for arrived, timeout := 0, false; arrived < len(txs) && !timeout; { - select { - case event := <-txChs[i]: - arrived += len(event.Txs) - case <-time.After(2 * time.Second): - t.Errorf("sink %d: transaction propagation timed out: have %d, want %d", i, arrived, len(txs)) - timeout = true - } - } - } -} - -// Tests that blocks are broadcast to a sqrt number of peers only. -func TestBroadcastBlock1Peer(t *testing.T) { testBroadcastBlock(t, 1, 1) } -func TestBroadcastBlock2Peers(t *testing.T) { testBroadcastBlock(t, 2, 1) } -func TestBroadcastBlock3Peers(t *testing.T) { testBroadcastBlock(t, 3, 1) } -func TestBroadcastBlock4Peers(t *testing.T) { testBroadcastBlock(t, 4, 2) } -func TestBroadcastBlock5Peers(t *testing.T) { testBroadcastBlock(t, 5, 2) } -func TestBroadcastBlock8Peers(t *testing.T) { testBroadcastBlock(t, 9, 3) } -func TestBroadcastBlock12Peers(t *testing.T) { testBroadcastBlock(t, 12, 3) } -func TestBroadcastBlock16Peers(t *testing.T) { testBroadcastBlock(t, 16, 4) } -func TestBroadcastBloc26Peers(t *testing.T) { testBroadcastBlock(t, 26, 5) } -func TestBroadcastBlock100Peers(t *testing.T) { testBroadcastBlock(t, 100, 10) } - -func testBroadcastBlock(t *testing.T, peers, bcasts int) { - t.Parallel() - - // Create a source handler to broadcast blocks from and a number of sinks - // to receive them. - source := newTestHandlerWithBlocks(1) - defer source.close() - - sinks := make([]*testEthHandler, peers) - for i := 0; i < len(sinks); i++ { - sinks[i] = new(testEthHandler) - } - // Interconnect all the sink handlers with the source handler - var ( - genesis = source.chain.Genesis() - td = source.chain.GetTd(genesis.Hash(), genesis.NumberU64()) - ) - for i, sink := range sinks { - sink := sink // Closure for gorotuine below - - sourcePipe, sinkPipe := p2p.MsgPipe() - defer sourcePipe.Close() - defer sinkPipe.Close() - - sourcePeer := eth.NewPeer(eth.ETH67, p2p.NewPeerPipe(enode.ID{byte(i)}, "", nil, sourcePipe), sourcePipe, nil) - sinkPeer := eth.NewPeer(eth.ETH67, p2p.NewPeerPipe(enode.ID{0}, "", nil, sinkPipe), sinkPipe, nil) - defer sourcePeer.Close() - defer sinkPeer.Close() - - go source.handler.runEthPeer(sourcePeer, func(peer *eth.Peer) error { - return eth.Handle((*ethHandler)(source.handler), peer) - }) - if err := sinkPeer.Handshake(1, td, genesis.Hash(), genesis.Hash(), forkid.NewIDWithChain(source.chain), forkid.NewFilter(source.chain)); err != nil { - t.Fatalf("failed to run protocol handshake") - } - go eth.Handle(sink, sinkPeer) - } - // Subscribe to all the transaction pools - blockChs := make([]chan *types.Block, len(sinks)) - for i := 0; i < len(sinks); i++ { - blockChs[i] = make(chan *types.Block, 1) - defer close(blockChs[i]) - - sub := sinks[i].blockBroadcasts.Subscribe(blockChs[i]) - defer sub.Unsubscribe() - } - // Initiate a block propagation across the peers - time.Sleep(100 * time.Millisecond) - header := source.chain.CurrentBlock() - source.handler.BroadcastBlock(source.chain.GetBlock(header.Hash(), header.Number.Uint64()), true) - - // Iterate through all the sinks and ensure the correct number got the block - done := make(chan struct{}, peers) - for _, ch := range blockChs { - ch := ch - go func() { - <-ch - done <- struct{}{} - }() - } - var received int - for { - select { - case <-done: - received++ - - case <-time.After(100 * time.Millisecond): - if received != bcasts { - t.Errorf("broadcast count mismatch: have %d, want %d", received, bcasts) - } - return - } - } -} - -// Tests that a propagated malformed block (uncles or transactions don't match -// with the hashes in the header) gets discarded and not broadcast forward. -func TestBroadcastMalformedBlock67(t *testing.T) { testBroadcastMalformedBlock(t, eth.ETH67) } -func TestBroadcastMalformedBlock68(t *testing.T) { testBroadcastMalformedBlock(t, eth.ETH68) } - -func testBroadcastMalformedBlock(t *testing.T, protocol uint) { - t.Parallel() - - // Create a source handler to broadcast blocks from and a number of sinks - // to receive them. - source := newTestHandlerWithBlocks(1) - defer source.close() - - // Create a source handler to send messages through and a sink peer to receive them - p2pSrc, p2pSink := p2p.MsgPipe() - defer p2pSrc.Close() - defer p2pSink.Close() - - src := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{1}, "", nil, p2pSrc), p2pSrc, source.txpool) - sink := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{2}, "", nil, p2pSink), p2pSink, source.txpool) - defer src.Close() - defer sink.Close() - - go source.handler.runEthPeer(src, func(peer *eth.Peer) error { - return eth.Handle((*ethHandler)(source.handler), peer) - }) - // Run the handshake locally to avoid spinning up a sink handler - var ( - genesis = source.chain.Genesis() - td = source.chain.GetTd(genesis.Hash(), genesis.NumberU64()) - ) - if err := sink.Handshake(1, td, genesis.Hash(), genesis.Hash(), forkid.NewIDWithChain(source.chain), forkid.NewFilter(source.chain)); err != nil { - t.Fatalf("failed to run protocol handshake") - } - // After the handshake completes, the source handler should stream the sink - // the blocks, subscribe to inbound network events - backend := new(testEthHandler) - - blocks := make(chan *types.Block, 1) - sub := backend.blockBroadcasts.Subscribe(blocks) - defer sub.Unsubscribe() - - go eth.Handle(backend, sink) - - // Create various combinations of malformed blocks - head := source.chain.CurrentBlock() - block := source.chain.GetBlock(head.Hash(), head.Number.Uint64()) - - malformedUncles := head - malformedUncles.UncleHash[0]++ - malformedTransactions := head - malformedTransactions.TxHash[0]++ - malformedEverything := head - malformedEverything.UncleHash[0]++ - malformedEverything.TxHash[0]++ - - // Try to broadcast all malformations and ensure they all get discarded - for _, header := range []*types.Header{malformedUncles, malformedTransactions, malformedEverything} { - block := types.NewBlockWithHeader(header).WithBody(block.Transactions(), block.Uncles()) - if err := src.SendNewBlock(block, big.NewInt(131136)); err != nil { - t.Fatalf("failed to broadcast block: %v", err) - } - select { - case <-blocks: - t.Fatalf("malformed block forwarded") - case <-time.After(100 * time.Millisecond): - } - } -} diff --git a/eth/handler_snap.go b/eth/handler_snap.go deleted file mode 100644 index 767416ffd6..0000000000 --- a/eth/handler_snap.go +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2020 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 . - -package eth - -import ( - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/eth/protocols/snap" - "github.com/ethereum/go-ethereum/p2p/enode" -) - -// snapHandler implements the snap.Backend interface to handle the various network -// packets that are sent as replies or broadcasts. -type snapHandler handler - -func (h *snapHandler) Chain() *core.BlockChain { return h.chain } - -// RunPeer is invoked when a peer joins on the `snap` protocol. -func (h *snapHandler) RunPeer(peer *snap.Peer, hand snap.Handler) error { - return (*handler)(h).runSnapExtension(peer, hand) -} - -// PeerInfo retrieves all known `snap` information about a peer. -func (h *snapHandler) PeerInfo(id enode.ID) interface{} { - if p := h.peers.peer(id.String()); p != nil { - if p.snapExt != nil { - return p.snapExt.info() - } - } - return nil -} - -// Handle is invoked from a peer's message handler when it receives a new remote -// message that the handler couldn't consume and serve itself. -func (h *snapHandler) Handle(peer *snap.Peer, packet snap.Packet) error { - return h.downloader.DeliverSnapPacket(peer, packet) -} diff --git a/eth/handler_test.go b/eth/handler_test.go deleted file mode 100644 index 6d6132ee4c..0000000000 --- a/eth/handler_test.go +++ /dev/null @@ -1,185 +0,0 @@ -// Copyright 2015 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 . - -package eth - -import ( - "math/big" - "sort" - "sync" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/consensus" - "github.com/ethereum/go-ethereum/consensus/ethash" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/txpool" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/vm" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/eth/downloader" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/event" - "github.com/ethereum/go-ethereum/params" -) - -var ( - // testKey is a private key to use for funding a tester account. - testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") - - // testAddr is the Ethereum address of the tester account. - testAddr = crypto.PubkeyToAddress(testKey.PublicKey) -) - -// testTxPool is a mock transaction pool that blindly accepts all transactions. -// Its goal is to get around setting up a valid statedb for the balance and nonce -// checks. -type testTxPool struct { - pool map[common.Hash]*types.Transaction // Hash map of collected transactions - - txFeed event.Feed // Notification feed to allow waiting for inclusion - lock sync.RWMutex // Protects the transaction pool -} - -// newTestTxPool creates a mock transaction pool. -func newTestTxPool() *testTxPool { - return &testTxPool{ - pool: make(map[common.Hash]*types.Transaction), - } -} - -// Has returns an indicator whether txpool has a transaction -// cached with the given hash. -func (p *testTxPool) Has(hash common.Hash) bool { - p.lock.Lock() - defer p.lock.Unlock() - - return p.pool[hash] != nil -} - -// Get retrieves the transaction from local txpool with given -// tx hash. -func (p *testTxPool) Get(hash common.Hash) *types.Transaction { - p.lock.Lock() - defer p.lock.Unlock() - return p.pool[hash] -} - -// Add appends a batch of transactions to the pool, and notifies any -// listeners if the addition channel is non nil -func (p *testTxPool) Add(txs []*types.Transaction, local bool, sync bool) []error { - p.lock.Lock() - defer p.lock.Unlock() - - for _, tx := range txs { - p.pool[tx.Hash()] = tx - } - p.txFeed.Send(core.NewTxsEvent{Txs: txs}) - return make([]error, len(txs)) -} - -// Pending returns all the transactions known to the pool -func (p *testTxPool) Pending(enforceTips bool) map[common.Address][]*txpool.LazyTransaction { - p.lock.RLock() - defer p.lock.RUnlock() - - batches := make(map[common.Address][]*types.Transaction) - for _, tx := range p.pool { - from, _ := types.Sender(types.HomesteadSigner{}, tx) - batches[from] = append(batches[from], tx) - } - for _, batch := range batches { - sort.Sort(types.TxByNonce(batch)) - } - pending := make(map[common.Address][]*txpool.LazyTransaction) - for addr, batch := range batches { - for _, tx := range batch { - pending[addr] = append(pending[addr], &txpool.LazyTransaction{ - Hash: tx.Hash(), - Tx: tx, - Time: tx.Time(), - GasFeeCap: tx.GasFeeCap(), - GasTipCap: tx.GasTipCap(), - Gas: tx.Gas(), - BlobGas: tx.BlobGas(), - }) - } - } - return pending -} - -// SubscribeTransactions should return an event subscription of NewTxsEvent and -// send events to the given channel. -func (p *testTxPool) SubscribeTransactions(ch chan<- core.NewTxsEvent, reorgs bool) event.Subscription { - return p.txFeed.Subscribe(ch) -} - -// testHandler is a live implementation of the Ethereum protocol handler, just -// preinitialized with some sane testing defaults and the transaction pool mocked -// out. -type testHandler struct { - db ethdb.Database - chain *core.BlockChain - txpool *testTxPool - handler *handler -} - -// newTestHandler creates a new handler for testing purposes with no blocks. -func newTestHandler() *testHandler { - return newTestHandlerWithBlocks(0) -} - -// newTestHandlerWithBlocks creates a new handler for testing purposes, with a -// given number of initial blocks. -func newTestHandlerWithBlocks(blocks int) *testHandler { - // Create a database pre-initialize with a genesis block - db := rawdb.NewMemoryDatabase() - gspec := &core.Genesis{ - Config: params.TestChainConfig, - Alloc: core.GenesisAlloc{testAddr: {Balance: big.NewInt(1000000)}}, - } - chain, _ := core.NewBlockChain(db, nil, gspec, nil, ethash.NewFaker(), vm.Config{}, nil, nil) - - _, bs, _ := core.GenerateChainWithGenesis(gspec, ethash.NewFaker(), blocks, nil) - if _, err := chain.InsertChain(bs); err != nil { - panic(err) - } - txpool := newTestTxPool() - - handler, _ := newHandler(&handlerConfig{ - Database: db, - Chain: chain, - TxPool: txpool, - Merger: consensus.NewMerger(rawdb.NewMemoryDatabase()), - Network: 1, - Sync: downloader.SnapSync, - BloomCache: 1, - }) - handler.Start(1000) - - return &testHandler{ - db: db, - chain: chain, - txpool: txpool, - handler: handler, - } -} - -// close tears down the handler and all its internal constructs. -func (b *testHandler) close() { - b.handler.Stop() - b.chain.Stop() -} diff --git a/eth/peer.go b/eth/peer.go deleted file mode 100644 index 7618777716..0000000000 --- a/eth/peer.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2015 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 . - -package eth - -import ( - "github.com/ethereum/go-ethereum/eth/protocols/eth" - "github.com/ethereum/go-ethereum/eth/protocols/snap" -) - -// ethPeerInfo represents a short summary of the `eth` sub-protocol metadata known -// about a connected peer. -type ethPeerInfo struct { - Version uint `json:"version"` // Ethereum protocol version negotiated -} - -// ethPeer is a wrapper around eth.Peer to maintain a few extra metadata. -type ethPeer struct { - *eth.Peer - snapExt *snapPeer // Satellite `snap` connection -} - -// info gathers and returns some `eth` protocol metadata known about a peer. -func (p *ethPeer) info() *ethPeerInfo { - return ðPeerInfo{ - Version: p.Version(), - } -} - -// snapPeerInfo represents a short summary of the `snap` sub-protocol metadata known -// about a connected peer. -type snapPeerInfo struct { - Version uint `json:"version"` // Snapshot protocol version negotiated -} - -// snapPeer is a wrapper around snap.Peer to maintain a few extra metadata. -type snapPeer struct { - *snap.Peer -} - -// info gathers and returns some `snap` protocol metadata known about a peer. -func (p *snapPeer) info() *snapPeerInfo { - return &snapPeerInfo{ - Version: p.Version(), - } -} diff --git a/eth/peerset.go b/eth/peerset.go deleted file mode 100644 index b27d3964a1..0000000000 --- a/eth/peerset.go +++ /dev/null @@ -1,260 +0,0 @@ -// Copyright 2020 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 . - -package eth - -import ( - "errors" - "fmt" - "math/big" - "sync" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/eth/protocols/eth" - "github.com/ethereum/go-ethereum/eth/protocols/snap" - "github.com/ethereum/go-ethereum/p2p" -) - -var ( - // errPeerSetClosed is returned if a peer is attempted to be added or removed - // from the peer set after it has been terminated. - errPeerSetClosed = errors.New("peerset closed") - - // errPeerAlreadyRegistered is returned if a peer is attempted to be added - // to the peer set, but one with the same id already exists. - errPeerAlreadyRegistered = errors.New("peer already registered") - - // errPeerNotRegistered is returned if a peer is attempted to be removed from - // a peer set, but no peer with the given id exists. - errPeerNotRegistered = errors.New("peer not registered") - - // errSnapWithoutEth is returned if a peer attempts to connect only on the - // snap protocol without advertising the eth main protocol. - errSnapWithoutEth = errors.New("peer connected on snap without compatible eth support") -) - -// peerSet represents the collection of active peers currently participating in -// the `eth` protocol, with or without the `snap` extension. -type peerSet struct { - peers map[string]*ethPeer // Peers connected on the `eth` protocol - snapPeers int // Number of `snap` compatible peers for connection prioritization - - snapWait map[string]chan *snap.Peer // Peers connected on `eth` waiting for their snap extension - snapPend map[string]*snap.Peer // Peers connected on the `snap` protocol, but not yet on `eth` - - lock sync.RWMutex - closed bool -} - -// newPeerSet creates a new peer set to track the active participants. -func newPeerSet() *peerSet { - return &peerSet{ - peers: make(map[string]*ethPeer), - snapWait: make(map[string]chan *snap.Peer), - snapPend: make(map[string]*snap.Peer), - } -} - -// registerSnapExtension unblocks an already connected `eth` peer waiting for its -// `snap` extension, or if no such peer exists, tracks the extension for the time -// being until the `eth` main protocol starts looking for it. -func (ps *peerSet) registerSnapExtension(peer *snap.Peer) error { - // Reject the peer if it advertises `snap` without `eth` as `snap` is only a - // satellite protocol meaningful with the chain selection of `eth` - if !peer.RunningCap(eth.ProtocolName, eth.ProtocolVersions) { - return fmt.Errorf("%w: have %v", errSnapWithoutEth, peer.Caps()) - } - // Ensure nobody can double connect - ps.lock.Lock() - defer ps.lock.Unlock() - - id := peer.ID() - if _, ok := ps.peers[id]; ok { - return errPeerAlreadyRegistered // avoid connections with the same id as existing ones - } - if _, ok := ps.snapPend[id]; ok { - return errPeerAlreadyRegistered // avoid connections with the same id as pending ones - } - // Inject the peer into an `eth` counterpart is available, otherwise save for later - if wait, ok := ps.snapWait[id]; ok { - delete(ps.snapWait, id) - wait <- peer - return nil - } - ps.snapPend[id] = peer - return nil -} - -// waitExtensions blocks until all satellite protocols are connected and tracked -// by the peerset. -func (ps *peerSet) waitSnapExtension(peer *eth.Peer) (*snap.Peer, error) { - // If the peer does not support a compatible `snap`, don't wait - if !peer.RunningCap(snap.ProtocolName, snap.ProtocolVersions) { - return nil, nil - } - // Ensure nobody can double connect - ps.lock.Lock() - - id := peer.ID() - if _, ok := ps.peers[id]; ok { - ps.lock.Unlock() - return nil, errPeerAlreadyRegistered // avoid connections with the same id as existing ones - } - if _, ok := ps.snapWait[id]; ok { - ps.lock.Unlock() - return nil, errPeerAlreadyRegistered // avoid connections with the same id as pending ones - } - // If `snap` already connected, retrieve the peer from the pending set - if snap, ok := ps.snapPend[id]; ok { - delete(ps.snapPend, id) - - ps.lock.Unlock() - return snap, nil - } - // Otherwise wait for `snap` to connect concurrently - wait := make(chan *snap.Peer) - ps.snapWait[id] = wait - ps.lock.Unlock() - - return <-wait, nil -} - -// registerPeer injects a new `eth` peer into the working set, or returns an error -// if the peer is already known. -func (ps *peerSet) registerPeer(peer *eth.Peer, ext *snap.Peer) error { - // Start tracking the new peer - ps.lock.Lock() - defer ps.lock.Unlock() - - if ps.closed { - return errPeerSetClosed - } - id := peer.ID() - if _, ok := ps.peers[id]; ok { - return errPeerAlreadyRegistered - } - eth := ðPeer{ - Peer: peer, - } - if ext != nil { - eth.snapExt = &snapPeer{ext} - ps.snapPeers++ - } - ps.peers[id] = eth - return nil -} - -// unregisterPeer removes a remote peer from the active set, disabling any further -// actions to/from that particular entity. -func (ps *peerSet) unregisterPeer(id string) error { - ps.lock.Lock() - defer ps.lock.Unlock() - - peer, ok := ps.peers[id] - if !ok { - return errPeerNotRegistered - } - delete(ps.peers, id) - if peer.snapExt != nil { - ps.snapPeers-- - } - return nil -} - -// peer retrieves the registered peer with the given id. -func (ps *peerSet) peer(id string) *ethPeer { - ps.lock.RLock() - defer ps.lock.RUnlock() - - return ps.peers[id] -} - -// peersWithoutBlock retrieves a list of peers that do not have a given block in -// their set of known hashes so it might be propagated to them. -func (ps *peerSet) peersWithoutBlock(hash common.Hash) []*ethPeer { - ps.lock.RLock() - defer ps.lock.RUnlock() - - list := make([]*ethPeer, 0, len(ps.peers)) - for _, p := range ps.peers { - if !p.KnownBlock(hash) { - list = append(list, p) - } - } - return list -} - -// peersWithoutTransaction retrieves a list of peers that do not have a given -// transaction in their set of known hashes. -func (ps *peerSet) peersWithoutTransaction(hash common.Hash) []*ethPeer { - ps.lock.RLock() - defer ps.lock.RUnlock() - - list := make([]*ethPeer, 0, len(ps.peers)) - for _, p := range ps.peers { - if !p.KnownTransaction(hash) { - list = append(list, p) - } - } - return list -} - -// len returns if the current number of `eth` peers in the set. Since the `snap` -// peers are tied to the existence of an `eth` connection, that will always be a -// subset of `eth`. -func (ps *peerSet) len() int { - ps.lock.RLock() - defer ps.lock.RUnlock() - - return len(ps.peers) -} - -// snapLen returns if the current number of `snap` peers in the set. -func (ps *peerSet) snapLen() int { - ps.lock.RLock() - defer ps.lock.RUnlock() - - return ps.snapPeers -} - -// peerWithHighestTD retrieves the known peer with the currently highest total -// difficulty, but below the given PoS switchover threshold. -func (ps *peerSet) peerWithHighestTD() *eth.Peer { - ps.lock.RLock() - defer ps.lock.RUnlock() - - var ( - bestPeer *eth.Peer - bestTd *big.Int - ) - for _, p := range ps.peers { - if _, td := p.Head(); bestPeer == nil || td.Cmp(bestTd) > 0 { - bestPeer, bestTd = p.Peer, td - } - } - return bestPeer -} - -// close disconnects all peers. -func (ps *peerSet) close() { - ps.lock.Lock() - defer ps.lock.Unlock() - - for _, p := range ps.peers { - p.Disconnect(p2p.DiscQuitting) - } - ps.closed = true -} diff --git a/eth/state_accessor.go b/eth/state_accessor.go deleted file mode 100644 index 24694df66c..0000000000 --- a/eth/state_accessor.go +++ /dev/null @@ -1,259 +0,0 @@ -// Copyright 2021 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 . - -package eth - -import ( - "context" - "errors" - "fmt" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/state" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/vm" - "github.com/ethereum/go-ethereum/eth/tracers" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/trie" -) - -// noopReleaser is returned in case there is no operation expected -// for releasing state. -var noopReleaser = tracers.StateReleaseFunc(func() {}) - -func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, readOnly bool, preferDisk bool) (statedb *state.StateDB, release tracers.StateReleaseFunc, err error) { - var ( - current *types.Block - database state.Database - triedb *trie.Database - report = true - origin = block.NumberU64() - ) - // The state is only for reading purposes, check the state presence in - // live database. - if readOnly { - // The state is available in live database, create a reference - // on top to prevent garbage collection and return a release - // function to deref it. - if statedb, err = eth.blockchain.StateAt(block.Root()); err == nil { - eth.blockchain.TrieDB().Reference(block.Root(), common.Hash{}) - return statedb, func() { - eth.blockchain.TrieDB().Dereference(block.Root()) - }, nil - } - } - // The state is both for reading and writing, or it's unavailable in disk, - // try to construct/recover the state over an ephemeral trie.Database for - // isolating the live one. - if base != nil { - if preferDisk { - // Create an ephemeral trie.Database for isolating the live one. Otherwise - // the internal junks created by tracing will be persisted into the disk. - // TODO(rjl493456442), clean cache is disabled to prevent memory leak, - // please re-enable it for better performance. - database = state.NewDatabaseWithConfig(eth.chainDb, trie.HashDefaults) - if statedb, err = state.New(block.Root(), database, nil); err == nil { - log.Info("Found disk backend for state trie", "root", block.Root(), "number", block.Number()) - return statedb, noopReleaser, nil - } - } - // The optional base statedb is given, mark the start point as parent block - statedb, database, triedb, report = base, base.Database(), base.Database().TrieDB(), false - current = eth.blockchain.GetBlock(block.ParentHash(), block.NumberU64()-1) - } else { - // Otherwise, try to reexec blocks until we find a state or reach our limit - current = block - - // Create an ephemeral trie.Database for isolating the live one. Otherwise - // the internal junks created by tracing will be persisted into the disk. - // TODO(rjl493456442), clean cache is disabled to prevent memory leak, - // please re-enable it for better performance. - triedb = trie.NewDatabase(eth.chainDb, trie.HashDefaults) - database = state.NewDatabaseWithNodeDB(eth.chainDb, triedb) - - // If we didn't check the live database, do check state over ephemeral database, - // otherwise we would rewind past a persisted block (specific corner case is - // chain tracing from the genesis). - if !readOnly { - statedb, err = state.New(current.Root(), database, nil) - if err == nil { - return statedb, noopReleaser, nil - } - } - // Database does not have the state for the given block, try to regenerate - for i := uint64(0); i < reexec; i++ { - if err := ctx.Err(); err != nil { - return nil, nil, err - } - if current.NumberU64() == 0 { - return nil, nil, errors.New("genesis state is missing") - } - parent := eth.blockchain.GetBlock(current.ParentHash(), current.NumberU64()-1) - if parent == nil { - return nil, nil, fmt.Errorf("missing block %v %d", current.ParentHash(), current.NumberU64()-1) - } - current = parent - - statedb, err = state.New(current.Root(), database, nil) - if err == nil { - break - } - } - if err != nil { - switch err.(type) { - case *trie.MissingNodeError: - return nil, nil, fmt.Errorf("required historical state unavailable (reexec=%d)", reexec) - default: - return nil, nil, err - } - } - } - // State is available at historical point, re-execute the blocks on top for - // the desired state. - var ( - start = time.Now() - logged time.Time - parent common.Hash - ) - for current.NumberU64() < origin { - if err := ctx.Err(); err != nil { - return nil, nil, err - } - // Print progress logs if long enough time elapsed - if time.Since(logged) > 8*time.Second && report { - log.Info("Regenerating historical state", "block", current.NumberU64()+1, "target", origin, "remaining", origin-current.NumberU64()-1, "elapsed", time.Since(start)) - logged = time.Now() - } - // Retrieve the next block to regenerate and process it - next := current.NumberU64() + 1 - if current = eth.blockchain.GetBlockByNumber(next); current == nil { - return nil, nil, fmt.Errorf("block #%d not found", next) - } - _, _, _, err := eth.blockchain.Processor().Process(current, statedb, vm.Config{}) - if err != nil { - return nil, nil, fmt.Errorf("processing block %d failed: %v", current.NumberU64(), err) - } - // Finalize the state so any modifications are written to the trie - root, err := statedb.Commit(current.NumberU64(), eth.blockchain.Config().IsEIP158(current.Number())) - if err != nil { - return nil, nil, fmt.Errorf("stateAtBlock commit failed, number %d root %v: %w", - current.NumberU64(), current.Root().Hex(), err) - } - statedb, err = state.New(root, database, nil) - if err != nil { - return nil, nil, fmt.Errorf("state reset after block %d failed: %v", current.NumberU64(), err) - } - // Hold the state reference and also drop the parent state - // to prevent accumulating too many nodes in memory. - triedb.Reference(root, common.Hash{}) - if parent != (common.Hash{}) { - triedb.Dereference(parent) - } - parent = root - } - if report { - _, nodes, imgs := triedb.Size() // all memory is contained within the nodes return in hashdb - log.Info("Historical state regenerated", "block", current.NumberU64(), "elapsed", time.Since(start), "nodes", nodes, "preimages", imgs) - } - return statedb, func() { triedb.Dereference(block.Root()) }, nil -} - -func (eth *Ethereum) pathState(block *types.Block) (*state.StateDB, func(), error) { - // Check if the requested state is available in the live chain. - statedb, err := eth.blockchain.StateAt(block.Root()) - if err == nil { - return statedb, noopReleaser, nil - } - // TODO historic state is not supported in path-based scheme. - // Fully archive node in pbss will be implemented by relying - // on state history, but needs more work on top. - return nil, nil, errors.New("historical state not available in path scheme yet") -} - -// stateAtBlock retrieves the state database associated with a certain block. -// If no state is locally available for the given block, a number of blocks -// are attempted to be reexecuted to generate the desired state. The optional -// base layer statedb can be provided which is regarded as the statedb of the -// parent block. -// -// An additional release function will be returned if the requested state is -// available. Release is expected to be invoked when the returned state is no -// longer needed. Its purpose is to prevent resource leaking. Though it can be -// noop in some cases. -// -// Parameters: -// - block: The block for which we want the state(state = block.Root) -// - reexec: The maximum number of blocks to reprocess trying to obtain the desired state -// - base: If the caller is tracing multiple blocks, the caller can provide the parent -// state continuously from the callsite. -// - readOnly: If true, then the live 'blockchain' state database is used. No mutation should -// be made from caller, e.g. perform Commit or other 'save-to-disk' changes. -// Otherwise, the trash generated by caller may be persisted permanently. -// - preferDisk: This arg can be used by the caller to signal that even though the 'base' is -// provided, it would be preferable to start from a fresh state, if we have it -// on disk. -func (eth *Ethereum) stateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, readOnly bool, preferDisk bool) (statedb *state.StateDB, release tracers.StateReleaseFunc, err error) { - if eth.blockchain.TrieDB().Scheme() == rawdb.HashScheme { - return eth.hashState(ctx, block, reexec, base, readOnly, preferDisk) - } - return eth.pathState(block) -} - -// stateAtTransaction returns the execution environment of a certain transaction. -func (eth *Ethereum) stateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (*core.Message, vm.BlockContext, *state.StateDB, tracers.StateReleaseFunc, error) { - // Short circuit if it's genesis block. - if block.NumberU64() == 0 { - return nil, vm.BlockContext{}, nil, nil, errors.New("no transaction in genesis") - } - // Create the parent state database - parent := eth.blockchain.GetBlock(block.ParentHash(), block.NumberU64()-1) - if parent == nil { - return nil, vm.BlockContext{}, nil, nil, fmt.Errorf("parent %#x not found", block.ParentHash()) - } - // Lookup the statedb of parent block from the live database, - // otherwise regenerate it on the flight. - statedb, release, err := eth.stateAtBlock(ctx, parent, reexec, nil, true, false) - if err != nil { - return nil, vm.BlockContext{}, nil, nil, err - } - if txIndex == 0 && len(block.Transactions()) == 0 { - return nil, vm.BlockContext{}, statedb, release, nil - } - // Recompute transactions up to the target index. - signer := types.MakeSigner(eth.blockchain.Config(), block.Number(), block.Time()) - for idx, tx := range block.Transactions() { - // Assemble the transaction call message and return if the requested offset - msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee()) - txContext := core.NewEVMTxContext(msg) - context := core.NewEVMBlockContext(block.Header(), eth.blockchain, nil) - if idx == txIndex { - return msg, context, statedb, release, nil - } - // Not yet the searched for transaction, execute on top of the current state - vmenv := vm.NewEVM(context, txContext, statedb, eth.blockchain.Config(), vm.Config{}) - statedb.SetTxContext(tx.Hash(), idx) - if _, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(tx.Gas())); err != nil { - return nil, vm.BlockContext{}, nil, nil, fmt.Errorf("transaction %#x failed: %v", tx.Hash(), err) - } - // Ensure any modifications are committed to the state - // Only delete empty objects if EIP158/161 (a.k.a Spurious Dragon) is in effect - statedb.Finalise(vmenv.ChainConfig().IsEIP158(block.Number())) - } - return nil, vm.BlockContext{}, nil, nil, fmt.Errorf("transaction index %d out of range for block %#x", txIndex, block.Hash()) -} diff --git a/eth/sync.go b/eth/sync.go deleted file mode 100644 index c7ba7c93d6..0000000000 --- a/eth/sync.go +++ /dev/null @@ -1,269 +0,0 @@ -// Copyright 2015 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 . - -package eth - -import ( - "errors" - "math/big" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/eth/downloader" - "github.com/ethereum/go-ethereum/eth/protocols/eth" - "github.com/ethereum/go-ethereum/log" -) - -const ( - forceSyncCycle = 10 * time.Second // Time interval to force syncs, even if few peers are available - defaultMinSyncPeers = 5 // Amount of peers desired to start syncing -) - -// syncTransactions starts sending all currently pending transactions to the given peer. -func (h *handler) syncTransactions(p *eth.Peer) { - var hashes []common.Hash - for _, batch := range h.txpool.Pending(false) { - for _, tx := range batch { - hashes = append(hashes, tx.Hash) - } - } - if len(hashes) == 0 { - return - } - p.AsyncSendPooledTransactionHashes(hashes) -} - -// chainSyncer coordinates blockchain sync components. -type chainSyncer struct { - handler *handler - force *time.Timer - forced bool // true when force timer fired - warned time.Time - peerEventCh chan struct{} - doneCh chan error // non-nil when sync is running -} - -// chainSyncOp is a scheduled sync operation. -type chainSyncOp struct { - mode downloader.SyncMode - peer *eth.Peer - td *big.Int - head common.Hash -} - -// newChainSyncer creates a chainSyncer. -func newChainSyncer(handler *handler) *chainSyncer { - return &chainSyncer{ - handler: handler, - peerEventCh: make(chan struct{}), - } -} - -// handlePeerEvent notifies the syncer about a change in the peer set. -// This is called for new peers and every time a peer announces a new -// chain head. -func (cs *chainSyncer) handlePeerEvent() bool { - select { - case cs.peerEventCh <- struct{}{}: - return true - case <-cs.handler.quitSync: - return false - } -} - -// loop runs in its own goroutine and launches the sync when necessary. -func (cs *chainSyncer) loop() { - defer cs.handler.wg.Done() - - cs.handler.blockFetcher.Start() - cs.handler.txFetcher.Start() - defer cs.handler.blockFetcher.Stop() - defer cs.handler.txFetcher.Stop() - defer cs.handler.downloader.Terminate() - - // The force timer lowers the peer count threshold down to one when it fires. - // This ensures we'll always start sync even if there aren't enough peers. - cs.force = time.NewTimer(forceSyncCycle) - defer cs.force.Stop() - - for { - if op := cs.nextSyncOp(); op != nil { - cs.startSync(op) - } - select { - case <-cs.peerEventCh: - // Peer information changed, recheck. - case err := <-cs.doneCh: - cs.doneCh = nil - cs.force.Reset(forceSyncCycle) - cs.forced = false - - // If we've reached the merge transition but no beacon client is available, or - // it has not yet switched us over, keep warning the user that their infra is - // potentially flaky. - if errors.Is(err, downloader.ErrMergeTransition) && time.Since(cs.warned) > 10*time.Second { - log.Warn("Local chain is post-merge, waiting for beacon client sync switch-over...") - cs.warned = time.Now() - } - case <-cs.force.C: - cs.forced = true - - case <-cs.handler.quitSync: - // Disable all insertion on the blockchain. This needs to happen before - // terminating the downloader because the downloader waits for blockchain - // inserts, and these can take a long time to finish. - cs.handler.chain.StopInsert() - cs.handler.downloader.Terminate() - if cs.doneCh != nil { - <-cs.doneCh - } - return - } - } -} - -// nextSyncOp determines whether sync is required at this time. -func (cs *chainSyncer) nextSyncOp() *chainSyncOp { - if cs.doneCh != nil { - return nil // Sync already running - } - // If a beacon client once took over control, disable the entire legacy sync - // path from here on end. Note, there is a slight "race" between reaching TTD - // and the beacon client taking over. The downloader will enforce that nothing - // above the first TTD will be delivered to the chain for import. - // - // An alternative would be to check the local chain for exceeding the TTD and - // avoid triggering a sync in that case, but that could also miss sibling or - // other family TTD block being accepted. - if cs.handler.chain.Config().TerminalTotalDifficultyPassed || cs.handler.merger.TDDReached() { - return nil - } - // Ensure we're at minimum peer count. - minPeers := defaultMinSyncPeers - if cs.forced { - minPeers = 1 - } else if minPeers > cs.handler.maxPeers { - minPeers = cs.handler.maxPeers - } - if cs.handler.peers.len() < minPeers { - return nil - } - // We have enough peers, pick the one with the highest TD, but avoid going - // over the terminal total difficulty. Above that we expect the consensus - // clients to direct the chain head to sync to. - peer := cs.handler.peers.peerWithHighestTD() - if peer == nil { - return nil - } - mode, ourTD := cs.modeAndLocalHead() - op := peerToSyncOp(mode, peer) - if op.td.Cmp(ourTD) <= 0 { - // We seem to be in sync according to the legacy rules. In the merge - // world, it can also mean we're stuck on the merge block, waiting for - // a beacon client. In the latter case, notify the user. - if ttd := cs.handler.chain.Config().TerminalTotalDifficulty; ttd != nil && ourTD.Cmp(ttd) >= 0 && time.Since(cs.warned) > 10*time.Second { - log.Warn("Local chain is post-merge, waiting for beacon client sync switch-over...") - cs.warned = time.Now() - } - return nil // We're in sync - } - return op -} - -func peerToSyncOp(mode downloader.SyncMode, p *eth.Peer) *chainSyncOp { - peerHead, peerTD := p.Head() - return &chainSyncOp{mode: mode, peer: p, td: peerTD, head: peerHead} -} - -func (cs *chainSyncer) modeAndLocalHead() (downloader.SyncMode, *big.Int) { - // If we're in snap sync mode, return that directly - if cs.handler.snapSync.Load() { - block := cs.handler.chain.CurrentSnapBlock() - td := cs.handler.chain.GetTd(block.Hash(), block.Number.Uint64()) - return downloader.SnapSync, td - } - // We are probably in full sync, but we might have rewound to before the - // snap sync pivot, check if we should re-enable snap sync. - head := cs.handler.chain.CurrentBlock() - if pivot := rawdb.ReadLastPivotNumber(cs.handler.database); pivot != nil { - if head.Number.Uint64() < *pivot { - block := cs.handler.chain.CurrentSnapBlock() - td := cs.handler.chain.GetTd(block.Hash(), block.Number.Uint64()) - return downloader.SnapSync, td - } - } - // We are in a full sync, but the associated head state is missing. To complete - // the head state, forcefully rerun the snap sync. Note it doesn't mean the - // persistent state is corrupted, just mismatch with the head block. - if !cs.handler.chain.HasState(head.Root) { - block := cs.handler.chain.CurrentSnapBlock() - td := cs.handler.chain.GetTd(block.Hash(), block.Number.Uint64()) - log.Info("Reenabled snap sync as chain is stateless") - return downloader.SnapSync, td - } - // Nope, we're really full syncing - td := cs.handler.chain.GetTd(head.Hash(), head.Number.Uint64()) - return downloader.FullSync, td -} - -// startSync launches doSync in a new goroutine. -func (cs *chainSyncer) startSync(op *chainSyncOp) { - cs.doneCh = make(chan error, 1) - go func() { cs.doneCh <- cs.handler.doSync(op) }() -} - -// doSync synchronizes the local blockchain with a remote peer. -func (h *handler) doSync(op *chainSyncOp) error { - if op.mode == downloader.SnapSync { - // Before launch the snap sync, we have to ensure user uses the same - // txlookup limit. - // The main concern here is: during the snap sync Geth won't index the - // block(generate tx indices) before the HEAD-limit. But if user changes - // the limit in the next snap sync(e.g. user kill Geth manually and - // restart) then it will be hard for Geth to figure out the oldest block - // has been indexed. So here for the user-experience wise, it's non-optimal - // that user can't change limit during the snap sync. If changed, Geth - // will just blindly use the original one. - limit := h.chain.TxLookupLimit() - if stored := rawdb.ReadFastTxLookupLimit(h.database); stored == nil { - rawdb.WriteFastTxLookupLimit(h.database, limit) - } else if *stored != limit { - h.chain.SetTxLookupLimit(*stored) - log.Warn("Update txLookup limit", "provided", limit, "updated", *stored) - } - } - // Run the sync cycle, and disable snap sync if we're past the pivot block - err := h.downloader.LegacySync(op.peer.ID(), op.head, op.td, h.chain.Config().TerminalTotalDifficulty, op.mode) - if err != nil { - return err - } - h.enableSyncedFeatures() - - head := h.chain.CurrentBlock() - if head.Number.Uint64() > 0 { - // We've completed a sync cycle, notify all peers of new state. This path is - // essential in star-topology networks where a gateway node needs to notify - // all its out-of-date peers of the availability of a new block. This failure - // scenario will most often crop up in private and hackathon networks with - // degenerate connectivity, but it should be healthy for the mainnet too to - // more reliably update peers or the local TD state. - if block := h.chain.GetBlock(head.Hash(), head.Number.Uint64()); block != nil { - h.BroadcastBlock(block, false) - } - } - return nil -}