Merge branch 'master' into remove-unused-prefetcher-error

This commit is contained in:
Felix Lange 2024-05-28 14:49:14 +02:00 committed by GitHub
commit d391c9a615
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 1774 additions and 871 deletions

View file

@ -11,9 +11,9 @@ jobs:
build:
runs-on: self-hosted
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v5
with:
go-version: 1.21.4
- name: Run tests

View file

@ -2,31 +2,35 @@
# with Go source code. If you know what GOPATH is then you probably
# don't need to bother with make.
.PHONY: geth all test lint clean devtools help
.PHONY: geth all test lint fmt clean devtools help
GOBIN = ./build/bin
GO ?= latest
GORUN = go run
#? geth: Build geth
#? geth: Build geth.
geth:
$(GORUN) build/ci.go install ./cmd/geth
@echo "Done building."
@echo "Run \"$(GOBIN)/geth\" to launch geth."
#? all: Build all packages and executables
#? all: Build all packages and executables.
all:
$(GORUN) build/ci.go install
#? test: Run the tests
#? test: Run the tests.
test: all
$(GORUN) build/ci.go test
#? lint: Run certain pre-selected linters
#? lint: Run certain pre-selected linters.
lint: ## Run linters.
$(GORUN) build/ci.go lint
#? clean: Clean go cache, built executables, and the auto generated folder
#? fmt: Ensure consistent code formatting.
fmt:
gofmt -s -w $(shell find . -name "*.go")
#? clean: Clean go cache, built executables, and the auto generated folder.
clean:
go clean -cache
rm -fr build/_workspace/pkg/ $(GOBIN)/*
@ -34,7 +38,7 @@ clean:
# The devtools target installs tools required for 'go generate'.
# You need to put $GOBIN (or $GOPATH/bin) in your PATH to use 'go generate'.
#? devtools: Install recommended developer tools
#? devtools: Install recommended developer tools.
devtools:
env GOBIN= go install golang.org/x/tools/cmd/stringer@latest
env GOBIN= go install github.com/fjl/gencodec@latest
@ -45,5 +49,9 @@ devtools:
#? help: Get more info on make commands.
help: Makefile
@echo " Choose a command run in go-ethereum:"
@echo ''
@echo 'Usage:'
@echo ' make [target]'
@echo ''
@echo 'Targets:'
@sed -n 's/^#?//p' $< | column -t -s ':' | sort | sed -e 's/^/ /'

View file

@ -65,7 +65,7 @@ func convertPayload[T payloadType](payload T, parentRoot *zrntcommon.Root) (*typ
block := types.NewBlockWithHeader(&header).WithBody(types.Body{Transactions: transactions, Withdrawals: withdrawals})
if hash := block.Hash(); hash != expectedHash {
return nil, fmt.Errorf("Sanity check failed, payload hash does not match (expected %x, got %x)", expectedHash, hash)
return nil, fmt.Errorf("sanity check failed, payload hash does not match (expected %x, got %x)", expectedHash, hash)
}
return block, nil
}

View file

@ -552,7 +552,7 @@ func listWallets(c *cli.Context) error {
// accountImport imports a raw hexadecimal private key via CLI.
func accountImport(c *cli.Context) error {
if c.Args().Len() != 1 {
return errors.New("<keyfile> must be given as first argument.")
return errors.New("<keyfile> must be given as first argument")
}
internalApi, ui, err := initInternalApi(c)
if err != nil {

View file

@ -20,6 +20,7 @@ import (
"errors"
"fmt"
"net"
"net/http"
"strconv"
"strings"
"time"
@ -28,9 +29,11 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/internal/flags"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p/discover"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc"
"github.com/urfave/cli/v2"
)
@ -45,6 +48,7 @@ var (
discv4ResolveJSONCommand,
discv4CrawlCommand,
discv4TestCommand,
discv4ListenCommand,
},
}
discv4PingCommand = &cli.Command{
@ -75,6 +79,14 @@ var (
Flags: discoveryNodeFlags,
ArgsUsage: "<nodes.json file>",
}
discv4ListenCommand = &cli.Command{
Name: "listen",
Usage: "Runs a discovery node",
Action: discv4Listen,
Flags: flags.Merge(discoveryNodeFlags, []cli.Flag{
httpAddrFlag,
}),
}
discv4CrawlCommand = &cli.Command{
Name: "crawl",
Usage: "Updates a nodes.json file with random nodes found in the DHT",
@ -131,6 +143,10 @@ var (
Usage: "Enode of the remote node under test",
EnvVars: []string{"REMOTE_ENODE"},
}
httpAddrFlag = &cli.StringFlag{
Name: "rpc",
Usage: "HTTP server listening address",
}
)
var discoveryNodeFlags = []cli.Flag{
@ -154,6 +170,27 @@ func discv4Ping(ctx *cli.Context) error {
return nil
}
func discv4Listen(ctx *cli.Context) error {
disc, _ := startV4(ctx)
defer disc.Close()
fmt.Println(disc.Self())
httpAddr := ctx.String(httpAddrFlag.Name)
if httpAddr == "" {
// Non-HTTP mode.
select {}
}
api := &discv4API{disc}
log.Info("Starting RPC API server", "addr", httpAddr)
srv := rpc.NewServer()
srv.RegisterName("discv4", api)
http.DefaultServeMux.Handle("/", srv)
httpsrv := http.Server{Addr: httpAddr, Handler: http.DefaultServeMux}
return httpsrv.ListenAndServe()
}
func discv4RequestRecord(ctx *cli.Context) error {
n := getNodeArg(ctx)
disc, _ := startV4(ctx)
@ -362,3 +399,23 @@ func parseBootnodes(ctx *cli.Context) ([]*enode.Node, error) {
}
return nodes, nil
}
type discv4API struct {
host *discover.UDPv4
}
func (api *discv4API) LookupRandom(n int) (ns []*enode.Node) {
it := api.host.RandomNodes()
for len(ns) < n && it.Next() {
ns = append(ns, it.Node())
}
return ns
}
func (api *discv4API) Buckets() [][]discover.BucketNode {
return api.host.TableBuckets()
}
func (api *discv4API) Self() *enode.Node {
return api.host.Self()
}

View file

@ -166,5 +166,37 @@
"severity": "Low",
"CVE": "CVE-2022-29177",
"check": "(Geth\\/v1\\.10\\.(0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16)-.*)$"
},
{
"name": "DoS via malicious p2p message",
"uid": "GETH-2023-01",
"summary": "A vulnerable node can be made to consume unbounded amounts of memory when handling specially crafted p2p messages sent from an attacker node.",
"description": "The p2p handler spawned a new goroutine to respond to ping requests. By flooding a node with ping requests, an unbounded number of goroutines can be created, leading to resource exhaustion and potentially crash due to OOM.",
"links": [
"https://github.com/ethereum/go-ethereum/security/advisories/GHSA-ppjg-v974-84cm",
"https://geth.ethereum.org/docs/vulnerabilities/vulnerabilities"
],
"introduced": "v1.10.0",
"fixed": "v1.12.1",
"published": "2023-09-06",
"severity": "High",
"CVE": "CVE-2023-40591",
"check": "(Geth\\/v1\\.(10|11)\\..*)|(Geth\\/v1\\.12\\.0-.*)$"
},
{
"name": "DoS via malicious p2p message",
"uid": "GETH-2024-01",
"summary": "A vulnerable node can be made to consume very large amounts of memory when handling specially crafted p2p messages sent from an attacker node.",
"description": "A vulnerable node can be made to consume very large amounts of memory when handling specially crafted p2p messages sent from an attacker node. Full details will be available at the Github security [advisory](https://github.com/ethereum/go-ethereum/security/advisories/GHSA-4xc9-8hmq-j652)",
"links": [
"https://github.com/ethereum/go-ethereum/security/advisories/GHSA-4xc9-8hmq-j652",
"https://geth.ethereum.org/docs/vulnerabilities/vulnerabilities"
],
"introduced": "v1.10.0",
"fixed": "v1.13.15",
"published": "2024-05-06",
"severity": "High",
"CVE": "CVE-2024-32972",
"check": "(Geth\\/v1\\.(10|11|12)\\..*)|(Geth\\/v1\\.13\\.\\d-.*)|(Geth\\/v1\\.13\\.1(0|1|2|3|4)-.*)$"
}
]

View file

@ -228,7 +228,7 @@ func (b *testChainIndexBackend) Process(ctx context.Context, header *types.Heade
b.t.Error("Unexpected call to Process")
// Can't use Fatal since this is not the test's goroutine.
// Returning error stops the chainIndexer's updateLoop
return errors.New("Unexpected call to Process")
return errors.New("unexpected call to Process")
case b.processCh <- header.Number.Uint64():
}
return nil

View file

@ -43,12 +43,11 @@ func TestGeneratePOSChain(t *testing.T) {
bb = common.Address{0xbb}
funds = big.NewInt(0).Mul(big.NewInt(1337), big.NewInt(params.Ether))
config = *params.AllEthashProtocolChanges
asm4788 = common.Hex2Bytes("3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500")
gspec = &Genesis{
Config: &config,
Alloc: types.GenesisAlloc{
address: {Balance: funds},
params.BeaconRootsAddress: {Balance: common.Big0, Code: asm4788},
params.BeaconRootsAddress: {Code: params.BeaconRootsCode},
},
BaseFee: big.NewInt(params.InitialBaseFee),
Difficulty: common.Big1,

View file

@ -593,6 +593,8 @@ func DeveloperGenesisBlock(gasLimit uint64, faucet *common.Address) *Genesis {
common.BytesToAddress([]byte{7}): {Balance: big.NewInt(1)}, // ECScalarMul
common.BytesToAddress([]byte{8}): {Balance: big.NewInt(1)}, // ECPairing
common.BytesToAddress([]byte{9}): {Balance: big.NewInt(1)}, // BLAKE2b
// Pre-deploy EIP-4788 system contract
params.BeaconRootsAddress: {Nonce: 1, Code: params.BeaconRootsCode},
},
}
if faucet != nil {

View file

@ -33,6 +33,7 @@ type freezerOpenFunc = func() (*Freezer, error)
// resettableFreezer is a wrapper of the freezer which makes the
// freezer resettable.
type resettableFreezer struct {
readOnly bool
freezer *Freezer
opener freezerOpenFunc
datadir string
@ -60,6 +61,7 @@ func newResettableFreezer(datadir string, namespace string, readonly bool, maxTa
return nil, err
}
return &resettableFreezer{
readOnly: readonly,
freezer: freezer,
opener: opener,
datadir: datadir,
@ -74,6 +76,9 @@ func (f *resettableFreezer) Reset() error {
f.lock.Lock()
defer f.lock.Unlock()
if f.readOnly {
return errReadOnly
}
if err := f.freezer.Close(); err != nil {
return err
}

View file

@ -127,7 +127,7 @@ func (s *stateObject) touch() {
}
}
// getTrie returns the associated storage trie. The trie will be opened if it'
// getTrie returns the associated storage trie. The trie will be opened if it's
// not loaded previously. An error will be returned if trie can't be loaded.
//
// If a new trie is opened, it will be cached within the state object to allow
@ -156,7 +156,7 @@ func (s *stateObject) getPrefetchedTrie() Trie {
if s.data.Root == types.EmptyRootHash || s.db.prefetcher == nil {
return nil
}
// Attempt to retrieve the trie from the pretecher
// Attempt to retrieve the trie from the prefetcher
return s.db.prefetcher.trie(s.addrHash, s.data.Root)
}
@ -304,10 +304,9 @@ func (s *stateObject) finalise() {
// loading or updating of the trie, an error will be returned. Furthermore,
// this function will return the mutated storage trie, or nil if there is no
// storage change at all.
//
// It assumes all the dirty storage slots have been finalized before.
func (s *stateObject) updateTrie() (Trie, error) {
// Make sure all dirty slots are finalized into the pending storage area
s.finalise()
// Short circuit if nothing changed, don't bother with hashing anything
if len(s.pendingStorage) == 0 {
return s.trie, nil

View file

@ -72,7 +72,7 @@ func newTriePrefetcher(db Database, root common.Hash, namespace string) *triePre
}
}
// terminate iterates over all the subfetchers and issues a terminateion request
// terminate iterates over all the subfetchers and issues a termination request
// to all of them. Depending on the async parameter, the method will either block
// until all subfetchers spin down, or return immediately.
func (p *triePrefetcher) terminate(async bool) {
@ -234,7 +234,7 @@ func (sf *subfetcher) schedule(keys [][]byte) error {
case sf.wake <- struct{}{}:
// Wake signal sent
default:
// Wake signal not sent as a previous is already queued
// Wake signal not sent as a previous one is already queued
}
return nil
}
@ -250,7 +250,7 @@ func (sf *subfetcher) wait() {
// peeks for the original data. The method will block until all the scheduled
// data has been loaded and the fethcer terminated.
func (sf *subfetcher) peek() Trie {
// Block until the fertcher terminates, then retrieve the trie
// Block until the fetcher terminates, then retrieve the trie
sf.wait()
return sf.trie
}
@ -296,7 +296,7 @@ func (sf *subfetcher) loop() {
for {
select {
case <-sf.wake:
// Execute all remaining tasks in single run
// Execute all remaining tasks in a single run
sf.lock.Lock()
tasks := sf.tasks
sf.tasks = nil

View file

@ -240,8 +240,9 @@ func (st *StateTransition) buyGas() error {
if st.msg.GasFeeCap != nil {
balanceCheck.SetUint64(st.msg.GasLimit)
balanceCheck = balanceCheck.Mul(balanceCheck, st.msg.GasFeeCap)
balanceCheck.Add(balanceCheck, st.msg.Value)
}
balanceCheck.Add(balanceCheck, st.msg.Value)
if st.evm.ChainConfig().IsCancun(st.evm.Context.BlockNumber, st.evm.Context.Time) {
if blobGas := st.blobGasUsed(); blobGas > 0 {
// Check that the user has enough funds to cover blobGasUsed * tx.BlobGasFeeCap

View file

@ -556,7 +556,7 @@ func (s Transactions) EncodeIndex(i int, w *bytes.Buffer) {
}
}
// TxDifference returns a new set which is the difference between a and b.
// TxDifference returns a new set of transactions that are present in a but not in b.
func TxDifference(a, b Transactions) Transactions {
keep := make(Transactions, 0, len(a))
@ -574,7 +574,7 @@ func TxDifference(a, b Transactions) Transactions {
return keep
}
// HashDifference returns a new set which is the difference between a and b.
// HashDifference returns a new set of hashes that are present in a but not in b.
func HashDifference(a, b []common.Hash) []common.Hash {
keep := make([]common.Hash, 0, len(a))

View file

@ -379,7 +379,7 @@ func assertEqual(orig *Transaction, cpy *Transaction) error {
}
if orig.AccessList() != nil {
if !reflect.DeepEqual(orig.AccessList(), cpy.AccessList()) {
return errors.New("access list wrong!")
return errors.New("access list wrong")
}
}
return nil

View file

@ -57,6 +57,10 @@ type Config struct {
// sets defaults on the config
func setDefaults(cfg *Config) {
if cfg.ChainConfig == nil {
var (
shanghaiTime = uint64(0)
cancunTime = uint64(0)
)
cfg.ChainConfig = &params.ChainConfig{
ChainID: big.NewInt(1),
HomesteadBlock: new(big.Int),
@ -72,9 +76,14 @@ func setDefaults(cfg *Config) {
MuirGlacierBlock: new(big.Int),
BerlinBlock: new(big.Int),
LondonBlock: new(big.Int),
ArrowGlacierBlock: nil,
GrayGlacierBlock: nil,
TerminalTotalDifficulty: big.NewInt(0),
TerminalTotalDifficultyPassed: true,
MergeNetsplitBlock: nil,
ShanghaiTime: &shanghaiTime,
CancunTime: &cancunTime}
}
}
if cfg.Difficulty == nil {
cfg.Difficulty = new(big.Int)
}
@ -101,6 +110,10 @@ func setDefaults(cfg *Config) {
if cfg.BlobBaseFee == nil {
cfg.BlobBaseFee = big.NewInt(params.BlobTxMinBlobGasprice)
}
// Merge indicators
if t := cfg.ChainConfig.ShanghaiTime; cfg.ChainConfig.TerminalTotalDifficultyPassed || (t != nil && *t == 0) {
cfg.Random = &(common.Hash{})
}
}
// Execute executes the code using the input as call data during the execution.

View file

@ -105,7 +105,7 @@ func TestExecute(t *testing.T) {
func TestCall(t *testing.T) {
state, _ := state.New(types.EmptyRootHash, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
address := common.HexToAddress("0x0a")
address := common.HexToAddress("0xaa")
state.SetCode(address, []byte{
byte(vm.PUSH1), 10,
byte(vm.PUSH1), 0,
@ -725,7 +725,7 @@ func TestRuntimeJSTracer(t *testing.T) {
byte(vm.CREATE),
byte(vm.POP),
},
results: []string{`"1,1,952855,6,12"`, `"1,1,952855,6,0"`},
results: []string{`"1,1,952853,6,12"`, `"1,1,952853,6,0"`},
},
{
// CREATE2
@ -741,7 +741,7 @@ func TestRuntimeJSTracer(t *testing.T) {
byte(vm.CREATE2),
byte(vm.POP),
},
results: []string{`"1,1,952846,6,13"`, `"1,1,952846,6,0"`},
results: []string{`"1,1,952844,6,13"`, `"1,1,952844,6,0"`},
},
{
// CALL

View file

@ -208,7 +208,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
}
t, err := tracers.LiveDirectory.New(config.VMTrace, traceConfig)
if err != nil {
return nil, fmt.Errorf("Failed to create tracer %s: %v", config.VMTrace, err)
return nil, fmt.Errorf("failed to create tracer %s: %v", config.VMTrace, err)
}
vmConfig.Tracer = t
}

View file

@ -979,11 +979,11 @@ func TestSimultaneousNewBlock(t *testing.T) {
defer wg.Done()
if newResp, err := api.NewPayloadV1(*execData); err != nil {
errMu.Lock()
testErr = fmt.Errorf("Failed to insert block: %w", err)
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)
testErr = fmt.Errorf("failed to insert block: %v", newResp.Status)
errMu.Unlock()
}
}()
@ -1018,7 +1018,7 @@ func TestSimultaneousNewBlock(t *testing.T) {
defer wg.Done()
if _, err := api.ForkchoiceUpdatedV1(fcState, nil); err != nil {
errMu.Lock()
testErr = fmt.Errorf("Failed to insert block: %w", err)
testErr = fmt.Errorf("failed to insert block: %w", err)
errMu.Unlock()
}
}()

View file

@ -29,6 +29,7 @@ import (
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/eth/protocols/eth"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
)
@ -376,20 +377,9 @@ func TestSkeletonSyncInit(t *testing.T) {
skeleton.Terminate()
// Ensure the correct resulting sync status
var progress skeletonProgress
json.Unmarshal(rawdb.ReadSkeletonSyncStatus(db), &progress)
if len(progress.Subchains) != len(tt.newstate) {
t.Errorf("test %d: subchain count mismatch: have %d, want %d", i, len(progress.Subchains), len(tt.newstate))
continue
}
for j := 0; j < len(progress.Subchains); j++ {
if progress.Subchains[j].Head != tt.newstate[j].Head {
t.Errorf("test %d: subchain %d head mismatch: have %d, want %d", i, j, progress.Subchains[j].Head, tt.newstate[j].Head)
}
if progress.Subchains[j].Tail != tt.newstate[j].Tail {
t.Errorf("test %d: subchain %d tail mismatch: have %d, want %d", i, j, progress.Subchains[j].Tail, tt.newstate[j].Tail)
}
expect := skeletonExpect{state: tt.newstate}
if err := checkSkeletonProgress(db, false, nil, expect); err != nil {
t.Errorf("test %d: %v", i, err)
}
}
}
@ -493,28 +483,36 @@ func TestSkeletonSyncExtend(t *testing.T) {
skeleton.Terminate()
// Ensure the correct resulting sync status
var progress skeletonProgress
json.Unmarshal(rawdb.ReadSkeletonSyncStatus(db), &progress)
expect := skeletonExpect{state: tt.newstate}
if err := checkSkeletonProgress(db, false, nil, expect); err != nil {
t.Errorf("test %d: %v", i, err)
}
}
}
if len(progress.Subchains) != len(tt.newstate) {
t.Errorf("test %d: subchain count mismatch: have %d, want %d", i, len(progress.Subchains), len(tt.newstate))
continue
}
for j := 0; j < len(progress.Subchains); j++ {
if progress.Subchains[j].Head != tt.newstate[j].Head {
t.Errorf("test %d: subchain %d head mismatch: have %d, want %d", i, j, progress.Subchains[j].Head, tt.newstate[j].Head)
}
if progress.Subchains[j].Tail != tt.newstate[j].Tail {
t.Errorf("test %d: subchain %d tail mismatch: have %d, want %d", i, j, progress.Subchains[j].Tail, tt.newstate[j].Tail)
}
}
type skeletonExpect struct {
state []*subchain // Expected sync state after the post-init event
serve uint64 // Expected number of header retrievals after initial cycle
drop uint64 // Expected number of peers dropped after initial cycle
}
type skeletonTest struct {
fill bool // Whether to run a real backfiller in this test case
unpredictable bool // Whether to ignore drops/serves due to uncertain packet assignments
head *types.Header // New head header to announce to reorg to
peers []*skeletonTestPeer // Initial peer set to start the sync with
mid skeletonExpect
newHead *types.Header // New header to anoint on top of the old one
newPeer *skeletonTestPeer // New peer to join the skeleton syncer
end skeletonExpect
}
// Tests that the skeleton sync correctly retrieves headers from one or more
// peers without duplicates or other strange side effects.
func TestSkeletonSyncRetrievals(t *testing.T) {
//log.Root().SetHandler(log.LvlFilterHandler(log.LvlTrace, log.StreamHandler(os.Stderr, log.TerminalFormat(true))))
//log.SetDefault(log.NewLogger(log.NewGlogHandler(log.NewTerminalHandler(os.Stderr, false))))
// Since skeleton headers don't need to be meaningful, beyond a parent hash
// progression, create a long fake chain to test with.
@ -537,22 +535,7 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
Extra: []byte("B"), // force a different hash
})
}
tests := []struct {
fill bool // Whether to run a real backfiller in this test case
unpredictable bool // Whether to ignore drops/serves due to uncertain packet assignments
head *types.Header // New head header to announce to reorg to
peers []*skeletonTestPeer // Initial peer set to start the sync with
midstate []*subchain // Expected sync state after initial cycle
midserve uint64 // Expected number of header retrievals after initial cycle
middrop uint64 // Expected number of peers dropped after initial cycle
newHead *types.Header // New header to anoint on top of the old one
newPeer *skeletonTestPeer // New peer to join the skeleton syncer
endstate []*subchain // Expected sync state after the post-init event
endserve uint64 // Expected number of header retrievals after the post-init event
enddrop uint64 // Expected number of peers dropped after the post-init event
}{
tests := []skeletonTest{
// Completely empty database with only the genesis set. The sync is expected
// to create a single subchain with the requested head. No peers however, so
// the sync should be stuck without any progression.
@ -561,11 +544,15 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
// to the genesis block.
{
head: chain[len(chain)-1],
midstate: []*subchain{{Head: uint64(len(chain) - 1), Tail: uint64(len(chain) - 1)}},
mid: skeletonExpect{
state: []*subchain{{Head: uint64(len(chain) - 1), Tail: uint64(len(chain) - 1)}},
},
newPeer: newSkeletonTestPeer("test-peer", chain),
endstate: []*subchain{{Head: uint64(len(chain) - 1), Tail: 1}},
endserve: uint64(len(chain) - 2), // len - head - genesis
end: skeletonExpect{
state: []*subchain{{Head: uint64(len(chain) - 1), Tail: 1}},
serve: uint64(len(chain) - 2), // len - head - genesis
},
},
// Completely empty database with only the genesis set. The sync is expected
// to create a single subchain with the requested head. With one valid peer,
@ -575,12 +562,16 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
{
head: chain[len(chain)-1],
peers: []*skeletonTestPeer{newSkeletonTestPeer("test-peer-1", chain)},
midstate: []*subchain{{Head: uint64(len(chain) - 1), Tail: 1}},
midserve: uint64(len(chain) - 2), // len - head - genesis
mid: skeletonExpect{
state: []*subchain{{Head: uint64(len(chain) - 1), Tail: 1}},
serve: uint64(len(chain) - 2), // len - head - genesis
},
newPeer: newSkeletonTestPeer("test-peer-2", chain),
endstate: []*subchain{{Head: uint64(len(chain) - 1), Tail: 1}},
endserve: uint64(len(chain) - 2), // len - head - genesis
end: skeletonExpect{
state: []*subchain{{Head: uint64(len(chain) - 1), Tail: 1}},
serve: uint64(len(chain) - 2), // len - head - genesis
},
},
// Completely empty database with only the genesis set. The sync is expected
// to create a single subchain with the requested head. With many valid peers,
@ -594,12 +585,16 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
newSkeletonTestPeer("test-peer-2", chain),
newSkeletonTestPeer("test-peer-3", chain),
},
midstate: []*subchain{{Head: uint64(len(chain) - 1), Tail: 1}},
midserve: uint64(len(chain) - 2), // len - head - genesis
mid: skeletonExpect{
state: []*subchain{{Head: uint64(len(chain) - 1), Tail: 1}},
serve: uint64(len(chain) - 2), // len - head - genesis
},
newPeer: newSkeletonTestPeer("test-peer-4", chain),
endstate: []*subchain{{Head: uint64(len(chain) - 1), Tail: 1}},
endserve: uint64(len(chain) - 2), // len - head - genesis
end: skeletonExpect{
state: []*subchain{{Head: uint64(len(chain) - 1), Tail: 1}},
serve: uint64(len(chain) - 2), // len - head - genesis
},
},
// This test checks if a peer tries to withhold a header - *on* the sync
// boundary - instead of sending the requested amount. The malicious short
@ -611,14 +606,18 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
peers: []*skeletonTestPeer{
newSkeletonTestPeer("header-skipper", append(append(append([]*types.Header{}, chain[:99]...), nil), chain[100:]...)),
},
midstate: []*subchain{{Head: requestHeaders + 100, Tail: 100}},
midserve: requestHeaders + 101 - 3, // len - head - genesis - missing
middrop: 1, // penalize shortened header deliveries
mid: skeletonExpect{
state: []*subchain{{Head: requestHeaders + 100, Tail: 100}},
serve: requestHeaders + 101 - 3, // len - head - genesis - missing
drop: 1, // penalize shortened header deliveries
},
newPeer: newSkeletonTestPeer("good-peer", chain),
endstate: []*subchain{{Head: requestHeaders + 100, Tail: 1}},
endserve: (requestHeaders + 101 - 3) + (100 - 1), // midserve + lenrest - genesis
enddrop: 1, // no new drops
end: skeletonExpect{
state: []*subchain{{Head: requestHeaders + 100, Tail: 1}},
serve: (requestHeaders + 101 - 3) + (100 - 1), // midserve + lenrest - genesis
drop: 1, // no new drops
},
},
// This test checks if a peer tries to withhold a header - *off* the sync
// boundary - instead of sending the requested amount. The malicious short
@ -630,14 +629,18 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
peers: []*skeletonTestPeer{
newSkeletonTestPeer("header-skipper", append(append(append([]*types.Header{}, chain[:50]...), nil), chain[51:]...)),
},
midstate: []*subchain{{Head: requestHeaders + 100, Tail: 100}},
midserve: requestHeaders + 101 - 3, // len - head - genesis - missing
middrop: 1, // penalize shortened header deliveries
mid: skeletonExpect{
state: []*subchain{{Head: requestHeaders + 100, Tail: 100}},
serve: requestHeaders + 101 - 3, // len - head - genesis - missing
drop: 1, // penalize shortened header deliveries
},
newPeer: newSkeletonTestPeer("good-peer", chain),
endstate: []*subchain{{Head: requestHeaders + 100, Tail: 1}},
endserve: (requestHeaders + 101 - 3) + (100 - 1), // midserve + lenrest - genesis
enddrop: 1, // no new drops
end: skeletonExpect{
state: []*subchain{{Head: requestHeaders + 100, Tail: 1}},
serve: (requestHeaders + 101 - 3) + (100 - 1), // midserve + lenrest - genesis
drop: 1, // no new drops
},
},
// This test checks if a peer tries to duplicate a header - *on* the sync
// boundary - instead of sending the correct sequence. The malicious duped
@ -649,14 +652,18 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
peers: []*skeletonTestPeer{
newSkeletonTestPeer("header-duper", append(append(append([]*types.Header{}, chain[:99]...), chain[98]), chain[100:]...)),
},
midstate: []*subchain{{Head: requestHeaders + 100, Tail: 100}},
midserve: requestHeaders + 101 - 2, // len - head - genesis
middrop: 1, // penalize invalid header sequences
mid: skeletonExpect{
state: []*subchain{{Head: requestHeaders + 100, Tail: 100}},
serve: requestHeaders + 101 - 2, // len - head - genesis
drop: 1, // penalize invalid header sequences
},
newPeer: newSkeletonTestPeer("good-peer", chain),
endstate: []*subchain{{Head: requestHeaders + 100, Tail: 1}},
endserve: (requestHeaders + 101 - 2) + (100 - 1), // midserve + lenrest - genesis
enddrop: 1, // no new drops
end: skeletonExpect{
state: []*subchain{{Head: requestHeaders + 100, Tail: 1}},
serve: (requestHeaders + 101 - 2) + (100 - 1), // midserve + lenrest - genesis
drop: 1, // no new drops
},
},
// This test checks if a peer tries to duplicate a header - *off* the sync
// boundary - instead of sending the correct sequence. The malicious duped
@ -668,14 +675,18 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
peers: []*skeletonTestPeer{
newSkeletonTestPeer("header-duper", append(append(append([]*types.Header{}, chain[:50]...), chain[49]), chain[51:]...)),
},
midstate: []*subchain{{Head: requestHeaders + 100, Tail: 100}},
midserve: requestHeaders + 101 - 2, // len - head - genesis
middrop: 1, // penalize invalid header sequences
mid: skeletonExpect{
state: []*subchain{{Head: requestHeaders + 100, Tail: 100}},
serve: requestHeaders + 101 - 2, // len - head - genesis
drop: 1, // penalize invalid header sequences
},
newPeer: newSkeletonTestPeer("good-peer", chain),
endstate: []*subchain{{Head: requestHeaders + 100, Tail: 1}},
endserve: (requestHeaders + 101 - 2) + (100 - 1), // midserve + lenrest - genesis
enddrop: 1, // no new drops
end: skeletonExpect{
state: []*subchain{{Head: requestHeaders + 100, Tail: 1}},
serve: (requestHeaders + 101 - 2) + (100 - 1), // midserve + lenrest - genesis
drop: 1, // no new drops
},
},
// This test checks if a peer tries to inject a different header - *on*
// the sync boundary - instead of sending the correct sequence. The bad
@ -698,14 +709,18 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
),
),
},
midstate: []*subchain{{Head: requestHeaders + 100, Tail: 100}},
midserve: requestHeaders + 101 - 2, // len - head - genesis
middrop: 1, // different set of headers, drop // TODO(karalabe): maybe just diff sync?
mid: skeletonExpect{
state: []*subchain{{Head: requestHeaders + 100, Tail: 100}},
serve: requestHeaders + 101 - 2, // len - head - genesis
drop: 1, // different set of headers, drop // TODO(karalabe): maybe just diff sync?
},
newPeer: newSkeletonTestPeer("good-peer", chain),
endstate: []*subchain{{Head: requestHeaders + 100, Tail: 1}},
endserve: (requestHeaders + 101 - 2) + (100 - 1), // midserve + lenrest - genesis
enddrop: 1, // no new drops
end: skeletonExpect{
state: []*subchain{{Head: requestHeaders + 100, Tail: 1}},
serve: (requestHeaders + 101 - 2) + (100 - 1), // midserve + lenrest - genesis
drop: 1, // no new drops
},
},
// This test checks if a peer tries to inject a different header - *off*
// the sync boundary - instead of sending the correct sequence. The bad
@ -728,14 +743,18 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
),
),
},
midstate: []*subchain{{Head: requestHeaders + 100, Tail: 100}},
midserve: requestHeaders + 101 - 2, // len - head - genesis
middrop: 1, // different set of headers, drop
mid: skeletonExpect{
state: []*subchain{{Head: requestHeaders + 100, Tail: 100}},
serve: requestHeaders + 101 - 2, // len - head - genesis
drop: 1, // different set of headers, drop
},
newPeer: newSkeletonTestPeer("good-peer", chain),
endstate: []*subchain{{Head: requestHeaders + 100, Tail: 1}},
endserve: (requestHeaders + 101 - 2) + (100 - 1), // midserve + lenrest - genesis
enddrop: 1, // no new drops
end: skeletonExpect{
state: []*subchain{{Head: requestHeaders + 100, Tail: 1}},
serve: (requestHeaders + 101 - 2) + (100 - 1), // midserve + lenrest - genesis
drop: 1, // no new drops
},
},
// This test reproduces a bug caught during review (kudos to @holiman)
// where a subchain is merged with a previously interrupted one, causing
@ -765,12 +784,16 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
return nil // Fallback to default behavior, just delayed
}),
},
midstate: []*subchain{{Head: 2 * requestHeaders, Tail: 1}},
midserve: 2*requestHeaders - 1, // len - head - genesis
mid: skeletonExpect{
state: []*subchain{{Head: 2 * requestHeaders, Tail: 1}},
serve: 2*requestHeaders - 1, // len - head - genesis
},
newHead: chain[2*requestHeaders+2],
endstate: []*subchain{{Head: 2*requestHeaders + 2, Tail: 1}},
endserve: 4 * requestHeaders,
end: skeletonExpect{
state: []*subchain{{Head: 2*requestHeaders + 2, Tail: 1}},
serve: 4 * requestHeaders,
},
},
// This test reproduces a bug caught by (@rjl493456442) where a skeleton
// header goes missing, causing the sync to get stuck and/or panic.
@ -794,11 +817,15 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
head: chain[len(chain)/2+1], // Sync up until the sidechain common ancestor + 2
peers: []*skeletonTestPeer{newSkeletonTestPeer("test-peer-oldchain", chain)},
midstate: []*subchain{{Head: uint64(len(chain)/2 + 1), Tail: 1}},
mid: skeletonExpect{
state: []*subchain{{Head: uint64(len(chain)/2 + 1), Tail: 1}},
},
newHead: sidechain[len(sidechain)/2+3], // Sync up until the sidechain common ancestor + 4
newPeer: newSkeletonTestPeer("test-peer-newchain", sidechain),
endstate: []*subchain{{Head: uint64(len(sidechain)/2 + 3), Tail: uint64(len(chain) / 2)}},
end: skeletonExpect{
state: []*subchain{{Head: uint64(len(sidechain)/2 + 3), Tail: uint64(len(chain) / 2)}},
},
},
}
for i, tt := range tests {
@ -861,115 +888,83 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
skeleton := newSkeleton(db, peerset, drop, filler)
skeleton.Sync(tt.head, nil, true)
var progress skeletonProgress
// Wait a bit (bleah) for the initial sync loop to go to idle. This might
// be either a finish or a never-start hence why there's no event to hook.
check := func() error {
if len(progress.Subchains) != len(tt.midstate) {
return fmt.Errorf("test %d, mid state: subchain count mismatch: have %d, want %d", i, len(progress.Subchains), len(tt.midstate))
}
for j := 0; j < len(progress.Subchains); j++ {
if progress.Subchains[j].Head != tt.midstate[j].Head {
return fmt.Errorf("test %d, mid state: subchain %d head mismatch: have %d, want %d", i, j, progress.Subchains[j].Head, tt.midstate[j].Head)
}
if progress.Subchains[j].Tail != tt.midstate[j].Tail {
return fmt.Errorf("test %d, mid state: subchain %d tail mismatch: have %d, want %d", i, j, progress.Subchains[j].Tail, tt.midstate[j].Tail)
}
}
return nil
}
waitStart := time.Now()
for waitTime := 20 * time.Millisecond; time.Since(waitStart) < 2*time.Second; waitTime = waitTime * 2 {
time.Sleep(waitTime)
// Check the post-init end state if it matches the required results
json.Unmarshal(rawdb.ReadSkeletonSyncStatus(db), &progress)
if err := check(); err == nil {
if err := checkSkeletonProgress(db, tt.unpredictable, tt.peers, tt.mid); err == nil {
break
}
}
if err := check(); err != nil {
t.Error(err)
if err := checkSkeletonProgress(db, tt.unpredictable, tt.peers, tt.mid); err != nil {
t.Errorf("test %d, mid: %v", i, err)
continue
}
if !tt.unpredictable {
var served uint64
for _, peer := range tt.peers {
served += peer.served.Load()
}
if served != tt.midserve {
t.Errorf("test %d, mid state: served headers mismatch: have %d, want %d", i, served, tt.midserve)
}
var drops uint64
for _, peer := range tt.peers {
drops += peer.dropped.Load()
}
if drops != tt.middrop {
t.Errorf("test %d, mid state: dropped peers mismatch: have %d, want %d", i, drops, tt.middrop)
}
}
// Apply the post-init events if there's any
if tt.newHead != nil {
skeleton.Sync(tt.newHead, nil, true)
}
endpeers := tt.peers
if tt.newPeer != nil {
if err := peerset.Register(newPeerConnection(tt.newPeer.id, eth.ETH68, tt.newPeer, log.New("id", tt.newPeer.id))); err != nil {
t.Errorf("test %d: failed to register new peer: %v", i, err)
}
time.Sleep(time.Millisecond * 50) // given time for peer registration
endpeers = append(tt.peers, tt.newPeer)
}
if tt.newHead != nil {
skeleton.Sync(tt.newHead, nil, true)
}
// Wait a bit (bleah) for the second sync loop to go to idle. This might
// be either a finish or a never-start hence why there's no event to hook.
check = func() error {
if len(progress.Subchains) != len(tt.endstate) {
return fmt.Errorf("test %d, end state: subchain count mismatch: have %d, want %d", i, len(progress.Subchains), len(tt.endstate))
}
for j := 0; j < len(progress.Subchains); j++ {
if progress.Subchains[j].Head != tt.endstate[j].Head {
return fmt.Errorf("test %d, end state: subchain %d head mismatch: have %d, want %d", i, j, progress.Subchains[j].Head, tt.endstate[j].Head)
}
if progress.Subchains[j].Tail != tt.endstate[j].Tail {
return fmt.Errorf("test %d, end state: subchain %d tail mismatch: have %d, want %d", i, j, progress.Subchains[j].Tail, tt.endstate[j].Tail)
}
}
return nil
}
waitStart = time.Now()
for waitTime := 20 * time.Millisecond; time.Since(waitStart) < 2*time.Second; waitTime = waitTime * 2 {
time.Sleep(waitTime)
// Check the post-init end state if it matches the required results
json.Unmarshal(rawdb.ReadSkeletonSyncStatus(db), &progress)
if err := check(); err == nil {
if err := checkSkeletonProgress(db, tt.unpredictable, endpeers, tt.end); err == nil {
break
}
}
if err := check(); err != nil {
t.Error(err)
if err := checkSkeletonProgress(db, tt.unpredictable, endpeers, tt.end); err != nil {
t.Errorf("test %d, end: %v", i, err)
continue
}
// Check that the peers served no more headers than we actually needed
if !tt.unpredictable {
served := uint64(0)
for _, peer := range tt.peers {
served += peer.served.Load()
}
if tt.newPeer != nil {
served += tt.newPeer.served.Load()
}
if served != tt.endserve {
t.Errorf("test %d, end state: served headers mismatch: have %d, want %d", i, served, tt.endserve)
}
drops := uint64(0)
for _, peer := range tt.peers {
drops += peer.dropped.Load()
}
if tt.newPeer != nil {
drops += tt.newPeer.dropped.Load()
}
if drops != tt.enddrop {
t.Errorf("test %d, end state: dropped peers mismatch: have %d, want %d", i, drops, tt.middrop)
}
}
// Clean up any leftover skeleton sync resources
skeleton.Terminate()
}
}
func checkSkeletonProgress(db ethdb.KeyValueReader, unpredictable bool, peers []*skeletonTestPeer, expected skeletonExpect) error {
var progress skeletonProgress
// Check the post-init end state if it matches the required results
json.Unmarshal(rawdb.ReadSkeletonSyncStatus(db), &progress)
if len(progress.Subchains) != len(expected.state) {
return fmt.Errorf("subchain count mismatch: have %d, want %d", len(progress.Subchains), len(expected.state))
}
for j := 0; j < len(progress.Subchains); j++ {
if progress.Subchains[j].Head != expected.state[j].Head {
return fmt.Errorf("subchain %d head mismatch: have %d, want %d", j, progress.Subchains[j].Head, expected.state[j].Head)
}
if progress.Subchains[j].Tail != expected.state[j].Tail {
return fmt.Errorf("subchain %d tail mismatch: have %d, want %d", j, progress.Subchains[j].Tail, expected.state[j].Tail)
}
}
if !unpredictable {
var served uint64
for _, peer := range peers {
served += peer.served.Load()
}
if served != expected.serve {
return fmt.Errorf("served headers mismatch: have %d, want %d", served, expected.serve)
}
var drops uint64
for _, peer := range peers {
drops += peer.dropped.Load()
}
if drops != expected.drop {
return fmt.Errorf("dropped peers mismatch: have %d, want %d", drops, expected.drop)
}
}
return nil
}

View file

@ -80,7 +80,7 @@ func makeLegacyProgress() legacyProgress {
Next: common.Hash{},
Last: common.Hash{0x77},
SubTasks: map[common.Hash][]*legacyStorageTask{
common.Hash{0x1}: {
{0x1}: {
{
Next: common.Hash{},
Last: common.Hash{0xff},

View file

@ -2358,7 +2358,7 @@ func (s *Syncer) commitHealer(force bool) {
}
batch := s.db.NewBatch()
if err := s.healer.scheduler.Commit(batch); err != nil {
log.Error("Failed to commit healing data", "err", err)
log.Crit("Failed to commit healing data", "err", err)
}
if err := batch.Write(); err != nil {
log.Crit("Failed to persist healing data", "err", err)

View file

@ -22,7 +22,6 @@ import (
"encoding/json"
"errors"
"fmt"
"math/big"
"os"
"runtime"
"sync"
@ -805,9 +804,13 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block
// Execute the transaction and flush any traces to disk
vmenv := vm.NewEVM(vmctx, txContext, statedb, chainConfig, vmConf)
statedb.SetTxContext(tx.Hash(), i)
if vmConf.Tracer.OnTxStart != nil {
vmConf.Tracer.OnTxStart(vmenv.GetVMContext(), tx, msg.From)
}
vmRet, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.GasLimit))
if vmConf.Tracer.OnTxEnd != nil {
vmConf.Tracer.OnTxEnd(&types.Receipt{GasUsed: vmRet.UsedGas}, err)
}
if writer != nil {
writer.Flush()
}
@ -982,7 +985,8 @@ func (api *API) traceTx(ctx context.Context, tx *types.Transaction, message *cor
return nil, err
}
}
vmenv := vm.NewEVM(vmctx, vm.TxContext{GasPrice: big.NewInt(0)}, statedb, api.backend.ChainConfig(), vm.Config{Tracer: tracer.Hooks, NoBaseFee: true})
// The actual TxContext will be created as part of ApplyTransactionWithEVM.
vmenv := vm.NewEVM(vmctx, vm.TxContext{GasPrice: message.GasPrice, BlobFeeCap: message.BlobGasFeeCap}, statedb, api.backend.ChainConfig(), vm.Config{Tracer: tracer.Hooks, NoBaseFee: true})
statedb.SetLogger(tracer.Hooks)
// Define a meaningful timeout of a single transaction trace

View file

@ -32,6 +32,7 @@ import (
"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/ethash"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/rawdb"
@ -994,3 +995,90 @@ func TestTraceChain(t *testing.T) {
}
}
}
// newTestMergedBackend creates a post-merge chain
func newTestMergedBackend(t *testing.T, n int, gspec *core.Genesis, generator func(i int, b *core.BlockGen)) *testBackend {
backend := &testBackend{
chainConfig: gspec.Config,
engine: beacon.NewFaker(),
chaindb: rawdb.NewMemoryDatabase(),
}
// Generate blocks for testing
_, blocks, _ := core.GenerateChainWithGenesis(gspec, backend.engine, n, generator)
// Import the canonical chain
cacheConfig := &core.CacheConfig{
TrieCleanLimit: 256,
TrieDirtyLimit: 256,
TrieTimeLimit: 5 * time.Minute,
SnapshotLimit: 0,
TrieDirtyDisabled: true, // Archive mode
}
chain, err := core.NewBlockChain(backend.chaindb, cacheConfig, gspec, nil, backend.engine, vm.Config{}, nil, nil)
if err != nil {
t.Fatalf("failed to create tester chain: %v", err)
}
if n, err := chain.InsertChain(blocks); err != nil {
t.Fatalf("block %d: failed to insert into chain: %v", n, err)
}
backend.chain = chain
return backend
}
func TestTraceBlockWithBasefee(t *testing.T) {
t.Parallel()
accounts := newAccounts(1)
target := common.HexToAddress("0x1111111111111111111111111111111111111111")
genesis := &core.Genesis{
Config: params.AllDevChainProtocolChanges,
Alloc: types.GenesisAlloc{
accounts[0].addr: {Balance: big.NewInt(1 * params.Ether)},
target: {Nonce: 1, Code: []byte{
byte(vm.BASEFEE), byte(vm.STOP),
}},
},
}
genBlocks := 1
signer := types.HomesteadSigner{}
var txHash common.Hash
var baseFee = new(big.Int)
backend := newTestMergedBackend(t, genBlocks, genesis, func(i int, b *core.BlockGen) {
tx, _ := types.SignTx(types.NewTx(&types.LegacyTx{
Nonce: uint64(i),
To: &target,
Value: big.NewInt(0),
Gas: 5 * params.TxGas,
GasPrice: b.BaseFee(),
Data: nil}),
signer, accounts[0].key)
b.AddTx(tx)
txHash = tx.Hash()
baseFee.Set(b.BaseFee())
})
defer backend.chain.Stop()
api := NewAPI(backend)
var testSuite = []struct {
blockNumber rpc.BlockNumber
config *TraceConfig
want string
}{
// Trace head block
{
blockNumber: rpc.BlockNumber(genBlocks),
want: fmt.Sprintf(`[{"txHash":"%#x","result":{"gas":21002,"failed":false,"returnValue":"","structLogs":[{"pc":0,"op":"BASEFEE","gas":84000,"gasCost":2,"depth":1,"stack":[]},{"pc":1,"op":"STOP","gas":83998,"gasCost":0,"depth":1,"stack":["%#x"]}]}}]`, txHash, baseFee),
},
}
for i, tc := range testSuite {
result, err := api.TraceBlockByNumber(context.Background(), tc.blockNumber, tc.config)
if err != nil {
t.Errorf("test %d, want no error, have %v", i, err)
continue
}
have, _ := json.Marshal(result)
want := tc.want
if string(have) != want {
t.Errorf("test %d, result mismatch\nhave: %v\nwant: %v\n", i, string(have), want)
}
}
}

File diff suppressed because one or more lines are too long

View file

@ -58,6 +58,7 @@ type jsonLogger struct {
encoder *json.Encoder
cfg *Config
env *tracing.VMContext
hooks *tracing.Hooks
}
// NewJSONLogger creates a new EVM tracer that prints execution steps as JSON objects
@ -67,12 +68,14 @@ func NewJSONLogger(cfg *Config, writer io.Writer) *tracing.Hooks {
if l.cfg == nil {
l.cfg = &Config{}
}
return &tracing.Hooks{
l.hooks = &tracing.Hooks{
OnTxStart: l.OnTxStart,
OnExit: l.OnExit,
OnSystemCallStart: l.onSystemCallStart,
OnExit: l.OnEnd,
OnOpcode: l.OnOpcode,
OnFault: l.OnFault,
}
return l.hooks
}
// NewJSONLoggerWithCallFrames creates a new EVM tracer that prints execution steps as JSON objects
@ -82,13 +85,15 @@ func NewJSONLoggerWithCallFrames(cfg *Config, writer io.Writer) *tracing.Hooks {
if l.cfg == nil {
l.cfg = &Config{}
}
return &tracing.Hooks{
l.hooks = &tracing.Hooks{
OnTxStart: l.OnTxStart,
OnSystemCallStart: l.onSystemCallStart,
OnEnter: l.OnEnter,
OnExit: l.OnExit,
OnOpcode: l.OnOpcode,
OnFault: l.OnFault,
}
return l.hooks
}
func (l *jsonLogger) OnFault(pc uint64, op byte, gas uint64, cost uint64, scope tracing.OpContext, depth int, err error) {
@ -122,6 +127,16 @@ func (l *jsonLogger) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracin
l.encoder.Encode(log)
}
func (l *jsonLogger) onSystemCallStart() {
// Process no events while in system call.
hooks := *l.hooks
*l.hooks = tracing.Hooks{
OnSystemCallEnd: func() {
*l.hooks = hooks
},
}
}
// OnEnter is not enabled by default.
func (l *jsonLogger) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
frame := callFrame{

View file

@ -74,6 +74,11 @@ func (f callFrame) failed() bool {
func (f *callFrame) processOutput(output []byte, err error, reverted bool) {
output = common.CopyBytes(output)
// Clear error if tx wasn't reverted. This happened
// for pre-homestead contract storage OOG.
if err != nil && !reverted {
err = nil
}
if err == nil {
f.Output = output
return

View file

@ -966,7 +966,7 @@ func (s *BlockChainAPI) GetBlockReceipts(ctx context.Context, blockNrOrHash rpc.
// of a message call.
// Note, state and stateDiff can't be specified at the same time. If state is
// set, message execution will only use the data in the given state. Otherwise
// if statDiff is set, all diff will be applied first and then execute the call
// if stateDiff is set, all diff will be applied first and then execute the call
// message.
type OverrideAccount struct {
Nonce *hexutil.Uint64 `json:"nonce"`

View file

@ -751,7 +751,7 @@ func TestEstimateGas(t *testing.T) {
From: &accounts[0].addr,
To: &accounts[1].addr,
Value: (*hexutil.Big)(big.NewInt(1)),
BlobHashes: []common.Hash{common.Hash{0x01, 0x22}},
BlobHashes: []common.Hash{{0x01, 0x22}},
BlobFeeCap: (*hexutil.Big)(big.NewInt(1)),
},
want: 21000,
@ -939,7 +939,7 @@ func TestCall(t *testing.T) {
call: TransactionArgs{
From: &accounts[1].addr,
To: &randomAccounts[2].addr,
BlobHashes: []common.Hash{common.Hash{0x01, 0x22}},
BlobHashes: []common.Hash{{0x01, 0x22}},
BlobFeeCap: (*hexutil.Big)(big.NewInt(1)),
},
overrides: StateOverride{
@ -1063,7 +1063,7 @@ func TestSendBlobTransaction(t *testing.T) {
From: &b.acc.Address,
To: &to,
Value: (*hexutil.Big)(big.NewInt(1)),
BlobHashes: []common.Hash{common.Hash{0x01, 0x22}},
BlobHashes: []common.Hash{{0x01, 0x22}},
})
if err != nil {
t.Fatalf("failed to fill tx defaults: %v\n", err)

View file

@ -58,7 +58,7 @@ func (h *bufHandler) Handle(_ context.Context, r slog.Record) error {
}
func (h *bufHandler) Enabled(_ context.Context, lvl slog.Level) bool {
return lvl <= h.level
return lvl >= h.level
}
func (h *bufHandler) WithAttrs(attrs []slog.Attr) slog.Handler {

View file

@ -97,7 +97,7 @@ func benchmarkLogger(b *testing.B, l Logger) {
tt = time.Now()
bigint = big.NewInt(100)
nilbig *big.Int
err = errors.New("Oh nooes it's crap")
err = errors.New("oh nooes it's crap")
)
b.ReportAllocs()
b.ResetTimer()
@ -126,7 +126,7 @@ func TestLoggerOutput(t *testing.T) {
tt = time.Time{}
bigint = big.NewInt(100)
nilbig *big.Int
err = errors.New("Oh nooes it's crap")
err = errors.New("oh nooes it's crap")
smallUint = uint256.NewInt(500_000)
bigUint = &uint256.Int{0xff, 0xff, 0xff, 0xff}
)
@ -150,7 +150,7 @@ func TestLoggerOutput(t *testing.T) {
have := out.String()
t.Logf("output %v", out.String())
want := `INFO [11-07|19:14:33.821] This is a message foo=123 bytes="[0 0 0 0 0 0 0 0 0 0]" bonk="a string with text" time=0001-01-01T00:00:00+0000 bigint=100 nilbig=<nil> err="Oh nooes it's crap" struct="{A:Foo B:12}" struct="{A:Foo\nLinebreak B:122}" ptrstruct="&{A:Foo B:12}" smalluint=500,000 bigUint=1,600,660,942,523,603,594,864,898,306,482,794,244,293,965,082,972,225,630,372,095
want := `INFO [11-07|19:14:33.821] This is a message foo=123 bytes="[0 0 0 0 0 0 0 0 0 0]" bonk="a string with text" time=0001-01-01T00:00:00+0000 bigint=100 nilbig=<nil> err="oh nooes it's crap" struct="{A:Foo B:12}" struct="{A:Foo\nLinebreak B:122}" ptrstruct="&{A:Foo B:12}" smalluint=500,000 bigUint=1,600,660,942,523,603,594,864,898,306,482,794,244,293,965,082,972,225,630,372,095
`
if !bytes.Equal([]byte(have)[25:], []byte(want)[25:]) {
t.Errorf("Error\nhave: %q\nwant: %q", have, want)

View file

@ -19,18 +19,18 @@ var (
gcStats debug.GCStats
)
// Capture new values for the Go garbage collector statistics exported in
// debug.GCStats. This is designed to be called as a goroutine.
// CaptureDebugGCStats captures new values for the Go garbage collector statistics
// exported in debug.GCStats. This is designed to be called as a goroutine.
func CaptureDebugGCStats(r Registry, d time.Duration) {
for range time.Tick(d) {
CaptureDebugGCStatsOnce(r)
}
}
// Capture new values for the Go garbage collector statistics exported in
// debug.GCStats. This is designed to be called in a background goroutine.
// Giving a registry which has not been given to RegisterDebugGCStats will
// panic.
// CaptureDebugGCStatsOnce captures new values for the Go garbage collector
// statistics exported in debug.GCStats. This is designed to be called in
// a background goroutine. Giving a registry which has not been given to
// RegisterDebugGCStats will panic.
//
// Be careful (but much less so) with this because debug.ReadGCStats calls
// the C function runtime·lock(runtime·mheap) which, while not a stop-the-world
@ -50,9 +50,9 @@ func CaptureDebugGCStatsOnce(r Registry) {
debugMetrics.GCStats.PauseTotal.Update(int64(gcStats.PauseTotal))
}
// Register metrics for the Go garbage collector statistics exported in
// debug.GCStats. The metrics are named by their fully-qualified Go symbols,
// i.e. debug.GCStats.PauseTotal.
// RegisterDebugGCStats registers metrics for the Go garbage collector statistics
// exported in debug.GCStats. The metrics are named by their fully-qualified Go
// symbols, i.e. debug.GCStats.PauseTotal.
func RegisterDebugGCStats(r Registry) {
debugMetrics.GCStats.LastGC = NewGauge()
debugMetrics.GCStats.NumGC = NewGauge()

View file

@ -103,18 +103,18 @@ func TestExpDecaySample(t *testing.T) {
}
snap := sample.Snapshot()
if have, want := int(snap.Count()), tc.updates; have != want {
t.Errorf("have %d want %d", have, want)
t.Errorf("unexpected count: have %d want %d", have, want)
}
if have, want := snap.Size(), min(tc.updates, tc.reservoirSize); have != want {
t.Errorf("have %d want %d", have, want)
t.Errorf("unexpected size: have %d want %d", have, want)
}
values := snap.(*sampleSnapshot).values
if have, want := len(values), min(tc.updates, tc.reservoirSize); have != want {
t.Errorf("have %d want %d", have, want)
t.Errorf("unexpected values length: have %d want %d", have, want)
}
for _, v := range values {
if v > int64(tc.updates) || v < 0 {
t.Errorf("out of range [0, %d): %v", tc.updates, v)
t.Errorf("out of range [0, %d]: %v", tc.updates, v)
}
}
}
@ -125,12 +125,12 @@ func TestExpDecaySample(t *testing.T) {
// The priority becomes +Inf quickly after starting if this is done,
// effectively freezing the set of samples until a rescale step happens.
func TestExpDecaySampleNanosecondRegression(t *testing.T) {
sw := NewExpDecaySample(100, 0.99)
for i := 0; i < 100; i++ {
sw := NewExpDecaySample(1000, 0.99)
for i := 0; i < 1000; i++ {
sw.Update(10)
}
time.Sleep(1 * time.Millisecond)
for i := 0; i < 100; i++ {
for i := 0; i < 1000; i++ {
sw.Update(20)
}
s := sw.Snapshot()
@ -195,7 +195,7 @@ func TestUniformSample(t *testing.T) {
}
for _, v := range values {
if v > 1000 || v < 0 {
t.Errorf("out of range [0, 100): %v\n", v)
t.Errorf("out of range [0, 1000]: %v\n", v)
}
}
}
@ -251,6 +251,9 @@ func benchmarkSample(b *testing.B, s Sample) {
}
func testExpDecaySampleStatistics(t *testing.T, s SampleSnapshot) {
if sum := s.Sum(); sum != 496598 {
t.Errorf("s.Sum(): 496598 != %v\n", sum)
}
if count := s.Count(); count != 10000 {
t.Errorf("s.Count(): 10000 != %v\n", count)
}

View file

@ -141,7 +141,7 @@ func (b *testWorkerBackend) TxPool() *txpool.TxPool { return b.txPool }
func newTestWorker(t *testing.T, chainConfig *params.ChainConfig, engine consensus.Engine, db ethdb.Database, blocks int) (*Miner, *testWorkerBackend) {
backend := newTestWorkerBackend(t, chainConfig, engine, db, blocks)
backend.txPool.Add(pendingTxs, true, false)
backend.txPool.Add(pendingTxs, true, true)
w := New(backend, testConfig, engine)
return w, backend
}

View file

@ -26,6 +26,7 @@ import (
"github.com/ethereum/go-ethereum/internal/debug"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/p2p/discover"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/rpc"
)
@ -39,6 +40,9 @@ func (n *Node) apis() []rpc.API {
}, {
Namespace: "debug",
Service: debug.Handler,
}, {
Namespace: "debug",
Service: &p2pDebugAPI{n},
}, {
Namespace: "web3",
Service: &web3API{n},
@ -333,3 +337,16 @@ func (s *web3API) ClientVersion() string {
func (s *web3API) Sha3(input hexutil.Bytes) hexutil.Bytes {
return crypto.Keccak256(input)
}
// p2pDebugAPI provides access to p2p internals for debugging.
type p2pDebugAPI struct {
stack *Node
}
func (s *p2pDebugAPI) DiscoveryV4Table() [][]discover.BucketNode {
disc := s.stack.server.DiscoveryV4()
if disc != nil {
return disc.TableBuckets()
}
return nil
}

View file

@ -18,7 +18,11 @@ package discover
import (
"crypto/ecdsa"
crand "crypto/rand"
"encoding/binary"
"math/rand"
"net"
"sync"
"time"
"github.com/ethereum/go-ethereum/common/mclock"
@ -62,7 +66,7 @@ type Config struct {
func (cfg Config) withDefaults() Config {
// Node table configuration:
if cfg.PingInterval == 0 {
cfg.PingInterval = 10 * time.Second
cfg.PingInterval = 3 * time.Second
}
if cfg.RefreshInterval == 0 {
cfg.RefreshInterval = 30 * time.Minute
@ -92,3 +96,44 @@ type ReadPacket struct {
Data []byte
Addr *net.UDPAddr
}
type randomSource interface {
Intn(int) int
Int63n(int64) int64
Shuffle(int, func(int, int))
}
// reseedingRandom is a random number generator that tracks when it was last re-seeded.
type reseedingRandom struct {
mu sync.Mutex
cur *rand.Rand
}
func (r *reseedingRandom) seed() {
var b [8]byte
crand.Read(b[:])
seed := binary.BigEndian.Uint64(b[:])
new := rand.New(rand.NewSource(int64(seed)))
r.mu.Lock()
r.cur = new
r.mu.Unlock()
}
func (r *reseedingRandom) Intn(n int) int {
r.mu.Lock()
defer r.mu.Unlock()
return r.cur.Intn(n)
}
func (r *reseedingRandom) Int63n(n int64) int64 {
r.mu.Lock()
defer r.mu.Unlock()
return r.cur.Int63n(n)
}
func (r *reseedingRandom) Shuffle(n int, swap func(i, j int)) {
r.mu.Lock()
defer r.mu.Unlock()
r.cur.Shuffle(n, swap)
}

View file

@ -140,32 +140,13 @@ func (it *lookup) slowdown() {
}
func (it *lookup) query(n *node, reply chan<- []*node) {
fails := it.tab.db.FindFails(n.ID(), n.IP())
r, err := it.queryfunc(n)
if errors.Is(err, errClosed) {
// Avoid recording failures on shutdown.
reply <- nil
return
} else if len(r) == 0 {
fails++
it.tab.db.UpdateFindFails(n.ID(), n.IP(), fails)
// Remove the node from the local table if it fails to return anything useful too
// many times, but only if there are enough other nodes in the bucket.
dropped := false
if fails >= maxFindnodeFailures && it.tab.bucketLen(n.ID()) >= bucketSize/2 {
dropped = true
it.tab.delete(n)
if !errors.Is(err, errClosed) { // avoid recording failures on shutdown.
success := len(r) > 0
it.tab.trackRequest(n, success, r)
if err != nil {
it.tab.log.Trace("FINDNODE failed", "id", n.ID(), "err", err)
}
it.tab.log.Trace("FINDNODE failed", "id", n.ID(), "failcount", fails, "dropped", dropped, "err", err)
} else if fails > 0 {
// Reset failure counter because it counts _consecutive_ failures.
it.tab.db.UpdateFindFails(n.ID(), n.IP(), 0)
}
// Grab as many nodes as possible. Some of them might not be alive anymore, but we'll
// just remove those again during revalidation.
for _, n := range r {
it.tab.addSeenNode(n)
}
reply <- r
}

View file

@ -29,12 +29,22 @@ import (
"github.com/ethereum/go-ethereum/p2p/enode"
)
type BucketNode struct {
Node *enode.Node `json:"node"`
AddedToTable time.Time `json:"addedToTable"`
AddedToBucket time.Time `json:"addedToBucket"`
Checks int `json:"checks"`
Live bool `json:"live"`
}
// node represents a host on the network.
// The fields of Node may not be modified.
type node struct {
enode.Node
addedAt time.Time // time when the node was added to the table
*enode.Node
addedToTable time.Time // first time node was added to bucket or replacement list
addedToBucket time.Time // time it was added in the actual bucket
livenessChecks uint // how often liveness was checked
isValidatedLive bool // true if existence of node is considered validated right now
}
type encPubkey [64]byte
@ -65,7 +75,7 @@ func (e encPubkey) id() enode.ID {
}
func wrapNode(n *enode.Node) *node {
return &node{Node: *n}
return &node{Node: n}
}
func wrapNodes(ns []*enode.Node) []*node {
@ -77,7 +87,7 @@ func wrapNodes(ns []*enode.Node) []*node {
}
func unwrapNode(n *node) *enode.Node {
return &n.Node
return n.Node
}
func unwrapNodes(ns []*node) []*enode.Node {

View file

@ -24,16 +24,15 @@ package discover
import (
"context"
crand "crypto/rand"
"encoding/binary"
"fmt"
mrand "math/rand"
"net"
"slices"
"sort"
"sync"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/mclock"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/p2p/enode"
@ -55,7 +54,6 @@ const (
bucketIPLimit, bucketSubnet = 2, 24 // at most 2 addresses from the same /24
tableIPLimit, tableSubnet = 10, 24
copyNodesInterval = 30 * time.Second
seedMinTableTime = 5 * time.Minute
seedCount = 30
seedMaxAge = 5 * 24 * time.Hour
@ -68,8 +66,9 @@ type Table struct {
mutex sync.Mutex // protects buckets, bucket content, nursery, rand
buckets [nBuckets]*bucket // index of known nodes by distance
nursery []*node // bootstrap nodes
rand *mrand.Rand // source of randomness, periodically reseeded
rand reseedingRandom // source of randomness, periodically reseeded
ips netutil.DistinctNetSet
revalidation tableRevalidation
db *enode.DB // database of known nodes
net transport
@ -78,6 +77,10 @@ type Table struct {
// loop channels
refreshReq chan chan struct{}
revalResponseCh chan revalidationResponse
addNodeCh chan addNodeOp
addNodeHandled chan bool
trackRequestCh chan trackRequestOp
initDone chan struct{}
closeReq chan struct{}
closed chan struct{}
@ -104,6 +107,17 @@ type bucket struct {
index int
}
type addNodeOp struct {
node *node
isInbound bool
}
type trackRequestOp struct {
node *node
foundNodes []*node
success bool
}
func newTable(t transport, db *enode.DB, cfg Config) (*Table, error) {
cfg = cfg.withDefaults()
tab := &Table{
@ -112,56 +126,49 @@ func newTable(t transport, db *enode.DB, cfg Config) (*Table, error) {
cfg: cfg,
log: cfg.Log,
refreshReq: make(chan chan struct{}),
revalResponseCh: make(chan revalidationResponse),
addNodeCh: make(chan addNodeOp),
addNodeHandled: make(chan bool),
trackRequestCh: make(chan trackRequestOp),
initDone: make(chan struct{}),
closeReq: make(chan struct{}),
closed: make(chan struct{}),
rand: mrand.New(mrand.NewSource(0)),
ips: netutil.DistinctNetSet{Subnet: tableSubnet, Limit: tableIPLimit},
}
if err := tab.setFallbackNodes(cfg.Bootnodes); err != nil {
return nil, err
}
for i := range tab.buckets {
tab.buckets[i] = &bucket{
index: i,
ips: netutil.DistinctNetSet{Subnet: bucketSubnet, Limit: bucketIPLimit},
}
}
tab.seedRand()
tab.rand.seed()
tab.revalidation.init(&cfg)
// initial table content
if err := tab.setFallbackNodes(cfg.Bootnodes); err != nil {
return nil, err
}
tab.loadSeedNodes()
return tab, nil
}
func newMeteredTable(t transport, db *enode.DB, cfg Config) (*Table, error) {
tab, err := newTable(t, db, cfg)
if err != nil {
return nil, err
}
if metrics.Enabled {
tab.nodeAddedHook = func(b *bucket, n *node) {
bucketsCounter[b.index].Inc(1)
}
tab.nodeRemovedHook = func(b *bucket, n *node) {
bucketsCounter[b.index].Dec(1)
}
}
return tab, nil
}
// Nodes returns all nodes contained in the table.
func (tab *Table) Nodes() []*enode.Node {
if !tab.isInitDone() {
return nil
}
func (tab *Table) Nodes() [][]BucketNode {
tab.mutex.Lock()
defer tab.mutex.Unlock()
var nodes []*enode.Node
for _, b := range &tab.buckets {
for _, n := range b.entries {
nodes = append(nodes, unwrapNode(n))
nodes := make([][]BucketNode, len(tab.buckets))
for i, b := range &tab.buckets {
nodes[i] = make([]BucketNode, len(b.entries))
for j, n := range b.entries {
nodes[i][j] = BucketNode{
Node: n.Node,
Checks: int(n.livenessChecks),
Live: n.isValidatedLive,
AddedToTable: n.addedToTable,
AddedToBucket: n.addedToBucket,
}
}
}
return nodes
@ -171,15 +178,6 @@ func (tab *Table) self() *enode.Node {
return tab.net.Self()
}
func (tab *Table) seedRand() {
var b [8]byte
crand.Read(b[:])
tab.mutex.Lock()
tab.rand.Seed(int64(binary.BigEndian.Uint64(b[:])))
tab.mutex.Unlock()
}
// getNode returns the node with the given ID or nil if it isn't in the table.
func (tab *Table) getNode(id enode.ID) *enode.Node {
tab.mutex.Lock()
@ -239,52 +237,173 @@ func (tab *Table) refresh() <-chan struct{} {
return done
}
// loop schedules runs of doRefresh, doRevalidate and copyLiveNodes.
// findnodeByID returns the n nodes in the table that are closest to the given id.
// This is used by the FINDNODE/v4 handler.
//
// The preferLive parameter says whether the caller wants liveness-checked results. If
// preferLive is true and the table contains any verified nodes, the result will not
// contain unverified nodes. However, if there are no verified nodes at all, the result
// will contain unverified nodes.
func (tab *Table) findnodeByID(target enode.ID, nresults int, preferLive bool) *nodesByDistance {
tab.mutex.Lock()
defer tab.mutex.Unlock()
// Scan all buckets. There might be a better way to do this, but there aren't that many
// buckets, so this solution should be fine. The worst-case complexity of this loop
// is O(tab.len() * nresults).
nodes := &nodesByDistance{target: target}
liveNodes := &nodesByDistance{target: target}
for _, b := range &tab.buckets {
for _, n := range b.entries {
nodes.push(n, nresults)
if preferLive && n.isValidatedLive {
liveNodes.push(n, nresults)
}
}
}
if preferLive && len(liveNodes.entries) > 0 {
return liveNodes
}
return nodes
}
// appendLiveNodes adds nodes at the given distance to the result slice.
// This is used by the FINDNODE/v5 handler.
func (tab *Table) appendLiveNodes(dist uint, result []*enode.Node) []*enode.Node {
if dist > 256 {
return result
}
if dist == 0 {
return append(result, tab.self())
}
tab.mutex.Lock()
for _, n := range tab.bucketAtDistance(int(dist)).entries {
if n.isValidatedLive {
result = append(result, n.Node)
}
}
tab.mutex.Unlock()
// Shuffle result to avoid always returning same nodes in FINDNODE/v5.
tab.rand.Shuffle(len(result), func(i, j int) {
result[i], result[j] = result[j], result[i]
})
return result
}
// len returns the number of nodes in the table.
func (tab *Table) len() (n int) {
tab.mutex.Lock()
defer tab.mutex.Unlock()
for _, b := range &tab.buckets {
n += len(b.entries)
}
return n
}
// addFoundNode adds a node which may not be live. If the bucket has space available,
// adding the node succeeds immediately. Otherwise, the node is added to the replacements
// list.
//
// The caller must not hold tab.mutex.
func (tab *Table) addFoundNode(n *node) bool {
op := addNodeOp{node: n, isInbound: false}
select {
case tab.addNodeCh <- op:
return <-tab.addNodeHandled
case <-tab.closeReq:
return false
}
}
// addInboundNode adds a node from an inbound contact. If the bucket has no space, the
// node is added to the replacements list.
//
// There is an additional safety measure: if the table is still initializing the node is
// not added. This prevents an attack where the table could be filled by just sending ping
// repeatedly.
//
// The caller must not hold tab.mutex.
func (tab *Table) addInboundNode(n *node) bool {
op := addNodeOp{node: n, isInbound: true}
select {
case tab.addNodeCh <- op:
return <-tab.addNodeHandled
case <-tab.closeReq:
return false
}
}
func (tab *Table) trackRequest(n *node, success bool, foundNodes []*node) {
op := trackRequestOp{n, foundNodes, success}
select {
case tab.trackRequestCh <- op:
case <-tab.closeReq:
}
}
// loop is the main loop of Table.
func (tab *Table) loop() {
var (
revalidate = time.NewTimer(tab.nextRevalidateTime())
refresh = time.NewTimer(tab.nextRefreshTime())
copyNodes = time.NewTicker(copyNodesInterval)
refreshDone = make(chan struct{}) // where doRefresh reports completion
revalidateDone chan struct{} // where doRevalidate reports completion
waiting = []chan struct{}{tab.initDone} // holds waiting callers while doRefresh runs
revalTimer = mclock.NewAlarm(tab.cfg.Clock)
reseedRandTimer = time.NewTicker(10 * time.Minute)
)
defer refresh.Stop()
defer revalidate.Stop()
defer copyNodes.Stop()
defer revalTimer.Stop()
defer reseedRandTimer.Stop()
// Start initial refresh.
go tab.doRefresh(refreshDone)
loop:
for {
nextTime := tab.revalidation.run(tab, tab.cfg.Clock.Now())
revalTimer.Schedule(nextTime)
select {
case <-reseedRandTimer.C:
tab.rand.seed()
case <-revalTimer.C():
case r := <-tab.revalResponseCh:
tab.revalidation.handleResponse(tab, r)
case op := <-tab.addNodeCh:
tab.mutex.Lock()
ok := tab.handleAddNode(op)
tab.mutex.Unlock()
tab.addNodeHandled <- ok
case op := <-tab.trackRequestCh:
tab.handleTrackRequest(op)
case <-refresh.C:
tab.seedRand()
if refreshDone == nil {
refreshDone = make(chan struct{})
go tab.doRefresh(refreshDone)
}
case req := <-tab.refreshReq:
waiting = append(waiting, req)
if refreshDone == nil {
refreshDone = make(chan struct{})
go tab.doRefresh(refreshDone)
}
case <-refreshDone:
for _, ch := range waiting {
close(ch)
}
waiting, refreshDone = nil, nil
refresh.Reset(tab.nextRefreshTime())
case <-revalidate.C:
revalidateDone = make(chan struct{})
go tab.doRevalidate(revalidateDone)
case <-revalidateDone:
revalidate.Reset(tab.nextRevalidateTime())
revalidateDone = nil
case <-copyNodes.C:
go tab.copyLiveNodes()
case <-tab.closeReq:
break loop
}
@ -296,9 +415,6 @@ loop:
for _, ch := range waiting {
close(ch)
}
if revalidateDone != nil {
<-revalidateDone
}
close(tab.closed)
}
@ -335,169 +451,15 @@ func (tab *Table) loadSeedNodes() {
age := time.Since(tab.db.LastPongReceived(seed.ID(), seed.IP()))
tab.log.Trace("Found seed node in database", "id", seed.ID(), "addr", seed.addr(), "age", age)
}
tab.addSeenNode(seed)
tab.handleAddNode(addNodeOp{node: seed, isInbound: false})
}
}
// doRevalidate checks that the last node in a random bucket is still live and replaces or
// deletes the node if it isn't.
func (tab *Table) doRevalidate(done chan<- struct{}) {
defer func() { done <- struct{}{} }()
last, bi := tab.nodeToRevalidate()
if last == nil {
// No non-empty bucket found.
return
}
// Ping the selected node and wait for a pong.
remoteSeq, err := tab.net.ping(unwrapNode(last))
// Also fetch record if the node replied and returned a higher sequence number.
if last.Seq() < remoteSeq {
n, err := tab.net.RequestENR(unwrapNode(last))
if err != nil {
tab.log.Debug("ENR request failed", "id", last.ID(), "addr", last.addr(), "err", err)
} else {
last = &node{Node: *n, addedAt: last.addedAt, livenessChecks: last.livenessChecks}
}
}
tab.mutex.Lock()
defer tab.mutex.Unlock()
b := tab.buckets[bi]
if err == nil {
// The node responded, move it to the front.
last.livenessChecks++
tab.log.Debug("Revalidated node", "b", bi, "id", last.ID(), "checks", last.livenessChecks)
tab.bumpInBucket(b, last)
return
}
// No reply received, pick a replacement or delete the node if there aren't
// any replacements.
if r := tab.replace(b, last); r != nil {
tab.log.Debug("Replaced dead node", "b", bi, "id", last.ID(), "ip", last.IP(), "checks", last.livenessChecks, "r", r.ID(), "rip", r.IP())
} else {
tab.log.Debug("Removed dead node", "b", bi, "id", last.ID(), "ip", last.IP(), "checks", last.livenessChecks)
}
}
// nodeToRevalidate returns the last node in a random, non-empty bucket.
func (tab *Table) nodeToRevalidate() (n *node, bi int) {
tab.mutex.Lock()
defer tab.mutex.Unlock()
for _, bi = range tab.rand.Perm(len(tab.buckets)) {
b := tab.buckets[bi]
if len(b.entries) > 0 {
last := b.entries[len(b.entries)-1]
return last, bi
}
}
return nil, 0
}
func (tab *Table) nextRevalidateTime() time.Duration {
tab.mutex.Lock()
defer tab.mutex.Unlock()
return time.Duration(tab.rand.Int63n(int64(tab.cfg.PingInterval)))
}
func (tab *Table) nextRefreshTime() time.Duration {
tab.mutex.Lock()
defer tab.mutex.Unlock()
half := tab.cfg.RefreshInterval / 2
return half + time.Duration(tab.rand.Int63n(int64(half)))
}
// copyLiveNodes adds nodes from the table to the database if they have been in the table
// longer than seedMinTableTime.
func (tab *Table) copyLiveNodes() {
tab.mutex.Lock()
defer tab.mutex.Unlock()
now := time.Now()
for _, b := range &tab.buckets {
for _, n := range b.entries {
if n.livenessChecks > 0 && now.Sub(n.addedAt) >= seedMinTableTime {
tab.db.UpdateNode(unwrapNode(n))
}
}
}
}
// findnodeByID returns the n nodes in the table that are closest to the given id.
// This is used by the FINDNODE/v4 handler.
//
// The preferLive parameter says whether the caller wants liveness-checked results. If
// preferLive is true and the table contains any verified nodes, the result will not
// contain unverified nodes. However, if there are no verified nodes at all, the result
// will contain unverified nodes.
func (tab *Table) findnodeByID(target enode.ID, nresults int, preferLive bool) *nodesByDistance {
tab.mutex.Lock()
defer tab.mutex.Unlock()
// Scan all buckets. There might be a better way to do this, but there aren't that many
// buckets, so this solution should be fine. The worst-case complexity of this loop
// is O(tab.len() * nresults).
nodes := &nodesByDistance{target: target}
liveNodes := &nodesByDistance{target: target}
for _, b := range &tab.buckets {
for _, n := range b.entries {
nodes.push(n, nresults)
if preferLive && n.livenessChecks > 0 {
liveNodes.push(n, nresults)
}
}
}
if preferLive && len(liveNodes.entries) > 0 {
return liveNodes
}
return nodes
}
// appendLiveNodes adds nodes at the given distance to the result slice.
func (tab *Table) appendLiveNodes(dist uint, result []*enode.Node) []*enode.Node {
if dist > 256 {
return result
}
if dist == 0 {
return append(result, tab.self())
}
tab.mutex.Lock()
defer tab.mutex.Unlock()
for _, n := range tab.bucketAtDistance(int(dist)).entries {
if n.livenessChecks >= 1 {
node := n.Node // avoid handing out pointer to struct field
result = append(result, &node)
}
}
return result
}
// len returns the number of nodes in the table.
func (tab *Table) len() (n int) {
tab.mutex.Lock()
defer tab.mutex.Unlock()
for _, b := range &tab.buckets {
n += len(b.entries)
}
return n
}
// bucketLen returns the number of nodes in the bucket for the given ID.
func (tab *Table) bucketLen(id enode.ID) int {
tab.mutex.Lock()
defer tab.mutex.Unlock()
return len(tab.bucket(id).entries)
}
// bucket returns the bucket for the given node ID hash.
func (tab *Table) bucket(id enode.ID) *bucket {
d := enode.LogDist(tab.self().ID(), id)
@ -511,95 +473,6 @@ func (tab *Table) bucketAtDistance(d int) *bucket {
return tab.buckets[d-bucketMinDistance-1]
}
// addSeenNode adds a node which may or may not be live to the end of a bucket. If the
// bucket has space available, adding the node succeeds immediately. Otherwise, the node is
// added to the replacements list.
//
// The caller must not hold tab.mutex.
func (tab *Table) addSeenNode(n *node) {
if n.ID() == tab.self().ID() {
return
}
tab.mutex.Lock()
defer tab.mutex.Unlock()
b := tab.bucket(n.ID())
if contains(b.entries, n.ID()) {
// Already in bucket, don't add.
return
}
if len(b.entries) >= bucketSize {
// Bucket full, maybe add as replacement.
tab.addReplacement(b, n)
return
}
if !tab.addIP(b, n.IP()) {
// Can't add: IP limit reached.
return
}
// Add to end of bucket:
b.entries = append(b.entries, n)
b.replacements = deleteNode(b.replacements, n)
n.addedAt = time.Now()
if tab.nodeAddedHook != nil {
tab.nodeAddedHook(b, n)
}
}
// addVerifiedNode adds a node whose existence has been verified recently to the front of a
// bucket. If the node is already in the bucket, it is moved to the front. If the bucket
// has no space, the node is added to the replacements list.
//
// There is an additional safety measure: if the table is still initializing the node
// is not added. This prevents an attack where the table could be filled by just sending
// ping repeatedly.
//
// The caller must not hold tab.mutex.
func (tab *Table) addVerifiedNode(n *node) {
if !tab.isInitDone() {
return
}
if n.ID() == tab.self().ID() {
return
}
tab.mutex.Lock()
defer tab.mutex.Unlock()
b := tab.bucket(n.ID())
if tab.bumpInBucket(b, n) {
// Already in bucket, moved to front.
return
}
if len(b.entries) >= bucketSize {
// Bucket full, maybe add as replacement.
tab.addReplacement(b, n)
return
}
if !tab.addIP(b, n.IP()) {
// Can't add: IP limit reached.
return
}
// Add to front of bucket.
b.entries, _ = pushNode(b.entries, n, bucketSize)
b.replacements = deleteNode(b.replacements, n)
n.addedAt = time.Now()
if tab.nodeAddedHook != nil {
tab.nodeAddedHook(b, n)
}
}
// delete removes an entry from the node table. It is used to evacuate dead nodes.
func (tab *Table) delete(node *node) {
tab.mutex.Lock()
defer tab.mutex.Unlock()
tab.deleteInBucket(tab.bucket(node.ID()), node)
}
func (tab *Table) addIP(b *bucket, ip net.IP) bool {
if len(ip) == 0 {
return false // Nodes without IP cannot be added.
@ -627,15 +500,51 @@ func (tab *Table) removeIP(b *bucket, ip net.IP) {
b.ips.Remove(ip)
}
func (tab *Table) addReplacement(b *bucket, n *node) {
for _, e := range b.replacements {
if e.ID() == n.ID() {
return // already in list
// handleAddNode adds the node in the request to the table, if there is space.
// The caller must hold tab.mutex.
func (tab *Table) handleAddNode(req addNodeOp) bool {
if req.node.ID() == tab.self().ID() {
return false
}
// For nodes from inbound contact, there is an additional safety measure: if the table
// is still initializing the node is not added.
if req.isInbound && !tab.isInitDone() {
return false
}
b := tab.bucket(req.node.ID())
if tab.bumpInBucket(b, req.node.Node) {
// Already in bucket, update record.
return false
}
if len(b.entries) >= bucketSize {
// Bucket full, maybe add as replacement.
tab.addReplacement(b, req.node)
return false
}
if !tab.addIP(b, req.node.IP()) {
// Can't add: IP limit reached.
return false
}
// Add to bucket.
b.entries = append(b.entries, req.node)
b.replacements = deleteNode(b.replacements, req.node)
tab.nodeAdded(b, req.node)
return true
}
// addReplacement adds n to the replacement cache of bucket b.
func (tab *Table) addReplacement(b *bucket, n *node) {
if contains(b.replacements, n.ID()) {
// TODO: update ENR
return
}
if !tab.addIP(b, n.IP()) {
return
}
n.addedToTable = time.Now()
var removed *node
b.replacements, removed = pushNode(b.replacements, n, maxReplacements)
if removed != nil {
@ -643,59 +552,107 @@ func (tab *Table) addReplacement(b *bucket, n *node) {
}
}
// replace removes n from the replacement list and replaces 'last' with it if it is the
// last entry in the bucket. If 'last' isn't the last entry, it has either been replaced
// with someone else or became active.
func (tab *Table) replace(b *bucket, last *node) *node {
if len(b.entries) == 0 || b.entries[len(b.entries)-1].ID() != last.ID() {
// Entry has moved, don't replace it.
return nil
func (tab *Table) nodeAdded(b *bucket, n *node) {
if n.addedToTable == (time.Time{}) {
n.addedToTable = time.Now()
}
// Still the last entry.
if len(b.replacements) == 0 {
tab.deleteInBucket(b, last)
return nil
n.addedToBucket = time.Now()
tab.revalidation.nodeAdded(tab, n)
if tab.nodeAddedHook != nil {
tab.nodeAddedHook(b, n)
}
if metrics.Enabled {
bucketsCounter[b.index].Inc(1)
}
r := b.replacements[tab.rand.Intn(len(b.replacements))]
b.replacements = deleteNode(b.replacements, r)
b.entries[len(b.entries)-1] = r
tab.removeIP(b, last.IP())
return r
}
// bumpInBucket moves the given node to the front of the bucket entry list
// if it is contained in that list.
func (tab *Table) bumpInBucket(b *bucket, n *node) bool {
for i := range b.entries {
if b.entries[i].ID() == n.ID() {
if !n.IP().Equal(b.entries[i].IP()) {
func (tab *Table) nodeRemoved(b *bucket, n *node) {
tab.revalidation.nodeRemoved(n)
if tab.nodeRemovedHook != nil {
tab.nodeRemovedHook(b, n)
}
if metrics.Enabled {
bucketsCounter[b.index].Dec(1)
}
}
// deleteInBucket removes node n from the table.
// If there are replacement nodes in the bucket, the node is replaced.
func (tab *Table) deleteInBucket(b *bucket, id enode.ID) *node {
index := slices.IndexFunc(b.entries, func(e *node) bool { return e.ID() == id })
if index == -1 {
// Entry has been removed already.
return nil
}
// Remove the node.
n := b.entries[index]
b.entries = slices.Delete(b.entries, index, index+1)
tab.removeIP(b, n.IP())
tab.nodeRemoved(b, n)
// Add replacement.
if len(b.replacements) == 0 {
tab.log.Debug("Removed dead node", "b", b.index, "id", n.ID(), "ip", n.IP())
return nil
}
rindex := tab.rand.Intn(len(b.replacements))
rep := b.replacements[rindex]
b.replacements = slices.Delete(b.replacements, rindex, rindex+1)
b.entries = append(b.entries, rep)
tab.nodeAdded(b, rep)
tab.log.Debug("Replaced dead node", "b", b.index, "id", n.ID(), "ip", n.IP(), "r", rep.ID(), "rip", rep.IP())
return rep
}
// bumpInBucket updates the node record of n in the bucket.
func (tab *Table) bumpInBucket(b *bucket, newRecord *enode.Node) bool {
i := slices.IndexFunc(b.entries, func(elem *node) bool {
return elem.ID() == newRecord.ID()
})
if i == -1 {
return false
}
if !newRecord.IP().Equal(b.entries[i].IP()) {
// Endpoint has changed, ensure that the new IP fits into table limits.
tab.removeIP(b, b.entries[i].IP())
if !tab.addIP(b, n.IP()) {
if !tab.addIP(b, newRecord.IP()) {
// It doesn't, put the previous one back.
tab.addIP(b, b.entries[i].IP())
return false
}
}
// Move it to the front.
copy(b.entries[1:], b.entries[:i])
b.entries[0] = n
b.entries[i].Node = newRecord
return true
}
}
return false
func (tab *Table) handleTrackRequest(op trackRequestOp) {
var fails int
if op.success {
// Reset failure counter because it counts _consecutive_ failures.
tab.db.UpdateFindFails(op.node.ID(), op.node.IP(), 0)
} else {
fails = tab.db.FindFails(op.node.ID(), op.node.IP())
fails++
tab.db.UpdateFindFails(op.node.ID(), op.node.IP(), fails)
}
func (tab *Table) deleteInBucket(b *bucket, n *node) {
// Check if the node is actually in the bucket so the removed hook
// isn't called multiple times for the same node.
if !contains(b.entries, n.ID()) {
return
tab.mutex.Lock()
defer tab.mutex.Unlock()
b := tab.bucket(op.node.ID())
// Remove the node from the local table if it fails to return anything useful too
// many times, but only if there are enough other nodes in the bucket. This latter
// condition specifically exists to make bootstrapping in smaller test networks more
// reliable.
if fails >= maxFindnodeFailures && len(b.entries) >= bucketSize/4 {
tab.deleteInBucket(b, op.node.ID())
}
b.entries = deleteNode(b.entries, n)
tab.removeIP(b, n.IP())
if tab.nodeRemovedHook != nil {
tab.nodeRemovedHook(b, n)
// Add found nodes.
for _, n := range op.foundNodes {
tab.handleAddNode(addNodeOp{n, false})
}
}

223
p2p/discover/table_reval.go Normal file
View file

@ -0,0 +1,223 @@
// Copyright 2024 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package discover
import (
"fmt"
"math"
"slices"
"time"
"github.com/ethereum/go-ethereum/common/mclock"
"github.com/ethereum/go-ethereum/p2p/enode"
)
const never = mclock.AbsTime(math.MaxInt64)
// tableRevalidation implements the node revalidation process.
// It tracks all nodes contained in Table, and schedules sending PING to them.
type tableRevalidation struct {
fast revalidationList
slow revalidationList
activeReq map[enode.ID]struct{}
}
type revalidationResponse struct {
n *node
newRecord *enode.Node
list *revalidationList
didRespond bool
}
func (tr *tableRevalidation) init(cfg *Config) {
tr.activeReq = make(map[enode.ID]struct{})
tr.fast.nextTime = never
tr.fast.interval = cfg.PingInterval
tr.fast.name = "fast"
tr.slow.nextTime = never
tr.slow.interval = cfg.PingInterval * 3
tr.slow.name = "slow"
}
// nodeAdded is called when the table receives a new node.
func (tr *tableRevalidation) nodeAdded(tab *Table, n *node) {
tr.fast.push(n, tab.cfg.Clock.Now(), &tab.rand)
}
// nodeRemoved is called when a node was removed from the table.
func (tr *tableRevalidation) nodeRemoved(n *node) {
if !tr.fast.remove(n) {
tr.slow.remove(n)
}
}
// run performs node revalidation.
// It returns the next time it should be invoked, which is used in the Table main loop
// to schedule a timer. However, run can be called at any time.
func (tr *tableRevalidation) run(tab *Table, now mclock.AbsTime) (nextTime mclock.AbsTime) {
if n := tr.fast.get(now, &tab.rand, tr.activeReq); n != nil {
tr.startRequest(tab, &tr.fast, n)
tr.fast.schedule(now, &tab.rand)
}
if n := tr.slow.get(now, &tab.rand, tr.activeReq); n != nil {
tr.startRequest(tab, &tr.slow, n)
tr.slow.schedule(now, &tab.rand)
}
return min(tr.fast.nextTime, tr.slow.nextTime)
}
// startRequest spawns a revalidation request for node n.
func (tr *tableRevalidation) startRequest(tab *Table, list *revalidationList, n *node) {
if _, ok := tr.activeReq[n.ID()]; ok {
panic(fmt.Errorf("duplicate startRequest (list %q, node %v)", list.name, n.ID()))
}
tr.activeReq[n.ID()] = struct{}{}
resp := revalidationResponse{n: n, list: list}
// Fetch the node while holding lock.
tab.mutex.Lock()
node := n.Node
tab.mutex.Unlock()
go tab.doRevalidate(resp, node)
}
func (tab *Table) doRevalidate(resp revalidationResponse, node *enode.Node) {
// Ping the selected node and wait for a pong response.
remoteSeq, err := tab.net.ping(node)
resp.didRespond = err == nil
// Also fetch record if the node replied and returned a higher sequence number.
if remoteSeq > node.Seq() {
newrec, err := tab.net.RequestENR(node)
if err != nil {
tab.log.Debug("ENR request failed", "id", node.ID(), "err", err)
} else {
resp.newRecord = newrec
}
}
select {
case tab.revalResponseCh <- resp:
case <-tab.closed:
}
}
// handleResponse processes the result of a revalidation request.
func (tr *tableRevalidation) handleResponse(tab *Table, resp revalidationResponse) {
now := tab.cfg.Clock.Now()
n := resp.n
b := tab.bucket(n.ID())
delete(tr.activeReq, n.ID())
tab.mutex.Lock()
defer tab.mutex.Unlock()
if !resp.didRespond {
// Revalidation failed.
n.livenessChecks /= 3
if n.livenessChecks <= 0 {
tab.deleteInBucket(b, n.ID())
} else {
tr.moveToList(&tr.fast, resp.list, n, now, &tab.rand)
}
return
}
// The node responded.
n.livenessChecks++
n.isValidatedLive = true
var endpointChanged bool
if resp.newRecord != nil {
endpointChanged = tab.bumpInBucket(b, resp.newRecord)
if endpointChanged {
// If the node changed its advertised endpoint, the updated ENR is not served
// until it has been revalidated.
n.isValidatedLive = false
}
}
tab.log.Debug("Revalidated node", "b", b.index, "id", n.ID(), "checks", n.livenessChecks, "q", resp.list.name)
// Move node over to slow queue after first validation.
if !endpointChanged {
tr.moveToList(&tr.slow, resp.list, n, now, &tab.rand)
} else {
tr.moveToList(&tr.fast, resp.list, n, now, &tab.rand)
}
// Store potential seeds in database.
if n.isValidatedLive && n.livenessChecks > 5 {
tab.db.UpdateNode(resp.n.Node)
}
}
func (tr *tableRevalidation) moveToList(dest, source *revalidationList, n *node, now mclock.AbsTime, rand randomSource) {
if source == dest {
return
}
if !source.remove(n) {
panic(fmt.Errorf("moveToList(%q -> %q): node %v not in source list", source.name, dest.name, n.ID()))
}
dest.push(n, now, rand)
}
// revalidationList holds a list nodes and the next revalidation time.
type revalidationList struct {
nodes []*node
nextTime mclock.AbsTime
interval time.Duration
name string
}
// get returns a random node from the queue. Nodes in the 'exclude' map are not returned.
func (list *revalidationList) get(now mclock.AbsTime, rand randomSource, exclude map[enode.ID]struct{}) *node {
if now < list.nextTime || len(list.nodes) == 0 {
return nil
}
for i := 0; i < len(list.nodes)*3; i++ {
n := list.nodes[rand.Intn(len(list.nodes))]
_, excluded := exclude[n.ID()]
if !excluded {
return n
}
}
return nil
}
func (list *revalidationList) schedule(now mclock.AbsTime, rand randomSource) {
list.nextTime = now.Add(time.Duration(rand.Int63n(int64(list.interval))))
}
func (list *revalidationList) push(n *node, now mclock.AbsTime, rand randomSource) {
list.nodes = append(list.nodes, n)
if list.nextTime == never {
list.schedule(now, rand)
}
}
func (list *revalidationList) remove(n *node) bool {
i := slices.Index(list.nodes, n)
if i == -1 {
return false
}
list.nodes = slices.Delete(list.nodes, i, i+1)
if len(list.nodes) == 0 {
list.nextTime = never
}
return true
}

View file

@ -20,14 +20,16 @@ import (
"crypto/ecdsa"
"fmt"
"math/rand"
"net"
"reflect"
"testing"
"testing/quick"
"time"
"github.com/ethereum/go-ethereum/common/mclock"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/internal/testlog"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/p2p/enr"
"github.com/ethereum/go-ethereum/p2p/netutil"
@ -49,106 +51,109 @@ func TestTable_pingReplace(t *testing.T) {
}
func testPingReplace(t *testing.T, newNodeIsResponding, lastInBucketIsResponding bool) {
simclock := new(mclock.Simulated)
transport := newPingRecorder()
tab, db := newTestTable(transport)
tab, db := newTestTable(transport, Config{
Clock: simclock,
Log: testlog.Logger(t, log.LevelTrace),
})
defer db.Close()
defer tab.close()
<-tab.initDone
// Fill up the sender's bucket.
pingKey, _ := crypto.HexToECDSA("45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8")
pingSender := wrapNode(enode.NewV4(&pingKey.PublicKey, net.IP{127, 0, 0, 1}, 99, 99))
last := fillBucket(tab, pingSender)
replacementNodeKey, _ := crypto.HexToECDSA("45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8")
replacementNode := wrapNode(enode.NewV4(&replacementNodeKey.PublicKey, net.IP{127, 0, 0, 1}, 99, 99))
last := fillBucket(tab, replacementNode.ID())
tab.mutex.Lock()
nodeEvents := newNodeEventRecorder(128)
tab.nodeAddedHook = nodeEvents.nodeAdded
tab.nodeRemovedHook = nodeEvents.nodeRemoved
tab.mutex.Unlock()
// Add the sender as if it just pinged us. Revalidate should replace the last node in
// its bucket if it is unresponsive. Revalidate again to ensure that
// The revalidation process should replace
// this node in the bucket if it is unresponsive.
transport.dead[last.ID()] = !lastInBucketIsResponding
transport.dead[pingSender.ID()] = !newNodeIsResponding
tab.addSeenNode(pingSender)
tab.doRevalidate(make(chan struct{}, 1))
tab.doRevalidate(make(chan struct{}, 1))
transport.dead[replacementNode.ID()] = !newNodeIsResponding
if !transport.pinged[last.ID()] {
// Oldest node in bucket is pinged to see whether it is still alive.
t.Error("table did not ping last node in bucket")
// Add replacement node to table.
tab.addFoundNode(replacementNode)
t.Log("last:", last.ID())
t.Log("replacement:", replacementNode.ID())
// Wait until the last node was pinged.
waitForRevalidationPing(t, transport, tab, last.ID())
if !lastInBucketIsResponding {
if !nodeEvents.waitNodeAbsent(last.ID(), 2*time.Second) {
t.Error("last node was not removed")
}
if !nodeEvents.waitNodePresent(replacementNode.ID(), 2*time.Second) {
t.Error("replacement node was not added")
}
// If a replacement is expected, we also need to wait until the replacement node
// was pinged and added/removed.
waitForRevalidationPing(t, transport, tab, replacementNode.ID())
if !newNodeIsResponding {
if !nodeEvents.waitNodeAbsent(replacementNode.ID(), 2*time.Second) {
t.Error("replacement node was not removed")
}
}
}
// Check bucket content.
tab.mutex.Lock()
defer tab.mutex.Unlock()
wantSize := bucketSize
if !lastInBucketIsResponding && !newNodeIsResponding {
wantSize--
}
if l := len(tab.bucket(pingSender.ID()).entries); l != wantSize {
t.Errorf("wrong bucket size after bond: got %d, want %d", l, wantSize)
bucket := tab.bucket(replacementNode.ID())
if l := len(bucket.entries); l != wantSize {
t.Errorf("wrong bucket size after revalidation: got %d, want %d", l, wantSize)
}
if found := contains(tab.bucket(pingSender.ID()).entries, last.ID()); found != lastInBucketIsResponding {
t.Errorf("last entry found: %t, want: %t", found, lastInBucketIsResponding)
if ok := contains(bucket.entries, last.ID()); ok != lastInBucketIsResponding {
t.Errorf("revalidated node found: %t, want: %t", ok, lastInBucketIsResponding)
}
wantNewEntry := newNodeIsResponding && !lastInBucketIsResponding
if found := contains(tab.bucket(pingSender.ID()).entries, pingSender.ID()); found != wantNewEntry {
t.Errorf("new entry found: %t, want: %t", found, wantNewEntry)
if ok := contains(bucket.entries, replacementNode.ID()); ok != wantNewEntry {
t.Errorf("replacement node found: %t, want: %t", ok, wantNewEntry)
}
}
func TestBucket_bumpNoDuplicates(t *testing.T) {
t.Parallel()
cfg := &quick.Config{
MaxCount: 1000,
Rand: rand.New(rand.NewSource(time.Now().Unix())),
Values: func(args []reflect.Value, rand *rand.Rand) {
// generate a random list of nodes. this will be the content of the bucket.
n := rand.Intn(bucketSize-1) + 1
nodes := make([]*node, n)
for i := range nodes {
nodes[i] = nodeAtDistance(enode.ID{}, 200, intIP(200))
}
args[0] = reflect.ValueOf(nodes)
// generate random bump positions.
bumps := make([]int, rand.Intn(100))
for i := range bumps {
bumps[i] = rand.Intn(len(nodes))
}
args[1] = reflect.ValueOf(bumps)
},
}
// waitForRevalidationPing waits until a PING message is sent to a node with the given id.
func waitForRevalidationPing(t *testing.T, transport *pingRecorder, tab *Table, id enode.ID) *enode.Node {
t.Helper()
prop := func(nodes []*node, bumps []int) (ok bool) {
tab, db := newTestTable(newPingRecorder())
defer db.Close()
defer tab.close()
b := &bucket{entries: make([]*node, len(nodes))}
copy(b.entries, nodes)
for i, pos := range bumps {
tab.bumpInBucket(b, b.entries[pos])
if hasDuplicates(b.entries) {
t.Logf("bucket has duplicates after %d/%d bumps:", i+1, len(bumps))
for _, n := range b.entries {
t.Logf(" %p", n)
simclock := tab.cfg.Clock.(*mclock.Simulated)
maxAttempts := tab.len() * 8
for i := 0; i < maxAttempts; i++ {
simclock.Run(tab.cfg.PingInterval)
p := transport.waitPing(2 * time.Second)
if p == nil {
t.Fatal("Table did not send revalidation ping")
}
return false
if id == (enode.ID{}) || p.ID() == id {
return p
}
}
checkIPLimitInvariant(t, tab)
return true
}
if err := quick.Check(prop, cfg); err != nil {
t.Error(err)
}
t.Fatalf("Table did not ping node %v (%d attempts)", id, maxAttempts)
return nil
}
// This checks that the table-wide IP limit is applied correctly.
func TestTable_IPLimit(t *testing.T) {
transport := newPingRecorder()
tab, db := newTestTable(transport)
tab, db := newTestTable(transport, Config{})
defer db.Close()
defer tab.close()
for i := 0; i < tableIPLimit+1; i++ {
n := nodeAtDistance(tab.self().ID(), i, net.IP{172, 0, 1, byte(i)})
tab.addSeenNode(n)
tab.addFoundNode(n)
}
if tab.len() > tableIPLimit {
t.Errorf("too many nodes in table")
@ -159,14 +164,14 @@ func TestTable_IPLimit(t *testing.T) {
// This checks that the per-bucket IP limit is applied correctly.
func TestTable_BucketIPLimit(t *testing.T) {
transport := newPingRecorder()
tab, db := newTestTable(transport)
tab, db := newTestTable(transport, Config{})
defer db.Close()
defer tab.close()
d := 3
for i := 0; i < bucketIPLimit+1; i++ {
n := nodeAtDistance(tab.self().ID(), d, net.IP{172, 0, 1, byte(i)})
tab.addSeenNode(n)
tab.addFoundNode(n)
}
if tab.len() > bucketIPLimit {
t.Errorf("too many nodes in table")
@ -196,7 +201,7 @@ func TestTable_findnodeByID(t *testing.T) {
test := func(test *closeTest) bool {
// for any node table, Target and N
transport := newPingRecorder()
tab, db := newTestTable(transport)
tab, db := newTestTable(transport, Config{})
defer db.Close()
defer tab.close()
fillTable(tab, test.All, true)
@ -271,7 +276,7 @@ func (*closeTest) Generate(rand *rand.Rand, size int) reflect.Value {
}
func TestTable_addVerifiedNode(t *testing.T) {
tab, db := newTestTable(newPingRecorder())
tab, db := newTestTable(newPingRecorder(), Config{})
<-tab.initDone
defer db.Close()
defer tab.close()
@ -279,31 +284,32 @@ func TestTable_addVerifiedNode(t *testing.T) {
// Insert two nodes.
n1 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 1})
n2 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 2})
tab.addSeenNode(n1)
tab.addSeenNode(n2)
tab.addFoundNode(n1)
tab.addFoundNode(n2)
bucket := tab.bucket(n1.ID())
// Verify bucket content:
bcontent := []*node{n1, n2}
if !reflect.DeepEqual(tab.bucket(n1.ID()).entries, bcontent) {
t.Fatalf("wrong bucket content: %v", tab.bucket(n1.ID()).entries)
if !reflect.DeepEqual(unwrapNodes(bucket.entries), unwrapNodes(bcontent)) {
t.Fatalf("wrong bucket content: %v", bucket.entries)
}
// Add a changed version of n2.
newrec := n2.Record()
newrec.Set(enr.IP{99, 99, 99, 99})
newn2 := wrapNode(enode.SignNull(newrec, n2.ID()))
tab.addVerifiedNode(newn2)
tab.addInboundNode(newn2)
// Check that bucket is updated correctly.
newBcontent := []*node{newn2, n1}
if !reflect.DeepEqual(tab.bucket(n1.ID()).entries, newBcontent) {
t.Fatalf("wrong bucket content after update: %v", tab.bucket(n1.ID()).entries)
newBcontent := []*node{n1, newn2}
if !reflect.DeepEqual(unwrapNodes(bucket.entries), unwrapNodes(newBcontent)) {
t.Fatalf("wrong bucket content after update: %v", bucket.entries)
}
checkIPLimitInvariant(t, tab)
}
func TestTable_addSeenNode(t *testing.T) {
tab, db := newTestTable(newPingRecorder())
tab, db := newTestTable(newPingRecorder(), Config{})
<-tab.initDone
defer db.Close()
defer tab.close()
@ -311,8 +317,8 @@ func TestTable_addSeenNode(t *testing.T) {
// Insert two nodes.
n1 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 1})
n2 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 2})
tab.addSeenNode(n1)
tab.addSeenNode(n2)
tab.addFoundNode(n1)
tab.addFoundNode(n2)
// Verify bucket content:
bcontent := []*node{n1, n2}
@ -324,7 +330,7 @@ func TestTable_addSeenNode(t *testing.T) {
newrec := n2.Record()
newrec.Set(enr.IP{99, 99, 99, 99})
newn2 := wrapNode(enode.SignNull(newrec, n2.ID()))
tab.addSeenNode(newn2)
tab.addFoundNode(newn2)
// Check that bucket content is unchanged.
if !reflect.DeepEqual(tab.bucket(n1.ID()).entries, bcontent) {
@ -337,7 +343,10 @@ func TestTable_addSeenNode(t *testing.T) {
// announces a new sequence number, the new record should be pulled.
func TestTable_revalidateSyncRecord(t *testing.T) {
transport := newPingRecorder()
tab, db := newTestTable(transport)
tab, db := newTestTable(transport, Config{
Clock: new(mclock.Simulated),
Log: testlog.Logger(t, log.LevelTrace),
})
<-tab.initDone
defer db.Close()
defer tab.close()
@ -347,14 +356,18 @@ func TestTable_revalidateSyncRecord(t *testing.T) {
r.Set(enr.IP(net.IP{127, 0, 0, 1}))
id := enode.ID{1}
n1 := wrapNode(enode.SignNull(&r, id))
tab.addSeenNode(n1)
tab.addFoundNode(n1)
// Update the node record.
r.Set(enr.WithEntry("foo", "bar"))
n2 := enode.SignNull(&r, id)
transport.updateRecord(n2)
tab.doRevalidate(make(chan struct{}, 1))
// Wait for revalidation. We wait for the node to be revalidated two times
// in order to synchronize with the update in the table.
waitForRevalidationPing(t, transport, tab, n2.ID())
waitForRevalidationPing(t, transport, tab, n2.ID())
intable := tab.getNode(id)
if !reflect.DeepEqual(intable, n2) {
t.Fatalf("table contains old record with seq %d, want seq %d", intable.Seq(), n2.Seq())

View file

@ -26,6 +26,8 @@ import (
"net"
"slices"
"sync"
"sync/atomic"
"time"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/p2p/enode"
@ -40,8 +42,7 @@ func init() {
nullNode = enode.SignNull(&r, enode.ID{})
}
func newTestTable(t transport) (*Table, *enode.DB) {
cfg := Config{}
func newTestTable(t transport, cfg Config) (*Table, *enode.DB) {
db, _ := enode.OpenDB("")
tab, _ := newTable(t, db, cfg)
go tab.loop()
@ -98,11 +99,14 @@ func intIP(i int) net.IP {
}
// fillBucket inserts nodes into the given bucket until it is full.
func fillBucket(tab *Table, n *node) (last *node) {
ld := enode.LogDist(tab.self().ID(), n.ID())
b := tab.bucket(n.ID())
func fillBucket(tab *Table, id enode.ID) (last *node) {
ld := enode.LogDist(tab.self().ID(), id)
b := tab.bucket(id)
for len(b.entries) < bucketSize {
b.entries = append(b.entries, nodeAtDistance(tab.self().ID(), ld, intIP(ld)))
node := nodeAtDistance(tab.self().ID(), ld, intIP(ld))
if !tab.addFoundNode(node) {
panic("node not added")
}
}
return b.entries[bucketSize-1]
}
@ -113,15 +117,18 @@ func fillTable(tab *Table, nodes []*node, setLive bool) {
for _, n := range nodes {
if setLive {
n.livenessChecks = 1
n.isValidatedLive = true
}
tab.addSeenNode(n)
tab.addFoundNode(n)
}
}
type pingRecorder struct {
mu sync.Mutex
dead, pinged map[enode.ID]bool
cond *sync.Cond
dead map[enode.ID]bool
records map[enode.ID]*enode.Node
pinged []*enode.Node
n *enode.Node
}
@ -130,12 +137,13 @@ func newPingRecorder() *pingRecorder {
r.Set(enr.IP{0, 0, 0, 0})
n := enode.SignNull(&r, enode.ID{})
return &pingRecorder{
t := &pingRecorder{
dead: make(map[enode.ID]bool),
pinged: make(map[enode.ID]bool),
records: make(map[enode.ID]*enode.Node),
n: n,
}
t.cond = sync.NewCond(&t.mu)
return t
}
// updateRecord updates a node record. Future calls to ping and
@ -151,12 +159,40 @@ func (t *pingRecorder) Self() *enode.Node { return nullNode }
func (t *pingRecorder) lookupSelf() []*enode.Node { return nil }
func (t *pingRecorder) lookupRandom() []*enode.Node { return nil }
func (t *pingRecorder) waitPing(timeout time.Duration) *enode.Node {
t.mu.Lock()
defer t.mu.Unlock()
// Wake up the loop on timeout.
var timedout atomic.Bool
timer := time.AfterFunc(timeout, func() {
timedout.Store(true)
t.cond.Broadcast()
})
defer timer.Stop()
// Wait for a ping.
for {
if timedout.Load() {
return nil
}
if len(t.pinged) > 0 {
n := t.pinged[0]
t.pinged = append(t.pinged[:0], t.pinged[1:]...)
return n
}
t.cond.Wait()
}
}
// ping simulates a ping request.
func (t *pingRecorder) ping(n *enode.Node) (seq uint64, err error) {
t.mu.Lock()
defer t.mu.Unlock()
t.pinged[n.ID()] = true
t.pinged = append(t.pinged, n)
t.cond.Broadcast()
if t.dead[n.ID()] {
return 0, errTimeout
}
@ -256,3 +292,57 @@ func hexEncPubkey(h string) (ret encPubkey) {
copy(ret[:], b)
return ret
}
type nodeEventRecorder struct {
evc chan recordedNodeEvent
}
type recordedNodeEvent struct {
node *node
added bool
}
func newNodeEventRecorder(buffer int) *nodeEventRecorder {
return &nodeEventRecorder{
evc: make(chan recordedNodeEvent, buffer),
}
}
func (set *nodeEventRecorder) nodeAdded(b *bucket, n *node) {
select {
case set.evc <- recordedNodeEvent{n, true}:
default:
panic("no space in event buffer")
}
}
func (set *nodeEventRecorder) nodeRemoved(b *bucket, n *node) {
select {
case set.evc <- recordedNodeEvent{n, false}:
default:
panic("no space in event buffer")
}
}
func (set *nodeEventRecorder) waitNodePresent(id enode.ID, timeout time.Duration) bool {
return set.waitNodeEvent(id, timeout, true)
}
func (set *nodeEventRecorder) waitNodeAbsent(id enode.ID, timeout time.Duration) bool {
return set.waitNodeEvent(id, timeout, false)
}
func (set *nodeEventRecorder) waitNodeEvent(id enode.ID, timeout time.Duration, added bool) bool {
timer := time.NewTimer(timeout)
defer timer.Stop()
for {
select {
case ev := <-set.evc:
if ev.node.ID() == id && ev.added == added {
return true
}
case <-timer.C:
return false
}
}
}

View file

@ -142,7 +142,7 @@ func ListenV4(c UDPConn, ln *enode.LocalNode, cfg Config) (*UDPv4, error) {
log: cfg.Log,
}
tab, err := newMeteredTable(t, ln.Database(), cfg)
tab, err := newTable(t, ln.Database(), cfg)
if err != nil {
return nil, err
}
@ -375,6 +375,10 @@ func (t *UDPv4) RequestENR(n *enode.Node) (*enode.Node, error) {
return respN, nil
}
func (t *UDPv4) TableBuckets() [][]BucketNode {
return t.tab.Nodes()
}
// pending adds a reply matcher to the pending reply queue.
// see the documentation of type replyMatcher for a detailed explanation.
func (t *UDPv4) pending(id enode.ID, ip net.IP, ptype byte, callback replyMatchFunc) *replyMatcher {
@ -669,10 +673,10 @@ func (t *UDPv4) handlePing(h *packetHandlerV4, from *net.UDPAddr, fromID enode.I
n := wrapNode(enode.NewV4(h.senderKey, from.IP, int(req.From.TCP), from.Port))
if time.Since(t.db.LastPongReceived(n.ID(), from.IP)) > bondExpiration {
t.sendPing(fromID, from, func() {
t.tab.addVerifiedNode(n)
t.tab.addInboundNode(n)
})
} else {
t.tab.addVerifiedNode(n)
t.tab.addInboundNode(n)
}
// Update node database and endpoint predictor.

View file

@ -264,7 +264,7 @@ func TestUDPv4_findnode(t *testing.T) {
n := wrapNode(enode.NewV4(&key.PublicKey, ip, 0, 2000))
// Ensure half of table content isn't verified live yet.
if i > numCandidates/2 {
n.livenessChecks = 1
n.isValidatedLive = true
live[n.ID()] = true
}
nodes.push(n, numCandidates)

View file

@ -175,7 +175,7 @@ func newUDPv5(conn UDPConn, ln *enode.LocalNode, cfg Config) (*UDPv5, error) {
cancelCloseCtx: cancelCloseCtx,
}
t.talk = newTalkSystem(t)
tab, err := newMeteredTable(t, t.db, cfg)
tab, err := newTable(t, t.db, cfg)
if err != nil {
return nil, err
}
@ -699,7 +699,7 @@ func (t *UDPv5) handlePacket(rawpacket []byte, fromAddr *net.UDPAddr) error {
}
if fromNode != nil {
// Handshake succeeded, add to table.
t.tab.addSeenNode(wrapNode(fromNode))
t.tab.addInboundNode(wrapNode(fromNode))
}
if packet.Kind() != v5wire.WhoareyouPacket {
// WHOAREYOU logged separately to report errors.

View file

@ -141,7 +141,7 @@ func TestUDPv5_unknownPacket(t *testing.T) {
// Make node known.
n := test.getNode(test.remotekey, test.remoteaddr).Node()
test.table.addSeenNode(wrapNode(n))
test.table.addFoundNode(wrapNode(n))
test.packetIn(&v5wire.Unknown{Nonce: nonce})
test.waitPacketOut(func(p *v5wire.Whoareyou, addr *net.UDPAddr, _ v5wire.Nonce) {

View file

@ -157,5 +157,5 @@ func SignNull(r *enr.Record, id ID) *Node {
if err := r.SetSig(NullID{}, []byte{}); err != nil {
panic(err)
}
return &Node{r: *r, id: id}
return newNodeWithID(r, id)
}

View file

@ -24,6 +24,7 @@ import (
"fmt"
"math/bits"
"net"
"net/netip"
"strings"
"github.com/ethereum/go-ethereum/p2p/enr"
@ -36,6 +37,10 @@ var errMissingPrefix = errors.New("missing 'enr:' prefix for base64-encoded reco
type Node struct {
r enr.Record
id ID
// endpoint information
ip netip.Addr
udp uint16
tcp uint16
}
// New wraps a node record. The record must be valid according to the given
@ -44,11 +49,76 @@ func New(validSchemes enr.IdentityScheme, r *enr.Record) (*Node, error) {
if err := r.VerifySignature(validSchemes); err != nil {
return nil, err
}
node := &Node{r: *r}
if n := copy(node.id[:], validSchemes.NodeAddr(&node.r)); n != len(ID{}) {
return nil, fmt.Errorf("invalid node ID length %d, need %d", n, len(ID{}))
var id ID
if n := copy(id[:], validSchemes.NodeAddr(r)); n != len(id) {
return nil, fmt.Errorf("invalid node ID length %d, need %d", n, len(id))
}
return newNodeWithID(r, id), nil
}
func newNodeWithID(r *enr.Record, id ID) *Node {
n := &Node{r: *r, id: id}
// Set the preferred endpoint.
// Here we decide between IPv4 and IPv6, choosing the 'most global' address.
var ip4 netip.Addr
var ip6 netip.Addr
n.Load((*enr.IPv4Addr)(&ip4))
n.Load((*enr.IPv6Addr)(&ip6))
valid4 := validIP(ip4)
valid6 := validIP(ip6)
switch {
case valid4 && valid6:
if localityScore(ip4) >= localityScore(ip6) {
n.setIP4(ip4)
} else {
n.setIP6(ip6)
}
case valid4:
n.setIP4(ip4)
case valid6:
n.setIP6(ip6)
}
return n
}
// validIP reports whether 'ip' is a valid node endpoint IP address.
func validIP(ip netip.Addr) bool {
return ip.IsValid() && !ip.IsMulticast()
}
func localityScore(ip netip.Addr) int {
switch {
case ip.IsUnspecified():
return 0
case ip.IsLoopback():
return 1
case ip.IsLinkLocalUnicast():
return 2
case ip.IsPrivate():
return 3
default:
return 4
}
}
func (n *Node) setIP4(ip netip.Addr) {
n.ip = ip
n.Load((*enr.UDP)(&n.udp))
n.Load((*enr.TCP)(&n.tcp))
}
func (n *Node) setIP6(ip netip.Addr) {
if ip.Is4In6() {
n.setIP4(ip)
return
}
n.ip = ip
if err := n.Load((*enr.UDP6)(&n.udp)); err != nil {
n.Load((*enr.UDP)(&n.udp))
}
if err := n.Load((*enr.TCP6)(&n.tcp)); err != nil {
n.Load((*enr.TCP)(&n.tcp))
}
return node, nil
}
// MustParse parses a node record or enode:// URL. It panics if the input is invalid.
@ -89,43 +159,45 @@ func (n *Node) Seq() uint64 {
return n.r.Seq()
}
// Incomplete returns true for nodes with no IP address.
func (n *Node) Incomplete() bool {
return n.IP() == nil
}
// Load retrieves an entry from the underlying record.
func (n *Node) Load(k enr.Entry) error {
return n.r.Load(k)
}
// IP returns the IP address of the node. This prefers IPv4 addresses.
// IP returns the IP address of the node.
func (n *Node) IP() net.IP {
var (
ip4 enr.IPv4
ip6 enr.IPv6
)
if n.Load(&ip4) == nil {
return net.IP(ip4)
return net.IP(n.ip.AsSlice())
}
if n.Load(&ip6) == nil {
return net.IP(ip6)
}
return nil
// IPAddr returns the IP address of the node.
func (n *Node) IPAddr() netip.Addr {
return n.ip
}
// UDP returns the UDP port of the node.
func (n *Node) UDP() int {
var port enr.UDP
n.Load(&port)
return int(port)
return int(n.udp)
}
// TCP returns the TCP port of the node.
func (n *Node) TCP() int {
var port enr.TCP
n.Load(&port)
return int(port)
return int(n.tcp)
}
// UDPEndpoint returns the announced UDP endpoint.
func (n *Node) UDPEndpoint() (netip.AddrPort, bool) {
if !n.ip.IsValid() || n.ip.IsUnspecified() || n.udp == 0 {
return netip.AddrPort{}, false
}
return netip.AddrPortFrom(n.ip, n.udp), true
}
// TCPEndpoint returns the announced TCP endpoint.
func (n *Node) TCPEndpoint() (netip.AddrPort, bool) {
if !n.ip.IsValid() || n.ip.IsUnspecified() || n.tcp == 0 {
return netip.AddrPort{}, false
}
return netip.AddrPortFrom(n.ip, n.tcp), true
}
// Pubkey returns the secp256k1 public key of the node, if present.
@ -147,16 +219,15 @@ func (n *Node) Record() *enr.Record {
// ValidateComplete checks whether n has a valid IP and UDP port.
// Deprecated: don't use this method.
func (n *Node) ValidateComplete() error {
if n.Incomplete() {
if !n.ip.IsValid() {
return errors.New("missing IP address")
}
if n.UDP() == 0 {
return errors.New("missing UDP port")
}
ip := n.IP()
if ip.IsMulticast() || ip.IsUnspecified() {
if n.ip.IsMulticast() || n.ip.IsUnspecified() {
return errors.New("invalid IP (multicast/unspecified)")
}
if n.udp == 0 {
return errors.New("missing UDP port")
}
// Validate the node key (on curve, etc.).
var key Secp256k1
return n.Load(&key)

View file

@ -21,6 +21,7 @@ import (
"encoding/hex"
"fmt"
"math/big"
"net/netip"
"testing"
"testing/quick"
@ -64,6 +65,167 @@ func TestPythonInterop(t *testing.T) {
}
}
func TestNodeEndpoints(t *testing.T) {
id := HexID("00000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc")
type endpointTest struct {
name string
node *Node
wantIP netip.Addr
wantUDP int
wantTCP int
}
tests := []endpointTest{
{
name: "no-addr",
node: func() *Node {
var r enr.Record
return SignNull(&r, id)
}(),
},
{
name: "udp-only",
node: func() *Node {
var r enr.Record
r.Set(enr.UDP(9000))
return SignNull(&r, id)
}(),
},
{
name: "tcp-only",
node: func() *Node {
var r enr.Record
r.Set(enr.TCP(9000))
return SignNull(&r, id)
}(),
},
{
name: "ipv4-only-loopback",
node: func() *Node {
var r enr.Record
r.Set(enr.IPv4Addr(netip.MustParseAddr("127.0.0.1")))
return SignNull(&r, id)
}(),
wantIP: netip.MustParseAddr("127.0.0.1"),
},
{
name: "ipv4-only-unspecified",
node: func() *Node {
var r enr.Record
r.Set(enr.IPv4Addr(netip.MustParseAddr("0.0.0.0")))
return SignNull(&r, id)
}(),
wantIP: netip.MustParseAddr("0.0.0.0"),
},
{
name: "ipv4-only",
node: func() *Node {
var r enr.Record
r.Set(enr.IPv4Addr(netip.MustParseAddr("99.22.33.1")))
return SignNull(&r, id)
}(),
wantIP: netip.MustParseAddr("99.22.33.1"),
},
{
name: "ipv6-only",
node: func() *Node {
var r enr.Record
r.Set(enr.IPv6Addr(netip.MustParseAddr("2001::ff00:0042:8329")))
return SignNull(&r, id)
}(),
wantIP: netip.MustParseAddr("2001::ff00:0042:8329"),
},
{
name: "ipv4-loopback-and-ipv6-global",
node: func() *Node {
var r enr.Record
r.Set(enr.IPv4Addr(netip.MustParseAddr("127.0.0.1")))
r.Set(enr.UDP(30304))
r.Set(enr.IPv6Addr(netip.MustParseAddr("2001::ff00:0042:8329")))
r.Set(enr.UDP6(30306))
return SignNull(&r, id)
}(),
wantIP: netip.MustParseAddr("2001::ff00:0042:8329"),
wantUDP: 30306,
},
{
name: "ipv4-unspecified-and-ipv6-loopback",
node: func() *Node {
var r enr.Record
r.Set(enr.IPv4Addr(netip.MustParseAddr("0.0.0.0")))
r.Set(enr.IPv6Addr(netip.MustParseAddr("::1")))
return SignNull(&r, id)
}(),
wantIP: netip.MustParseAddr("::1"),
},
{
name: "ipv4-private-and-ipv6-global",
node: func() *Node {
var r enr.Record
r.Set(enr.IPv4Addr(netip.MustParseAddr("192.168.2.2")))
r.Set(enr.UDP(30304))
r.Set(enr.IPv6Addr(netip.MustParseAddr("2001::ff00:0042:8329")))
r.Set(enr.UDP6(30306))
return SignNull(&r, id)
}(),
wantIP: netip.MustParseAddr("2001::ff00:0042:8329"),
wantUDP: 30306,
},
{
name: "ipv4-local-and-ipv6-global",
node: func() *Node {
var r enr.Record
r.Set(enr.IPv4Addr(netip.MustParseAddr("169.254.2.6")))
r.Set(enr.UDP(30304))
r.Set(enr.IPv6Addr(netip.MustParseAddr("2001::ff00:0042:8329")))
r.Set(enr.UDP6(30306))
return SignNull(&r, id)
}(),
wantIP: netip.MustParseAddr("2001::ff00:0042:8329"),
wantUDP: 30306,
},
{
name: "ipv4-private-and-ipv6-private",
node: func() *Node {
var r enr.Record
r.Set(enr.IPv4Addr(netip.MustParseAddr("192.168.2.2")))
r.Set(enr.UDP(30304))
r.Set(enr.IPv6Addr(netip.MustParseAddr("fd00::abcd:1")))
r.Set(enr.UDP6(30306))
return SignNull(&r, id)
}(),
wantIP: netip.MustParseAddr("192.168.2.2"),
wantUDP: 30304,
},
{
name: "ipv4-private-and-ipv6-link-local",
node: func() *Node {
var r enr.Record
r.Set(enr.IPv4Addr(netip.MustParseAddr("192.168.2.2")))
r.Set(enr.UDP(30304))
r.Set(enr.IPv6Addr(netip.MustParseAddr("fe80::1")))
r.Set(enr.UDP6(30306))
return SignNull(&r, id)
}(),
wantIP: netip.MustParseAddr("192.168.2.2"),
wantUDP: 30304,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.wantIP != test.node.IPAddr() {
t.Errorf("node has wrong IP %v, want %v", test.node.IPAddr(), test.wantIP)
}
if test.wantUDP != test.node.UDP() {
t.Errorf("node has wrong UDP port %d, want %d", test.node.UDP(), test.wantUDP)
}
if test.wantTCP != test.node.TCP() {
t.Errorf("node has wrong TCP port %d, want %d", test.node.TCP(), test.wantTCP)
}
})
}
}
func TestHexID(t *testing.T) {
ref := ID{0, 0, 0, 0, 0, 0, 0, 128, 106, 217, 182, 31, 165, 174, 1, 67, 7, 235, 220, 150, 66, 83, 173, 205, 159, 44, 10, 57, 42, 161, 26, 188}
id1 := HexID("0x00000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc")

View file

@ -26,6 +26,7 @@ import (
"sync"
"time"
"github.com/ethereum/go-ethereum/p2p/enr"
"github.com/ethereum/go-ethereum/rlp"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/errors"
@ -242,13 +243,14 @@ func (db *DB) Node(id ID) *Node {
}
func mustDecodeNode(id, data []byte) *Node {
node := new(Node)
if err := rlp.DecodeBytes(data, &node.r); err != nil {
var r enr.Record
if err := rlp.DecodeBytes(data, &r); err != nil {
panic(fmt.Errorf("p2p/enode: can't decode node %x in DB: %v", id, err))
}
// Restore node id cache.
copy(node.id[:], id)
return node
if len(id) != len(ID{}) {
panic(fmt.Errorf("invalid id length %d", len(id)))
}
return newNodeWithID(&r, ID(id))
}
// UpdateNode inserts - potentially overwriting - a node into the peer database.

View file

@ -181,7 +181,7 @@ func (n *Node) URLv4() string {
nodeid = fmt.Sprintf("%s.%x", scheme, n.id[:])
}
u := url.URL{Scheme: "enode"}
if n.Incomplete() {
if !n.ip.IsValid() {
u.Host = nodeid
} else {
addr := net.TCPAddr{IP: n.IP(), Port: n.TCP()}

View file

@ -21,6 +21,7 @@ import (
"fmt"
"io"
"net"
"net/netip"
"github.com/ethereum/go-ethereum/rlp"
)
@ -167,6 +168,60 @@ func (v *IPv6) DecodeRLP(s *rlp.Stream) error {
return nil
}
// IPv4Addr is the "ip" key, which holds the IP address of the node.
type IPv4Addr netip.Addr
func (v IPv4Addr) ENRKey() string { return "ip" }
// EncodeRLP implements rlp.Encoder.
func (v IPv4Addr) EncodeRLP(w io.Writer) error {
addr := netip.Addr(v)
if !addr.Is4() {
return fmt.Errorf("address is not IPv4")
}
enc := rlp.NewEncoderBuffer(w)
bytes := addr.As4()
enc.WriteBytes(bytes[:])
return enc.Flush()
}
// DecodeRLP implements rlp.Decoder.
func (v *IPv4Addr) DecodeRLP(s *rlp.Stream) error {
var bytes [4]byte
if err := s.ReadBytes(bytes[:]); err != nil {
return err
}
*v = IPv4Addr(netip.AddrFrom4(bytes))
return nil
}
// IPv6Addr is the "ip6" key, which holds the IP address of the node.
type IPv6Addr netip.Addr
func (v IPv6Addr) ENRKey() string { return "ip6" }
// EncodeRLP implements rlp.Encoder.
func (v IPv6Addr) EncodeRLP(w io.Writer) error {
addr := netip.Addr(v)
if !addr.Is6() {
return fmt.Errorf("address is not IPv6")
}
enc := rlp.NewEncoderBuffer(w)
bytes := addr.As16()
enc.WriteBytes(bytes[:])
return enc.Flush()
}
// DecodeRLP implements rlp.Decoder.
func (v *IPv6Addr) DecodeRLP(s *rlp.Stream) error {
var bytes [16]byte
if err := s.ReadBytes(bytes[:]); err != nil {
return err
}
*v = IPv6Addr(netip.AddrFrom16(bytes))
return nil
}
// KeyError is an error related to a key.
type KeyError struct {
Key string

View file

@ -190,8 +190,8 @@ type Server struct {
nodedb *enode.DB
localnode *enode.LocalNode
ntab *discover.UDPv4
DiscV5 *discover.UDPv5
discv4 *discover.UDPv4
discv5 *discover.UDPv5
discmix *enode.FairMix
dialsched *dialScheduler
@ -400,6 +400,16 @@ func (srv *Server) Self() *enode.Node {
return ln.Node()
}
// DiscoveryV4 returns the discovery v4 instance, if configured.
func (srv *Server) DiscoveryV4() *discover.UDPv4 {
return srv.discv4
}
// DiscoveryV5 returns the discovery v5 instance, if configured.
func (srv *Server) DiscoveryV5() *discover.UDPv5 {
return srv.discv5
}
// Stop terminates the server and all active peer connections.
// It blocks until all active connections have been closed.
func (srv *Server) Stop() {
@ -547,13 +557,13 @@ func (srv *Server) setupDiscovery() error {
)
// If both versions of discovery are running, setup a shared
// connection, so v5 can read unhandled messages from v4.
if srv.DiscoveryV4 && srv.DiscoveryV5 {
if srv.Config.DiscoveryV4 && srv.Config.DiscoveryV5 {
unhandled = make(chan discover.ReadPacket, 100)
sconn = &sharedUDPConn{conn, unhandled}
}
// Start discovery services.
if srv.DiscoveryV4 {
if srv.Config.DiscoveryV4 {
cfg := discover.Config{
PrivateKey: srv.PrivateKey,
NetRestrict: srv.NetRestrict,
@ -565,17 +575,17 @@ func (srv *Server) setupDiscovery() error {
if err != nil {
return err
}
srv.ntab = ntab
srv.discv4 = ntab
srv.discmix.AddSource(ntab.RandomNodes())
}
if srv.DiscoveryV5 {
if srv.Config.DiscoveryV5 {
cfg := discover.Config{
PrivateKey: srv.PrivateKey,
NetRestrict: srv.NetRestrict,
Bootnodes: srv.BootstrapNodesV5,
Log: srv.log,
}
srv.DiscV5, err = discover.ListenV5(sconn, srv.localnode, cfg)
srv.discv5, err = discover.ListenV5(sconn, srv.localnode, cfg)
if err != nil {
return err
}
@ -602,8 +612,8 @@ func (srv *Server) setupDialScheduler() {
dialer: srv.Dialer,
clock: srv.clock,
}
if srv.ntab != nil {
config.resolver = srv.ntab
if srv.discv4 != nil {
config.resolver = srv.discv4
}
if config.dialer == nil {
config.dialer = tcpDialer{&net.Dialer{Timeout: defaultDialTimeout}}
@ -799,11 +809,11 @@ running:
srv.log.Trace("P2P networking is spinning down")
// Terminate discovery. If there is a running lookup it will terminate soon.
if srv.ntab != nil {
srv.ntab.Close()
if srv.discv4 != nil {
srv.discv4.Close()
}
if srv.DiscV5 != nil {
srv.DiscV5.Close()
if srv.discv5 != nil {
srv.discv5.Close()
}
// Disconnect all peers.
for _, p := range peers {

View file

@ -187,6 +187,10 @@ var (
// BeaconRootsAddress is the address where historical beacon roots are stored as per EIP-4788
BeaconRootsAddress = common.HexToAddress("0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02")
// BeaconRootsCode is the code where historical beacon roots are stored as per EIP-4788
BeaconRootsCode = common.FromHex("3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500")
// SystemAddress is where the system-transaction is sent from as per EIP-4788
SystemAddress = common.HexToAddress("0xfffffffffffffffffffffffffffffffffffffffe")
)

View file

@ -22,6 +22,7 @@ import (
"sync"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/common/prque"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
@ -149,15 +150,42 @@ type CodeSyncResult struct {
// nodeOp represents an operation upon the trie node. It can either represent a
// deletion to the specific node or a node write for persisting retrieved node.
type nodeOp struct {
del bool // flag if op stands for a delete operation
owner common.Hash // identifier of the trie (empty for account trie)
path []byte // path from the root to the specified node.
blob []byte // the content of the node (nil for deletion)
hash common.Hash // hash of the node content (empty for node deletion)
}
// isDelete indicates if the operation is a database deletion.
func (op *nodeOp) isDelete() bool {
return len(op.blob) == 0
// valid checks whether the node operation is valid.
func (op *nodeOp) valid() bool {
if op.del && len(op.blob) != 0 {
return false
}
if !op.del && len(op.blob) == 0 {
return false
}
return true
}
// string returns the node operation in string representation.
func (op *nodeOp) string() string {
var node string
if op.owner == (common.Hash{}) {
node = fmt.Sprintf("node: (%v)", op.path)
} else {
node = fmt.Sprintf("node: (%x-%v)", op.owner, op.path)
}
var blobHex string
if len(op.blob) == 0 {
blobHex = "nil"
} else {
blobHex = hexutil.Encode(op.blob)
}
if op.del {
return fmt.Sprintf("del %s %s %s", node, blobHex, op.hash.Hex())
}
return fmt.Sprintf("write %s %s %s", node, blobHex, op.hash.Hex())
}
// syncMemBatch is an in-memory buffer of successfully downloaded but not yet
@ -220,6 +248,7 @@ func (batch *syncMemBatch) delNode(owner common.Hash, path []byte) {
batch.size += common.HashLength + uint64(len(path))
}
batch.nodes = append(batch.nodes, nodeOp{
del: true,
owner: owner,
path: path,
})
@ -428,7 +457,10 @@ func (s *Sync) Commit(dbw ethdb.Batch) error {
storage int
)
for _, op := range s.membatch.nodes {
if op.isDelete() {
if !op.valid() {
return fmt.Errorf("invalid op, %s", op.string())
}
if op.del {
// node deletion is only supported in path mode.
if op.owner == (common.Hash{}) {
rawdb.DeleteAccountTrieNode(dbw, op.path)

View file

@ -190,7 +190,7 @@ func (db *Database) repairHistory() error {
// all of them. Fix the tests first.
return nil
}
freezer, err := rawdb.NewStateFreezer(ancient, false)
freezer, err := rawdb.NewStateFreezer(ancient, db.readOnly)
if err != nil {
log.Crit("Failed to open state history freezer", "err", err)
}