mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-19 18:32:23 +00:00
Merge branch 'ethereum:master' into rlp-review
This commit is contained in:
commit
642f6c11ab
43 changed files with 1759 additions and 927 deletions
4
.github/workflows/go.yml
vendored
4
.github/workflows/go.yml
vendored
|
|
@ -11,9 +11,9 @@ jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: self-hosted
|
runs-on: self-hosted
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.21.4
|
go-version: 1.21.4
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
|
|
|
||||||
24
Makefile
24
Makefile
|
|
@ -2,31 +2,35 @@
|
||||||
# with Go source code. If you know what GOPATH is then you probably
|
# with Go source code. If you know what GOPATH is then you probably
|
||||||
# don't need to bother with make.
|
# 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
|
GOBIN = ./build/bin
|
||||||
GO ?= latest
|
GO ?= latest
|
||||||
GORUN = go run
|
GORUN = go run
|
||||||
|
|
||||||
#? geth: Build geth
|
#? geth: Build geth.
|
||||||
geth:
|
geth:
|
||||||
$(GORUN) build/ci.go install ./cmd/geth
|
$(GORUN) build/ci.go install ./cmd/geth
|
||||||
@echo "Done building."
|
@echo "Done building."
|
||||||
@echo "Run \"$(GOBIN)/geth\" to launch geth."
|
@echo "Run \"$(GOBIN)/geth\" to launch geth."
|
||||||
|
|
||||||
#? all: Build all packages and executables
|
#? all: Build all packages and executables.
|
||||||
all:
|
all:
|
||||||
$(GORUN) build/ci.go install
|
$(GORUN) build/ci.go install
|
||||||
|
|
||||||
#? test: Run the tests
|
#? test: Run the tests.
|
||||||
test: all
|
test: all
|
||||||
$(GORUN) build/ci.go test
|
$(GORUN) build/ci.go test
|
||||||
|
|
||||||
#? lint: Run certain pre-selected linters
|
#? lint: Run certain pre-selected linters.
|
||||||
lint: ## Run linters.
|
lint: ## Run linters.
|
||||||
$(GORUN) build/ci.go lint
|
$(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:
|
clean:
|
||||||
go clean -cache
|
go clean -cache
|
||||||
rm -fr build/_workspace/pkg/ $(GOBIN)/*
|
rm -fr build/_workspace/pkg/ $(GOBIN)/*
|
||||||
|
|
@ -34,7 +38,7 @@ clean:
|
||||||
# The devtools target installs tools required for 'go generate'.
|
# The devtools target installs tools required for 'go generate'.
|
||||||
# You need to put $GOBIN (or $GOPATH/bin) in your PATH to use '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:
|
devtools:
|
||||||
env GOBIN= go install golang.org/x/tools/cmd/stringer@latest
|
env GOBIN= go install golang.org/x/tools/cmd/stringer@latest
|
||||||
env GOBIN= go install github.com/fjl/gencodec@latest
|
env GOBIN= go install github.com/fjl/gencodec@latest
|
||||||
|
|
@ -45,5 +49,9 @@ devtools:
|
||||||
|
|
||||||
#? help: Get more info on make commands.
|
#? help: Get more info on make commands.
|
||||||
help: Makefile
|
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/^/ /'
|
@sed -n 's/^#?//p' $< | column -t -s ':' | sort | sed -e 's/^/ /'
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -28,9 +29,11 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
"github.com/ethereum/go-ethereum/internal/flags"
|
"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/discover"
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||||
"github.com/ethereum/go-ethereum/params"
|
"github.com/ethereum/go-ethereum/params"
|
||||||
|
"github.com/ethereum/go-ethereum/rpc"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -45,6 +48,7 @@ var (
|
||||||
discv4ResolveJSONCommand,
|
discv4ResolveJSONCommand,
|
||||||
discv4CrawlCommand,
|
discv4CrawlCommand,
|
||||||
discv4TestCommand,
|
discv4TestCommand,
|
||||||
|
discv4ListenCommand,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
discv4PingCommand = &cli.Command{
|
discv4PingCommand = &cli.Command{
|
||||||
|
|
@ -75,6 +79,14 @@ var (
|
||||||
Flags: discoveryNodeFlags,
|
Flags: discoveryNodeFlags,
|
||||||
ArgsUsage: "<nodes.json file>",
|
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{
|
discv4CrawlCommand = &cli.Command{
|
||||||
Name: "crawl",
|
Name: "crawl",
|
||||||
Usage: "Updates a nodes.json file with random nodes found in the DHT",
|
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",
|
Usage: "Enode of the remote node under test",
|
||||||
EnvVars: []string{"REMOTE_ENODE"},
|
EnvVars: []string{"REMOTE_ENODE"},
|
||||||
}
|
}
|
||||||
|
httpAddrFlag = &cli.StringFlag{
|
||||||
|
Name: "rpc",
|
||||||
|
Usage: "HTTP server listening address",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
var discoveryNodeFlags = []cli.Flag{
|
var discoveryNodeFlags = []cli.Flag{
|
||||||
|
|
@ -154,6 +170,27 @@ func discv4Ping(ctx *cli.Context) error {
|
||||||
return nil
|
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 {
|
func discv4RequestRecord(ctx *cli.Context) error {
|
||||||
n := getNodeArg(ctx)
|
n := getNodeArg(ctx)
|
||||||
disc, _ := startV4(ctx)
|
disc, _ := startV4(ctx)
|
||||||
|
|
@ -362,3 +399,23 @@ func parseBootnodes(ctx *cli.Context) ([]*enode.Node, error) {
|
||||||
}
|
}
|
||||||
return nodes, nil
|
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()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -234,7 +234,7 @@ func TestT8n(t *testing.T) {
|
||||||
{ // Test post-merge transition
|
{ // Test post-merge transition
|
||||||
base: "./testdata/24",
|
base: "./testdata/24",
|
||||||
input: t8nInput{
|
input: t8nInput{
|
||||||
"alloc.json", "txs.json", "env.json", "Merge", "",
|
"alloc.json", "txs.json", "env.json", "Paris", "",
|
||||||
},
|
},
|
||||||
output: t8nOutput{alloc: true, result: true},
|
output: t8nOutput{alloc: true, result: true},
|
||||||
expOut: "exp.json",
|
expOut: "exp.json",
|
||||||
|
|
@ -242,7 +242,7 @@ func TestT8n(t *testing.T) {
|
||||||
{ // Test post-merge transition where input is missing random
|
{ // Test post-merge transition where input is missing random
|
||||||
base: "./testdata/24",
|
base: "./testdata/24",
|
||||||
input: t8nInput{
|
input: t8nInput{
|
||||||
"alloc.json", "txs.json", "env-missingrandom.json", "Merge", "",
|
"alloc.json", "txs.json", "env-missingrandom.json", "Paris", "",
|
||||||
},
|
},
|
||||||
output: t8nOutput{alloc: false, result: false},
|
output: t8nOutput{alloc: false, result: false},
|
||||||
expExitCode: 3,
|
expExitCode: 3,
|
||||||
|
|
@ -250,7 +250,7 @@ func TestT8n(t *testing.T) {
|
||||||
{ // Test base fee calculation
|
{ // Test base fee calculation
|
||||||
base: "./testdata/25",
|
base: "./testdata/25",
|
||||||
input: t8nInput{
|
input: t8nInput{
|
||||||
"alloc.json", "txs.json", "env.json", "Merge", "",
|
"alloc.json", "txs.json", "env.json", "Paris", "",
|
||||||
},
|
},
|
||||||
output: t8nOutput{alloc: true, result: true},
|
output: t8nOutput{alloc: true, result: true},
|
||||||
expOut: "exp.json",
|
expOut: "exp.json",
|
||||||
|
|
@ -378,7 +378,7 @@ func TestT8nTracing(t *testing.T) {
|
||||||
{
|
{
|
||||||
base: "./testdata/32",
|
base: "./testdata/32",
|
||||||
input: t8nInput{
|
input: t8nInput{
|
||||||
"alloc.json", "txs.json", "env.json", "Merge", "",
|
"alloc.json", "txs.json", "env.json", "Paris", "",
|
||||||
},
|
},
|
||||||
extraArgs: []string{"--trace", "--trace.callframes"},
|
extraArgs: []string{"--trace", "--trace.callframes"},
|
||||||
expectedTraces: []string{"trace-0-0x47806361c0fa084be3caa18afe8c48156747c01dbdfc1ee11b5aecdbe4fcf23e.jsonl"},
|
expectedTraces: []string{"trace-0-0x47806361c0fa084be3caa18afe8c48156747c01dbdfc1ee11b5aecdbe4fcf23e.jsonl"},
|
||||||
|
|
|
||||||
|
|
@ -1805,8 +1805,12 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error)
|
||||||
}
|
}
|
||||||
statedb.SetLogger(bc.logger)
|
statedb.SetLogger(bc.logger)
|
||||||
|
|
||||||
// Enable prefetching to pull in trie node paths while processing transactions
|
// If we are past Byzantium, enable prefetching to pull in trie node paths
|
||||||
|
// while processing transactions. Before Byzantium the prefetcher is mostly
|
||||||
|
// useless due to the intermediate root hashing after each transaction.
|
||||||
|
if bc.chainConfig.IsByzantium(block.Number()) {
|
||||||
statedb.StartPrefetcher("chain")
|
statedb.StartPrefetcher("chain")
|
||||||
|
}
|
||||||
activeState = statedb
|
activeState = statedb
|
||||||
|
|
||||||
// If we have a followup block, run that against the current state to pre-cache
|
// If we have a followup block, run that against the current state to pre-cache
|
||||||
|
|
|
||||||
|
|
@ -43,12 +43,11 @@ func TestGeneratePOSChain(t *testing.T) {
|
||||||
bb = common.Address{0xbb}
|
bb = common.Address{0xbb}
|
||||||
funds = big.NewInt(0).Mul(big.NewInt(1337), big.NewInt(params.Ether))
|
funds = big.NewInt(0).Mul(big.NewInt(1337), big.NewInt(params.Ether))
|
||||||
config = *params.AllEthashProtocolChanges
|
config = *params.AllEthashProtocolChanges
|
||||||
asm4788 = common.Hex2Bytes("3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500")
|
|
||||||
gspec = &Genesis{
|
gspec = &Genesis{
|
||||||
Config: &config,
|
Config: &config,
|
||||||
Alloc: types.GenesisAlloc{
|
Alloc: types.GenesisAlloc{
|
||||||
address: {Balance: funds},
|
address: {Balance: funds},
|
||||||
params.BeaconRootsAddress: {Balance: common.Big0, Code: asm4788},
|
params.BeaconRootsAddress: {Code: params.BeaconRootsCode},
|
||||||
},
|
},
|
||||||
BaseFee: big.NewInt(params.InitialBaseFee),
|
BaseFee: big.NewInt(params.InitialBaseFee),
|
||||||
Difficulty: common.Big1,
|
Difficulty: common.Big1,
|
||||||
|
|
|
||||||
|
|
@ -593,6 +593,8 @@ func DeveloperGenesisBlock(gasLimit uint64, faucet *common.Address) *Genesis {
|
||||||
common.BytesToAddress([]byte{7}): {Balance: big.NewInt(1)}, // ECScalarMul
|
common.BytesToAddress([]byte{7}): {Balance: big.NewInt(1)}, // ECScalarMul
|
||||||
common.BytesToAddress([]byte{8}): {Balance: big.NewInt(1)}, // ECPairing
|
common.BytesToAddress([]byte{8}): {Balance: big.NewInt(1)}, // ECPairing
|
||||||
common.BytesToAddress([]byte{9}): {Balance: big.NewInt(1)}, // BLAKE2b
|
common.BytesToAddress([]byte{9}): {Balance: big.NewInt(1)}, // BLAKE2b
|
||||||
|
// Pre-deploy EIP-4788 system contract
|
||||||
|
params.BeaconRootsAddress: types.Account{Nonce: 1, Code: params.BeaconRootsCode},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if faucet != nil {
|
if faucet != nil {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"maps"
|
"maps"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
|
@ -33,6 +34,14 @@ import (
|
||||||
"github.com/holiman/uint256"
|
"github.com/holiman/uint256"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// hasherPool holds a pool of hashers used by state objects during concurrent
|
||||||
|
// trie updates.
|
||||||
|
var hasherPool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return crypto.NewKeccakState()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
type Storage map[common.Hash]common.Hash
|
type Storage map[common.Hash]common.Hash
|
||||||
|
|
||||||
func (s Storage) Copy() Storage {
|
func (s Storage) Copy() Storage {
|
||||||
|
|
@ -118,16 +127,12 @@ func (s *stateObject) touch() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getTrie returns the associated storage trie. The trie will be opened
|
// getTrie returns the associated storage trie. The trie will be opened if it'
|
||||||
// if it's not loaded previously. An error will be returned if trie can't
|
// not loaded previously. An error will be returned if trie can't be loaded.
|
||||||
// be loaded.
|
//
|
||||||
|
// If a new trie is opened, it will be cached within the state object to allow
|
||||||
|
// subsequent reads to expand the same trie instead of reloading from disk.
|
||||||
func (s *stateObject) getTrie() (Trie, error) {
|
func (s *stateObject) getTrie() (Trie, error) {
|
||||||
if s.trie == nil {
|
|
||||||
// Try fetching from prefetcher first
|
|
||||||
if s.data.Root != types.EmptyRootHash && s.db.prefetcher != nil {
|
|
||||||
// When the miner is creating the pending state, there is no prefetcher
|
|
||||||
s.trie = s.db.prefetcher.trie(s.addrHash, s.data.Root)
|
|
||||||
}
|
|
||||||
if s.trie == nil {
|
if s.trie == nil {
|
||||||
tr, err := s.db.db.OpenStorageTrie(s.db.originalRoot, s.address, s.data.Root, s.db.trie)
|
tr, err := s.db.db.OpenStorageTrie(s.db.originalRoot, s.address, s.data.Root, s.db.trie)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -135,10 +140,26 @@ func (s *stateObject) getTrie() (Trie, error) {
|
||||||
}
|
}
|
||||||
s.trie = tr
|
s.trie = tr
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return s.trie, nil
|
return s.trie, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getPrefetchedTrie returns the associated trie, as populated by the prefetcher
|
||||||
|
// if it's available.
|
||||||
|
//
|
||||||
|
// Note, opposed to getTrie, this method will *NOT* blindly cache the resulting
|
||||||
|
// trie in the state object. The caller might want to do that, but it's cleaner
|
||||||
|
// to break the hidden interdependency between retrieving tries from the db or
|
||||||
|
// from the prefetcher.
|
||||||
|
func (s *stateObject) getPrefetchedTrie() (Trie, error) {
|
||||||
|
// If there's nothing to meaningfully return, let the user figure it out by
|
||||||
|
// pulling the trie from disk.
|
||||||
|
if s.data.Root == types.EmptyRootHash || s.db.prefetcher == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
// Attempt to retrieve the trie from the pretecher
|
||||||
|
return s.db.prefetcher.trie(s.addrHash, s.data.Root)
|
||||||
|
}
|
||||||
|
|
||||||
// GetState retrieves a value from the account storage trie.
|
// GetState retrieves a value from the account storage trie.
|
||||||
func (s *stateObject) GetState(key common.Hash) common.Hash {
|
func (s *stateObject) GetState(key common.Hash) common.Hash {
|
||||||
value, _ := s.getState(key)
|
value, _ := s.getState(key)
|
||||||
|
|
@ -248,7 +269,7 @@ func (s *stateObject) setState(key common.Hash, value common.Hash, origin common
|
||||||
|
|
||||||
// finalise moves all dirty storage slots into the pending area to be hashed or
|
// finalise moves all dirty storage slots into the pending area to be hashed or
|
||||||
// committed later. It is invoked at the end of every transaction.
|
// committed later. It is invoked at the end of every transaction.
|
||||||
func (s *stateObject) finalise(prefetch bool) {
|
func (s *stateObject) finalise() {
|
||||||
slotsToPrefetch := make([][]byte, 0, len(s.dirtyStorage))
|
slotsToPrefetch := make([][]byte, 0, len(s.dirtyStorage))
|
||||||
for key, value := range s.dirtyStorage {
|
for key, value := range s.dirtyStorage {
|
||||||
// If the slot is different from its original value, move it into the
|
// If the slot is different from its original value, move it into the
|
||||||
|
|
@ -263,8 +284,10 @@ func (s *stateObject) finalise(prefetch bool) {
|
||||||
delete(s.pendingStorage, key)
|
delete(s.pendingStorage, key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if s.db.prefetcher != nil && prefetch && len(slotsToPrefetch) > 0 && s.data.Root != types.EmptyRootHash {
|
if s.db.prefetcher != nil && len(slotsToPrefetch) > 0 && s.data.Root != types.EmptyRootHash {
|
||||||
s.db.prefetcher.prefetch(s.addrHash, s.data.Root, s.address, slotsToPrefetch)
|
if err := s.db.prefetcher.prefetch(s.addrHash, s.data.Root, s.address, slotsToPrefetch); err != nil {
|
||||||
|
log.Error("Failed to prefetch slots", "addr", s.address, "slots", len(slotsToPrefetch), "err", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if len(s.dirtyStorage) > 0 {
|
if len(s.dirtyStorage) > 0 {
|
||||||
s.dirtyStorage = make(Storage)
|
s.dirtyStorage = make(Storage)
|
||||||
|
|
@ -281,27 +304,44 @@ func (s *stateObject) finalise(prefetch bool) {
|
||||||
// loading or updating of the trie, an error will be returned. Furthermore,
|
// 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
|
// this function will return the mutated storage trie, or nil if there is no
|
||||||
// storage change at all.
|
// storage change at all.
|
||||||
|
//
|
||||||
|
// It assumes all the dirty storage slots have been finalized before.
|
||||||
func (s *stateObject) updateTrie() (Trie, error) {
|
func (s *stateObject) updateTrie() (Trie, error) {
|
||||||
// Make sure all dirty slots are finalized into the pending storage area
|
|
||||||
s.finalise(false)
|
|
||||||
|
|
||||||
// Short circuit if nothing changed, don't bother with hashing anything
|
// Short circuit if nothing changed, don't bother with hashing anything
|
||||||
if len(s.pendingStorage) == 0 {
|
if len(s.pendingStorage) == 0 {
|
||||||
return s.trie, nil
|
return s.trie, nil
|
||||||
}
|
}
|
||||||
|
// Retrieve a pretecher populated trie, or fall back to the database
|
||||||
|
tr, err := s.getPrefetchedTrie()
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
// Fetcher retrieval failed, something's very wrong, abort
|
||||||
|
s.db.setError(err)
|
||||||
|
return nil, err
|
||||||
|
|
||||||
|
case tr == nil:
|
||||||
|
// Fetcher not running or empty trie, fallback to the database trie
|
||||||
|
tr, err = s.getTrie()
|
||||||
|
if err != nil {
|
||||||
|
s.db.setError(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Prefetcher returned a live trie, swap it out for the current one
|
||||||
|
s.trie = tr
|
||||||
|
}
|
||||||
// The snapshot storage map for the object
|
// The snapshot storage map for the object
|
||||||
var (
|
var (
|
||||||
storage map[common.Hash][]byte
|
storage map[common.Hash][]byte
|
||||||
origin map[common.Hash][]byte
|
origin map[common.Hash][]byte
|
||||||
)
|
)
|
||||||
tr, err := s.getTrie()
|
|
||||||
if err != nil {
|
|
||||||
s.db.setError(err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Insert all the pending storage updates into the trie
|
// Insert all the pending storage updates into the trie
|
||||||
usedStorage := make([][]byte, 0, len(s.pendingStorage))
|
usedStorage := make([][]byte, 0, len(s.pendingStorage))
|
||||||
|
|
||||||
|
hasher := hasherPool.Get().(crypto.KeccakState)
|
||||||
|
defer hasherPool.Put(hasher)
|
||||||
|
|
||||||
// Perform trie updates before deletions. This prevents resolution of unnecessary trie nodes
|
// Perform trie updates before deletions. This prevents resolution of unnecessary trie nodes
|
||||||
// in circumstances similar to the following:
|
// in circumstances similar to the following:
|
||||||
//
|
//
|
||||||
|
|
@ -330,26 +370,30 @@ func (s *stateObject) updateTrie() (Trie, error) {
|
||||||
s.db.setError(err)
|
s.db.setError(err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
s.db.StorageUpdated += 1
|
s.db.StorageUpdated.Add(1)
|
||||||
} else {
|
} else {
|
||||||
deletions = append(deletions, key)
|
deletions = append(deletions, key)
|
||||||
}
|
}
|
||||||
// Cache the mutated storage slots until commit
|
// Cache the mutated storage slots until commit
|
||||||
if storage == nil {
|
if storage == nil {
|
||||||
|
s.db.storagesLock.Lock()
|
||||||
if storage = s.db.storages[s.addrHash]; storage == nil {
|
if storage = s.db.storages[s.addrHash]; storage == nil {
|
||||||
storage = make(map[common.Hash][]byte)
|
storage = make(map[common.Hash][]byte)
|
||||||
s.db.storages[s.addrHash] = storage
|
s.db.storages[s.addrHash] = storage
|
||||||
}
|
}
|
||||||
|
s.db.storagesLock.Unlock()
|
||||||
}
|
}
|
||||||
khash := crypto.HashData(s.db.hasher, key[:])
|
khash := crypto.HashData(hasher, key[:])
|
||||||
storage[khash] = encoded // encoded will be nil if it's deleted
|
storage[khash] = encoded // encoded will be nil if it's deleted
|
||||||
|
|
||||||
// Cache the original value of mutated storage slots
|
// Cache the original value of mutated storage slots
|
||||||
if origin == nil {
|
if origin == nil {
|
||||||
|
s.db.storagesLock.Lock()
|
||||||
if origin = s.db.storagesOrigin[s.address]; origin == nil {
|
if origin = s.db.storagesOrigin[s.address]; origin == nil {
|
||||||
origin = make(map[common.Hash][]byte)
|
origin = make(map[common.Hash][]byte)
|
||||||
s.db.storagesOrigin[s.address] = origin
|
s.db.storagesOrigin[s.address] = origin
|
||||||
}
|
}
|
||||||
|
s.db.storagesLock.Unlock()
|
||||||
}
|
}
|
||||||
// Track the original value of slot only if it's mutated first time
|
// Track the original value of slot only if it's mutated first time
|
||||||
if _, ok := origin[khash]; !ok {
|
if _, ok := origin[khash]; !ok {
|
||||||
|
|
@ -369,7 +413,7 @@ func (s *stateObject) updateTrie() (Trie, error) {
|
||||||
s.db.setError(err)
|
s.db.setError(err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
s.db.StorageDeleted += 1
|
s.db.StorageDeleted.Add(1)
|
||||||
}
|
}
|
||||||
// If no slots were touched, issue a warning as we shouldn't have done all
|
// If no slots were touched, issue a warning as we shouldn't have done all
|
||||||
// the above work in the first place
|
// the above work in the first place
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
|
@ -97,9 +98,11 @@ type StateDB struct {
|
||||||
// These maps hold the state changes (including the corresponding
|
// These maps hold the state changes (including the corresponding
|
||||||
// original value) that occurred in this **block**.
|
// original value) that occurred in this **block**.
|
||||||
accounts map[common.Hash][]byte // The mutated accounts in 'slim RLP' encoding
|
accounts map[common.Hash][]byte // The mutated accounts in 'slim RLP' encoding
|
||||||
storages map[common.Hash]map[common.Hash][]byte // The mutated slots in prefix-zero trimmed rlp format
|
|
||||||
accountsOrigin map[common.Address][]byte // The original value of mutated accounts in 'slim RLP' encoding
|
accountsOrigin map[common.Address][]byte // The original value of mutated accounts in 'slim RLP' encoding
|
||||||
|
|
||||||
|
storages map[common.Hash]map[common.Hash][]byte // The mutated slots in prefix-zero trimmed rlp format
|
||||||
storagesOrigin map[common.Address]map[common.Hash][]byte // The original value of mutated slots in prefix-zero trimmed rlp format
|
storagesOrigin map[common.Address]map[common.Hash][]byte // The original value of mutated slots in prefix-zero trimmed rlp format
|
||||||
|
storagesLock sync.Mutex // Mutex protecting the maps during concurrent updates/commits
|
||||||
|
|
||||||
// This map holds 'live' objects, which will get modified while
|
// This map holds 'live' objects, which will get modified while
|
||||||
// processing a state transition.
|
// processing a state transition.
|
||||||
|
|
@ -165,9 +168,9 @@ type StateDB struct {
|
||||||
TrieDBCommits time.Duration
|
TrieDBCommits time.Duration
|
||||||
|
|
||||||
AccountUpdated int
|
AccountUpdated int
|
||||||
StorageUpdated int
|
StorageUpdated atomic.Int64
|
||||||
AccountDeleted int
|
AccountDeleted int
|
||||||
StorageDeleted int
|
StorageDeleted atomic.Int64
|
||||||
|
|
||||||
// Testing hooks
|
// Testing hooks
|
||||||
onCommit func(states *triestate.Set) // Hook invoked when commit is performed
|
onCommit func(states *triestate.Set) // Hook invoked when commit is performed
|
||||||
|
|
@ -214,7 +217,8 @@ func (s *StateDB) SetLogger(l *tracing.Hooks) {
|
||||||
// commit phase, most of the needed data is already hot.
|
// commit phase, most of the needed data is already hot.
|
||||||
func (s *StateDB) StartPrefetcher(namespace string) {
|
func (s *StateDB) StartPrefetcher(namespace string) {
|
||||||
if s.prefetcher != nil {
|
if s.prefetcher != nil {
|
||||||
s.prefetcher.close()
|
s.prefetcher.terminate(false)
|
||||||
|
s.prefetcher.report()
|
||||||
s.prefetcher = nil
|
s.prefetcher = nil
|
||||||
}
|
}
|
||||||
if s.snap != nil {
|
if s.snap != nil {
|
||||||
|
|
@ -226,7 +230,8 @@ func (s *StateDB) StartPrefetcher(namespace string) {
|
||||||
// from the gathered metrics.
|
// from the gathered metrics.
|
||||||
func (s *StateDB) StopPrefetcher() {
|
func (s *StateDB) StopPrefetcher() {
|
||||||
if s.prefetcher != nil {
|
if s.prefetcher != nil {
|
||||||
s.prefetcher.close()
|
s.prefetcher.terminate(false)
|
||||||
|
s.prefetcher.report()
|
||||||
s.prefetcher = nil
|
s.prefetcher = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -544,9 +549,6 @@ func (s *StateDB) GetTransientState(addr common.Address, key common.Hash) common
|
||||||
|
|
||||||
// updateStateObject writes the given object to the trie.
|
// updateStateObject writes the given object to the trie.
|
||||||
func (s *StateDB) updateStateObject(obj *stateObject) {
|
func (s *StateDB) updateStateObject(obj *stateObject) {
|
||||||
// Track the amount of time wasted on updating the account from the trie
|
|
||||||
defer func(start time.Time) { s.AccountUpdates += time.Since(start) }(time.Now())
|
|
||||||
|
|
||||||
// Encode the account and update the account trie
|
// Encode the account and update the account trie
|
||||||
addr := obj.Address()
|
addr := obj.Address()
|
||||||
if err := s.trie.UpdateAccount(addr, &obj.data); err != nil {
|
if err := s.trie.UpdateAccount(addr, &obj.data); err != nil {
|
||||||
|
|
@ -575,10 +577,6 @@ func (s *StateDB) updateStateObject(obj *stateObject) {
|
||||||
|
|
||||||
// deleteStateObject removes the given object from the state trie.
|
// deleteStateObject removes the given object from the state trie.
|
||||||
func (s *StateDB) deleteStateObject(addr common.Address) {
|
func (s *StateDB) deleteStateObject(addr common.Address) {
|
||||||
// Track the amount of time wasted on deleting the account from the trie
|
|
||||||
defer func(start time.Time) { s.AccountUpdates += time.Since(start) }(time.Now())
|
|
||||||
|
|
||||||
// Delete the account from the trie
|
|
||||||
if err := s.trie.DeleteAccount(addr); err != nil {
|
if err := s.trie.DeleteAccount(addr); err != nil {
|
||||||
s.setError(fmt.Errorf("deleteStateObject (%x) error: %v", addr[:], err))
|
s.setError(fmt.Errorf("deleteStateObject (%x) error: %v", addr[:], err))
|
||||||
}
|
}
|
||||||
|
|
@ -743,13 +741,6 @@ func (s *StateDB) Copy() *StateDB {
|
||||||
// in the middle of a transaction.
|
// in the middle of a transaction.
|
||||||
state.accessList = s.accessList.Copy()
|
state.accessList = s.accessList.Copy()
|
||||||
state.transientStorage = s.transientStorage.Copy()
|
state.transientStorage = s.transientStorage.Copy()
|
||||||
|
|
||||||
// If there's a prefetcher running, make an inactive copy of it that can
|
|
||||||
// only access data but does not actively preload (since the user will not
|
|
||||||
// know that they need to explicitly terminate an active copy).
|
|
||||||
if s.prefetcher != nil {
|
|
||||||
state.prefetcher = s.prefetcher.copy()
|
|
||||||
}
|
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -820,7 +811,7 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) {
|
||||||
delete(s.accountsOrigin, obj.address) // Clear out any previously updated account data (may be recreated via a resurrect)
|
delete(s.accountsOrigin, obj.address) // Clear out any previously updated account data (may be recreated via a resurrect)
|
||||||
delete(s.storagesOrigin, obj.address) // Clear out any previously updated storage data (may be recreated via a resurrect)
|
delete(s.storagesOrigin, obj.address) // Clear out any previously updated storage data (may be recreated via a resurrect)
|
||||||
} else {
|
} else {
|
||||||
obj.finalise(true) // Prefetch slots in the background
|
obj.finalise()
|
||||||
s.markUpdate(addr)
|
s.markUpdate(addr)
|
||||||
}
|
}
|
||||||
// At this point, also ship the address off to the precacher. The precacher
|
// At this point, also ship the address off to the precacher. The precacher
|
||||||
|
|
@ -829,7 +820,9 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) {
|
||||||
addressesToPrefetch = append(addressesToPrefetch, common.CopyBytes(addr[:])) // Copy needed for closure
|
addressesToPrefetch = append(addressesToPrefetch, common.CopyBytes(addr[:])) // Copy needed for closure
|
||||||
}
|
}
|
||||||
if s.prefetcher != nil && len(addressesToPrefetch) > 0 {
|
if s.prefetcher != nil && len(addressesToPrefetch) > 0 {
|
||||||
s.prefetcher.prefetch(common.Hash{}, s.originalRoot, common.Address{}, addressesToPrefetch)
|
if err := s.prefetcher.prefetch(common.Hash{}, s.originalRoot, common.Address{}, addressesToPrefetch); err != nil {
|
||||||
|
log.Error("Failed to prefetch addresses", "addresses", len(addressesToPrefetch), "err", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Invalidate journal because reverting across transactions is not allowed.
|
// Invalidate journal because reverting across transactions is not allowed.
|
||||||
s.clearJournalAndRefund()
|
s.clearJournalAndRefund()
|
||||||
|
|
@ -842,42 +835,52 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
|
||||||
// Finalise all the dirty storage states and write them into the tries
|
// Finalise all the dirty storage states and write them into the tries
|
||||||
s.Finalise(deleteEmptyObjects)
|
s.Finalise(deleteEmptyObjects)
|
||||||
|
|
||||||
// If there was a trie prefetcher operating, it gets aborted and irrevocably
|
// If there was a trie prefetcher operating, terminate it async so that the
|
||||||
// modified after we start retrieving tries. Remove it from the statedb after
|
// individual storage tries can be updated as soon as the disk load finishes.
|
||||||
// this round of use.
|
|
||||||
//
|
|
||||||
// This is weird pre-byzantium since the first tx runs with a prefetcher and
|
|
||||||
// the remainder without, but pre-byzantium even the initial prefetcher is
|
|
||||||
// useless, so no sleep lost.
|
|
||||||
prefetcher := s.prefetcher
|
|
||||||
if s.prefetcher != nil {
|
if s.prefetcher != nil {
|
||||||
|
s.prefetcher.terminate(true)
|
||||||
defer func() {
|
defer func() {
|
||||||
s.prefetcher.close()
|
s.prefetcher.report()
|
||||||
s.prefetcher = nil
|
s.prefetcher = nil // Pre-byzantium, unset any used up prefetcher
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
// Although naively it makes sense to retrieve the account trie and then do
|
// Process all storage updates concurrently. The state object update root
|
||||||
// the contract storage and account updates sequentially, that short circuits
|
// method will internally call a blocking trie fetch from the prefetcher,
|
||||||
// the account prefetcher. Instead, let's process all the storage updates
|
// so there's no need to explicitly wait for the prefetchers to finish.
|
||||||
// first, giving the account prefetches just a few more milliseconds of time
|
var (
|
||||||
// to pull useful data from disk.
|
start = time.Now()
|
||||||
start := time.Now()
|
workers errgroup.Group
|
||||||
|
)
|
||||||
|
if s.db.TrieDB().IsVerkle() {
|
||||||
|
// Whilst MPT storage tries are independent, Verkle has one single trie
|
||||||
|
// for all the accounts and all the storage slots merged together. The
|
||||||
|
// former can thus be simply parallelized, but updating the latter will
|
||||||
|
// need concurrency support within the trie itself. That's a TODO for a
|
||||||
|
// later time.
|
||||||
|
workers.SetLimit(1)
|
||||||
|
}
|
||||||
for addr, op := range s.mutations {
|
for addr, op := range s.mutations {
|
||||||
if op.applied {
|
if op.applied || op.isDelete() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if op.isDelete() {
|
obj := s.stateObjects[addr] // closure for the task runner below
|
||||||
continue
|
workers.Go(func() error {
|
||||||
}
|
obj.updateRoot()
|
||||||
s.stateObjects[addr].updateRoot()
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
workers.Wait()
|
||||||
s.StorageUpdates += time.Since(start)
|
s.StorageUpdates += time.Since(start)
|
||||||
|
|
||||||
// Now we're about to start to write changes to the trie. The trie is so far
|
// Now we're about to start to write changes to the trie. The trie is so far
|
||||||
// _untouched_. We can check with the prefetcher, if it can give us a trie
|
// _untouched_. We can check with the prefetcher, if it can give us a trie
|
||||||
// which has the same root, but also has some content loaded into it.
|
// which has the same root, but also has some content loaded into it.
|
||||||
if prefetcher != nil {
|
start = time.Now()
|
||||||
if trie := prefetcher.trie(common.Hash{}, s.originalRoot); trie != nil {
|
|
||||||
|
if s.prefetcher != nil {
|
||||||
|
if trie, err := s.prefetcher.trie(common.Hash{}, s.originalRoot); err != nil {
|
||||||
|
log.Error("Failed to retrieve account pre-fetcher trie", "err", err)
|
||||||
|
} else if trie != nil {
|
||||||
s.trie = trie
|
s.trie = trie
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -913,8 +916,10 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
|
||||||
s.deleteStateObject(deletedAddr)
|
s.deleteStateObject(deletedAddr)
|
||||||
s.AccountDeleted += 1
|
s.AccountDeleted += 1
|
||||||
}
|
}
|
||||||
if prefetcher != nil {
|
s.AccountUpdates += time.Since(start)
|
||||||
prefetcher.used(common.Hash{}, s.originalRoot, usedAddrs)
|
|
||||||
|
if s.prefetcher != nil {
|
||||||
|
s.prefetcher.used(common.Hash{}, s.originalRoot, usedAddrs)
|
||||||
}
|
}
|
||||||
// Track the amount of time wasted on hashing the account trie
|
// Track the amount of time wasted on hashing the account trie
|
||||||
defer func(start time.Time) { s.AccountHashes += time.Since(start) }(time.Now())
|
defer func(start time.Time) { s.AccountHashes += time.Since(start) }(time.Now())
|
||||||
|
|
@ -1255,15 +1260,16 @@ func (s *StateDB) Commit(block uint64, deleteEmptyObjects bool) (common.Hash, er
|
||||||
return common.Hash{}, err
|
return common.Hash{}, err
|
||||||
}
|
}
|
||||||
accountUpdatedMeter.Mark(int64(s.AccountUpdated))
|
accountUpdatedMeter.Mark(int64(s.AccountUpdated))
|
||||||
storageUpdatedMeter.Mark(int64(s.StorageUpdated))
|
storageUpdatedMeter.Mark(s.StorageUpdated.Load())
|
||||||
accountDeletedMeter.Mark(int64(s.AccountDeleted))
|
accountDeletedMeter.Mark(int64(s.AccountDeleted))
|
||||||
storageDeletedMeter.Mark(int64(s.StorageDeleted))
|
storageDeletedMeter.Mark(s.StorageDeleted.Load())
|
||||||
accountTrieUpdatedMeter.Mark(int64(accountTrieNodesUpdated))
|
accountTrieUpdatedMeter.Mark(int64(accountTrieNodesUpdated))
|
||||||
accountTrieDeletedMeter.Mark(int64(accountTrieNodesDeleted))
|
accountTrieDeletedMeter.Mark(int64(accountTrieNodesDeleted))
|
||||||
storageTriesUpdatedMeter.Mark(int64(storageTrieNodesUpdated))
|
storageTriesUpdatedMeter.Mark(int64(storageTrieNodesUpdated))
|
||||||
storageTriesDeletedMeter.Mark(int64(storageTrieNodesDeleted))
|
storageTriesDeletedMeter.Mark(int64(storageTrieNodesDeleted))
|
||||||
s.AccountUpdated, s.AccountDeleted = 0, 0
|
s.AccountUpdated, s.AccountDeleted = 0, 0
|
||||||
s.StorageUpdated, s.StorageDeleted = 0, 0
|
s.StorageUpdated.Store(0)
|
||||||
|
s.StorageDeleted.Store(0)
|
||||||
|
|
||||||
// If snapshotting is enabled, update the snapshot tree with this new version
|
// If snapshotting is enabled, update the snapshot tree with this new version
|
||||||
if s.snap != nil {
|
if s.snap != nil {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package state
|
package state
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
|
@ -27,6 +28,10 @@ import (
|
||||||
var (
|
var (
|
||||||
// triePrefetchMetricsPrefix is the prefix under which to publish the metrics.
|
// triePrefetchMetricsPrefix is the prefix under which to publish the metrics.
|
||||||
triePrefetchMetricsPrefix = "trie/prefetch/"
|
triePrefetchMetricsPrefix = "trie/prefetch/"
|
||||||
|
|
||||||
|
// errTerminated is returned if a fetcher is attempted to be operated after it
|
||||||
|
// has already terminated.
|
||||||
|
errTerminated = errors.New("fetcher is already terminated")
|
||||||
)
|
)
|
||||||
|
|
||||||
// triePrefetcher is an active prefetcher, which receives accounts or storage
|
// triePrefetcher is an active prefetcher, which receives accounts or storage
|
||||||
|
|
@ -37,52 +42,64 @@ var (
|
||||||
type triePrefetcher struct {
|
type triePrefetcher struct {
|
||||||
db Database // Database to fetch trie nodes through
|
db Database // Database to fetch trie nodes through
|
||||||
root common.Hash // Root hash of the account trie for metrics
|
root common.Hash // Root hash of the account trie for metrics
|
||||||
fetches map[string]Trie // Partially or fully fetched tries. Only populated for inactive copies.
|
|
||||||
fetchers map[string]*subfetcher // Subfetchers for each trie
|
fetchers map[string]*subfetcher // Subfetchers for each trie
|
||||||
|
term chan struct{} // Channel to signal interruption
|
||||||
|
|
||||||
deliveryMissMeter metrics.Meter
|
deliveryMissMeter metrics.Meter
|
||||||
accountLoadMeter metrics.Meter
|
accountLoadMeter metrics.Meter
|
||||||
accountDupMeter metrics.Meter
|
accountDupMeter metrics.Meter
|
||||||
accountSkipMeter metrics.Meter
|
|
||||||
accountWasteMeter metrics.Meter
|
accountWasteMeter metrics.Meter
|
||||||
storageLoadMeter metrics.Meter
|
storageLoadMeter metrics.Meter
|
||||||
storageDupMeter metrics.Meter
|
storageDupMeter metrics.Meter
|
||||||
storageSkipMeter metrics.Meter
|
|
||||||
storageWasteMeter metrics.Meter
|
storageWasteMeter metrics.Meter
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTriePrefetcher(db Database, root common.Hash, namespace string) *triePrefetcher {
|
func newTriePrefetcher(db Database, root common.Hash, namespace string) *triePrefetcher {
|
||||||
prefix := triePrefetchMetricsPrefix + namespace
|
prefix := triePrefetchMetricsPrefix + namespace
|
||||||
p := &triePrefetcher{
|
return &triePrefetcher{
|
||||||
db: db,
|
db: db,
|
||||||
root: root,
|
root: root,
|
||||||
fetchers: make(map[string]*subfetcher), // Active prefetchers use the fetchers map
|
fetchers: make(map[string]*subfetcher), // Active prefetchers use the fetchers map
|
||||||
|
term: make(chan struct{}),
|
||||||
|
|
||||||
deliveryMissMeter: metrics.GetOrRegisterMeter(prefix+"/deliverymiss", nil),
|
deliveryMissMeter: metrics.GetOrRegisterMeter(prefix+"/deliverymiss", nil),
|
||||||
accountLoadMeter: metrics.GetOrRegisterMeter(prefix+"/account/load", nil),
|
accountLoadMeter: metrics.GetOrRegisterMeter(prefix+"/account/load", nil),
|
||||||
accountDupMeter: metrics.GetOrRegisterMeter(prefix+"/account/dup", nil),
|
accountDupMeter: metrics.GetOrRegisterMeter(prefix+"/account/dup", nil),
|
||||||
accountSkipMeter: metrics.GetOrRegisterMeter(prefix+"/account/skip", nil),
|
|
||||||
accountWasteMeter: metrics.GetOrRegisterMeter(prefix+"/account/waste", nil),
|
accountWasteMeter: metrics.GetOrRegisterMeter(prefix+"/account/waste", nil),
|
||||||
storageLoadMeter: metrics.GetOrRegisterMeter(prefix+"/storage/load", nil),
|
storageLoadMeter: metrics.GetOrRegisterMeter(prefix+"/storage/load", nil),
|
||||||
storageDupMeter: metrics.GetOrRegisterMeter(prefix+"/storage/dup", nil),
|
storageDupMeter: metrics.GetOrRegisterMeter(prefix+"/storage/dup", nil),
|
||||||
storageSkipMeter: metrics.GetOrRegisterMeter(prefix+"/storage/skip", nil),
|
|
||||||
storageWasteMeter: metrics.GetOrRegisterMeter(prefix+"/storage/waste", nil),
|
storageWasteMeter: metrics.GetOrRegisterMeter(prefix+"/storage/waste", nil),
|
||||||
}
|
}
|
||||||
return p
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// close iterates over all the subfetchers, aborts any that were left spinning
|
// terminate iterates over all the subfetchers and issues a terminateion request
|
||||||
// and reports the stats to the metrics subsystem.
|
// to all of them. Depending on the async parameter, the method will either block
|
||||||
func (p *triePrefetcher) close() {
|
// until all subfetchers spin down, or return immediately.
|
||||||
|
func (p *triePrefetcher) terminate(async bool) {
|
||||||
|
// Short circuit if the fetcher is already closed
|
||||||
|
select {
|
||||||
|
case <-p.term:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
// Termiante all sub-fetchers, sync or async, depending on the request
|
||||||
for _, fetcher := range p.fetchers {
|
for _, fetcher := range p.fetchers {
|
||||||
fetcher.abort() // safe to do multiple times
|
fetcher.terminate(async)
|
||||||
|
}
|
||||||
|
close(p.term)
|
||||||
|
}
|
||||||
|
|
||||||
|
// report aggregates the pre-fetching and usage metrics and reports them.
|
||||||
|
func (p *triePrefetcher) report() {
|
||||||
|
if !metrics.Enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, fetcher := range p.fetchers {
|
||||||
|
fetcher.wait() // ensure the fetcher's idle before poking in its internals
|
||||||
|
|
||||||
if metrics.Enabled {
|
|
||||||
if fetcher.root == p.root {
|
if fetcher.root == p.root {
|
||||||
p.accountLoadMeter.Mark(int64(len(fetcher.seen)))
|
p.accountLoadMeter.Mark(int64(len(fetcher.seen)))
|
||||||
p.accountDupMeter.Mark(int64(fetcher.dups))
|
p.accountDupMeter.Mark(int64(fetcher.dups))
|
||||||
p.accountSkipMeter.Mark(int64(len(fetcher.tasks)))
|
|
||||||
|
|
||||||
for _, key := range fetcher.used {
|
for _, key := range fetcher.used {
|
||||||
delete(fetcher.seen, string(key))
|
delete(fetcher.seen, string(key))
|
||||||
}
|
}
|
||||||
|
|
@ -90,8 +107,6 @@ func (p *triePrefetcher) close() {
|
||||||
} else {
|
} else {
|
||||||
p.storageLoadMeter.Mark(int64(len(fetcher.seen)))
|
p.storageLoadMeter.Mark(int64(len(fetcher.seen)))
|
||||||
p.storageDupMeter.Mark(int64(fetcher.dups))
|
p.storageDupMeter.Mark(int64(fetcher.dups))
|
||||||
p.storageSkipMeter.Mark(int64(len(fetcher.tasks)))
|
|
||||||
|
|
||||||
for _, key := range fetcher.used {
|
for _, key := range fetcher.used {
|
||||||
delete(fetcher.seen, string(key))
|
delete(fetcher.seen, string(key))
|
||||||
}
|
}
|
||||||
|
|
@ -99,98 +114,54 @@ func (p *triePrefetcher) close() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Clear out all fetchers (will crash on a second call, deliberate)
|
|
||||||
p.fetchers = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// copy creates a deep-but-inactive copy of the trie prefetcher. Any trie data
|
// prefetch schedules a batch of trie items to prefetch. After the prefetcher is
|
||||||
// already loaded will be copied over, but no goroutines will be started. This
|
// closed, all the following tasks scheduled will not be executed and an error
|
||||||
// is mostly used in the miner which creates a copy of it's actively mutated
|
// will be returned.
|
||||||
// state to be sealed while it may further mutate the state.
|
//
|
||||||
func (p *triePrefetcher) copy() *triePrefetcher {
|
// prefetch is called from two locations:
|
||||||
copy := &triePrefetcher{
|
//
|
||||||
db: p.db,
|
// 1. Finalize of the state-objects storage roots. This happens at the end
|
||||||
root: p.root,
|
// of every transaction, meaning that if several transactions touches
|
||||||
fetches: make(map[string]Trie), // Active prefetchers use the fetches map
|
// upon the same contract, the parameters invoking this method may be
|
||||||
|
// repeated.
|
||||||
deliveryMissMeter: p.deliveryMissMeter,
|
// 2. Finalize of the main account trie. This happens only once per block.
|
||||||
accountLoadMeter: p.accountLoadMeter,
|
func (p *triePrefetcher) prefetch(owner common.Hash, root common.Hash, addr common.Address, keys [][]byte) error {
|
||||||
accountDupMeter: p.accountDupMeter,
|
// Ensure the subfetcher is still alive
|
||||||
accountSkipMeter: p.accountSkipMeter,
|
select {
|
||||||
accountWasteMeter: p.accountWasteMeter,
|
case <-p.term:
|
||||||
storageLoadMeter: p.storageLoadMeter,
|
return errTerminated
|
||||||
storageDupMeter: p.storageDupMeter,
|
default:
|
||||||
storageSkipMeter: p.storageSkipMeter,
|
|
||||||
storageWasteMeter: p.storageWasteMeter,
|
|
||||||
}
|
}
|
||||||
// If the prefetcher is already a copy, duplicate the data
|
|
||||||
if p.fetches != nil {
|
|
||||||
for root, fetch := range p.fetches {
|
|
||||||
if fetch == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
copy.fetches[root] = p.db.CopyTrie(fetch)
|
|
||||||
}
|
|
||||||
return copy
|
|
||||||
}
|
|
||||||
// Otherwise we're copying an active fetcher, retrieve the current states
|
|
||||||
for id, fetcher := range p.fetchers {
|
|
||||||
copy.fetches[id] = fetcher.peek()
|
|
||||||
}
|
|
||||||
return copy
|
|
||||||
}
|
|
||||||
|
|
||||||
// prefetch schedules a batch of trie items to prefetch.
|
|
||||||
func (p *triePrefetcher) prefetch(owner common.Hash, root common.Hash, addr common.Address, keys [][]byte) {
|
|
||||||
// If the prefetcher is an inactive one, bail out
|
|
||||||
if p.fetches != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Active fetcher, schedule the retrievals
|
|
||||||
id := p.trieID(owner, root)
|
id := p.trieID(owner, root)
|
||||||
fetcher := p.fetchers[id]
|
fetcher := p.fetchers[id]
|
||||||
if fetcher == nil {
|
if fetcher == nil {
|
||||||
fetcher = newSubfetcher(p.db, p.root, owner, root, addr)
|
fetcher = newSubfetcher(p.db, p.root, owner, root, addr)
|
||||||
p.fetchers[id] = fetcher
|
p.fetchers[id] = fetcher
|
||||||
}
|
}
|
||||||
fetcher.schedule(keys)
|
return fetcher.schedule(keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
// trie returns the trie matching the root hash, or nil if the prefetcher doesn't
|
// trie returns the trie matching the root hash, blocking until the fetcher of
|
||||||
// have it.
|
// the given trie terminates. If no fetcher exists for the request, nil will be
|
||||||
func (p *triePrefetcher) trie(owner common.Hash, root common.Hash) Trie {
|
// returned.
|
||||||
// If the prefetcher is inactive, return from existing deep copies
|
func (p *triePrefetcher) trie(owner common.Hash, root common.Hash) (Trie, error) {
|
||||||
id := p.trieID(owner, root)
|
// Bail if no trie was prefetched for this root
|
||||||
if p.fetches != nil {
|
fetcher := p.fetchers[p.trieID(owner, root)]
|
||||||
trie := p.fetches[id]
|
|
||||||
if trie == nil {
|
|
||||||
p.deliveryMissMeter.Mark(1)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return p.db.CopyTrie(trie)
|
|
||||||
}
|
|
||||||
// Otherwise the prefetcher is active, bail if no trie was prefetched for this root
|
|
||||||
fetcher := p.fetchers[id]
|
|
||||||
if fetcher == nil {
|
if fetcher == nil {
|
||||||
|
log.Error("Prefetcher missed to load trie", "owner", owner, "root", root)
|
||||||
p.deliveryMissMeter.Mark(1)
|
p.deliveryMissMeter.Mark(1)
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
// Interrupt the prefetcher if it's by any chance still running and return
|
// Subfetcher exists, retrieve its trie
|
||||||
// a copy of any pre-loaded trie.
|
return fetcher.peek(), nil
|
||||||
fetcher.abort() // safe to do multiple times
|
|
||||||
|
|
||||||
trie := fetcher.peek()
|
|
||||||
if trie == nil {
|
|
||||||
p.deliveryMissMeter.Mark(1)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return trie
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// used marks a batch of state items used to allow creating statistics as to
|
// used marks a batch of state items used to allow creating statistics as to
|
||||||
// how useful or wasteful the prefetcher is.
|
// how useful or wasteful the fetcher is.
|
||||||
func (p *triePrefetcher) used(owner common.Hash, root common.Hash, used [][]byte) {
|
func (p *triePrefetcher) used(owner common.Hash, root common.Hash, used [][]byte) {
|
||||||
if fetcher := p.fetchers[p.trieID(owner, root)]; fetcher != nil {
|
if fetcher := p.fetchers[p.trieID(owner, root)]; fetcher != nil {
|
||||||
|
fetcher.wait() // ensure the fetcher's idle before poking in its internals
|
||||||
fetcher.used = used
|
fetcher.used = used
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -221,7 +192,6 @@ type subfetcher struct {
|
||||||
wake chan struct{} // Wake channel if a new task is scheduled
|
wake chan struct{} // Wake channel if a new task is scheduled
|
||||||
stop chan struct{} // Channel to interrupt processing
|
stop chan struct{} // Channel to interrupt processing
|
||||||
term chan struct{} // Channel to signal interruption
|
term chan struct{} // Channel to signal interruption
|
||||||
copy chan chan Trie // Channel to request a copy of the current trie
|
|
||||||
|
|
||||||
seen map[string]struct{} // Tracks the entries already loaded
|
seen map[string]struct{} // Tracks the entries already loaded
|
||||||
dups int // Number of duplicate preload tasks
|
dups int // Number of duplicate preload tasks
|
||||||
|
|
@ -240,7 +210,6 @@ func newSubfetcher(db Database, state common.Hash, owner common.Hash, root commo
|
||||||
wake: make(chan struct{}, 1),
|
wake: make(chan struct{}, 1),
|
||||||
stop: make(chan struct{}),
|
stop: make(chan struct{}),
|
||||||
term: make(chan struct{}),
|
term: make(chan struct{}),
|
||||||
copy: make(chan chan Trie),
|
|
||||||
seen: make(map[string]struct{}),
|
seen: make(map[string]struct{}),
|
||||||
}
|
}
|
||||||
go sf.loop()
|
go sf.loop()
|
||||||
|
|
@ -248,50 +217,61 @@ func newSubfetcher(db Database, state common.Hash, owner common.Hash, root commo
|
||||||
}
|
}
|
||||||
|
|
||||||
// schedule adds a batch of trie keys to the queue to prefetch.
|
// schedule adds a batch of trie keys to the queue to prefetch.
|
||||||
func (sf *subfetcher) schedule(keys [][]byte) {
|
func (sf *subfetcher) schedule(keys [][]byte) error {
|
||||||
|
// Ensure the subfetcher is still alive
|
||||||
|
select {
|
||||||
|
case <-sf.term:
|
||||||
|
return errTerminated
|
||||||
|
default:
|
||||||
|
}
|
||||||
// Append the tasks to the current queue
|
// Append the tasks to the current queue
|
||||||
sf.lock.Lock()
|
sf.lock.Lock()
|
||||||
sf.tasks = append(sf.tasks, keys...)
|
sf.tasks = append(sf.tasks, keys...)
|
||||||
sf.lock.Unlock()
|
sf.lock.Unlock()
|
||||||
|
|
||||||
// Notify the prefetcher, it's fine if it's already terminated
|
// Notify the background thread to execute scheduled tasks
|
||||||
select {
|
select {
|
||||||
case sf.wake <- struct{}{}:
|
case sf.wake <- struct{}{}:
|
||||||
|
// Wake signal sent
|
||||||
default:
|
default:
|
||||||
|
// Wake signal not sent as a previous is already queued
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// peek tries to retrieve a deep copy of the fetcher's trie in whatever form it
|
|
||||||
// is currently.
|
|
||||||
func (sf *subfetcher) peek() Trie {
|
|
||||||
ch := make(chan Trie)
|
|
||||||
select {
|
|
||||||
case sf.copy <- ch:
|
|
||||||
// Subfetcher still alive, return copy from it
|
|
||||||
return <-ch
|
|
||||||
|
|
||||||
case <-sf.term:
|
|
||||||
// Subfetcher already terminated, return a copy directly
|
|
||||||
if sf.trie == nil {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return sf.db.CopyTrie(sf.trie)
|
|
||||||
}
|
// wait blocks until the subfetcher terminates. This method is used to block on
|
||||||
|
// an async termination before accessing internal fields from the fetcher.
|
||||||
|
func (sf *subfetcher) wait() {
|
||||||
|
<-sf.term
|
||||||
}
|
}
|
||||||
|
|
||||||
// abort interrupts the subfetcher immediately. It is safe to call abort multiple
|
// peek retrieves the fetcher's trie, populated with any pre-fetched data. The
|
||||||
// times but it is not thread safe.
|
// returned trie will be a shallow copy, so modifying it will break subsequent
|
||||||
func (sf *subfetcher) abort() {
|
// 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
|
||||||
|
sf.wait()
|
||||||
|
return sf.trie
|
||||||
|
}
|
||||||
|
|
||||||
|
// terminate requests the subfetcher to stop accepting new tasks and spin down
|
||||||
|
// as soon as everything is loaded. Depending on the async parameter, the method
|
||||||
|
// will either block until all disk loads finish or return immediately.
|
||||||
|
func (sf *subfetcher) terminate(async bool) {
|
||||||
select {
|
select {
|
||||||
case <-sf.stop:
|
case <-sf.stop:
|
||||||
default:
|
default:
|
||||||
close(sf.stop)
|
close(sf.stop)
|
||||||
}
|
}
|
||||||
|
if async {
|
||||||
|
return
|
||||||
|
}
|
||||||
<-sf.term
|
<-sf.term
|
||||||
}
|
}
|
||||||
|
|
||||||
// loop waits for new tasks to be scheduled and keeps loading them until it runs
|
// loop loads newly-scheduled trie tasks as they are received and loads them, stopping
|
||||||
// out of tasks or its underlying trie is retrieved for committing.
|
// when requested.
|
||||||
func (sf *subfetcher) loop() {
|
func (sf *subfetcher) loop() {
|
||||||
// No matter how the loop stops, signal anyone waiting that it's terminated
|
// No matter how the loop stops, signal anyone waiting that it's terminated
|
||||||
defer close(sf.term)
|
defer close(sf.term)
|
||||||
|
|
@ -305,8 +285,6 @@ func (sf *subfetcher) loop() {
|
||||||
}
|
}
|
||||||
sf.trie = trie
|
sf.trie = trie
|
||||||
} else {
|
} else {
|
||||||
// The trie argument can be nil as verkle doesn't support prefetching
|
|
||||||
// yet. TODO FIX IT(rjl493456442), otherwise code will panic here.
|
|
||||||
trie, err := sf.db.OpenStorageTrie(sf.state, sf.addr, sf.root, nil)
|
trie, err := sf.db.OpenStorageTrie(sf.state, sf.addr, sf.root, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Trie prefetcher failed opening trie", "root", sf.root, "err", err)
|
log.Warn("Trie prefetcher failed opening trie", "root", sf.root, "err", err)
|
||||||
|
|
@ -318,31 +296,17 @@ func (sf *subfetcher) loop() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-sf.wake:
|
case <-sf.wake:
|
||||||
// Subfetcher was woken up, retrieve any tasks to avoid spinning the lock
|
// Execute all remaining tasks in single run
|
||||||
sf.lock.Lock()
|
sf.lock.Lock()
|
||||||
tasks := sf.tasks
|
tasks := sf.tasks
|
||||||
sf.tasks = nil
|
sf.tasks = nil
|
||||||
sf.lock.Unlock()
|
sf.lock.Unlock()
|
||||||
|
|
||||||
// Prefetch any tasks until the loop is interrupted
|
for _, task := range tasks {
|
||||||
for i, task := range tasks {
|
|
||||||
select {
|
|
||||||
case <-sf.stop:
|
|
||||||
// If termination is requested, add any leftover back and return
|
|
||||||
sf.lock.Lock()
|
|
||||||
sf.tasks = append(sf.tasks, tasks[i:]...)
|
|
||||||
sf.lock.Unlock()
|
|
||||||
return
|
|
||||||
|
|
||||||
case ch := <-sf.copy:
|
|
||||||
// Somebody wants a copy of the current trie, grant them
|
|
||||||
ch <- sf.db.CopyTrie(sf.trie)
|
|
||||||
|
|
||||||
default:
|
|
||||||
// No termination request yet, prefetch the next entry
|
|
||||||
if _, ok := sf.seen[string(task)]; ok {
|
if _, ok := sf.seen[string(task)]; ok {
|
||||||
sf.dups++
|
sf.dups++
|
||||||
} else {
|
continue
|
||||||
|
}
|
||||||
if len(task) == common.AddressLength {
|
if len(task) == common.AddressLength {
|
||||||
sf.trie.GetAccount(common.BytesToAddress(task))
|
sf.trie.GetAccount(common.BytesToAddress(task))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -350,16 +314,20 @@ func (sf *subfetcher) loop() {
|
||||||
}
|
}
|
||||||
sf.seen[string(task)] = struct{}{}
|
sf.seen[string(task)] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case ch := <-sf.copy:
|
|
||||||
// Somebody wants a copy of the current trie, grant them
|
|
||||||
ch <- sf.db.CopyTrie(sf.trie)
|
|
||||||
|
|
||||||
case <-sf.stop:
|
case <-sf.stop:
|
||||||
// Termination is requested, abort and leave remaining tasks
|
// Termination is requested, abort if no more tasks are pending. If
|
||||||
|
// there are some, exhaust them first.
|
||||||
|
sf.lock.Lock()
|
||||||
|
done := sf.tasks == nil
|
||||||
|
sf.lock.Unlock()
|
||||||
|
|
||||||
|
if done {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Some tasks are pending, loop and pick them up (that wake branch
|
||||||
|
// will be selected eventually, whilst stop remains closed to this
|
||||||
|
// branch will also run afterwards).
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ package state
|
||||||
import (
|
import (
|
||||||
"math/big"
|
"math/big"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||||
|
|
@ -46,68 +45,20 @@ func filledStateDB() *StateDB {
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCopyAndClose(t *testing.T) {
|
func TestUseAfterTerminate(t *testing.T) {
|
||||||
db := filledStateDB()
|
db := filledStateDB()
|
||||||
prefetcher := newTriePrefetcher(db.db, db.originalRoot, "")
|
prefetcher := newTriePrefetcher(db.db, db.originalRoot, "")
|
||||||
skey := common.HexToHash("aaa")
|
skey := common.HexToHash("aaa")
|
||||||
prefetcher.prefetch(common.Hash{}, db.originalRoot, common.Address{}, [][]byte{skey.Bytes()})
|
|
||||||
prefetcher.prefetch(common.Hash{}, db.originalRoot, common.Address{}, [][]byte{skey.Bytes()})
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
a := prefetcher.trie(common.Hash{}, db.originalRoot)
|
|
||||||
prefetcher.prefetch(common.Hash{}, db.originalRoot, common.Address{}, [][]byte{skey.Bytes()})
|
|
||||||
b := prefetcher.trie(common.Hash{}, db.originalRoot)
|
|
||||||
cpy := prefetcher.copy()
|
|
||||||
cpy.prefetch(common.Hash{}, db.originalRoot, common.Address{}, [][]byte{skey.Bytes()})
|
|
||||||
cpy.prefetch(common.Hash{}, db.originalRoot, common.Address{}, [][]byte{skey.Bytes()})
|
|
||||||
c := cpy.trie(common.Hash{}, db.originalRoot)
|
|
||||||
prefetcher.close()
|
|
||||||
cpy2 := cpy.copy()
|
|
||||||
cpy2.prefetch(common.Hash{}, db.originalRoot, common.Address{}, [][]byte{skey.Bytes()})
|
|
||||||
d := cpy2.trie(common.Hash{}, db.originalRoot)
|
|
||||||
cpy.close()
|
|
||||||
cpy2.close()
|
|
||||||
if a.Hash() != b.Hash() || a.Hash() != c.Hash() || a.Hash() != d.Hash() {
|
|
||||||
t.Fatalf("Invalid trie, hashes should be equal: %v %v %v %v", a.Hash(), b.Hash(), c.Hash(), d.Hash())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUseAfterClose(t *testing.T) {
|
if err := prefetcher.prefetch(common.Hash{}, db.originalRoot, common.Address{}, [][]byte{skey.Bytes()}); err != nil {
|
||||||
db := filledStateDB()
|
t.Errorf("Prefetch failed before terminate: %v", err)
|
||||||
prefetcher := newTriePrefetcher(db.db, db.originalRoot, "")
|
|
||||||
skey := common.HexToHash("aaa")
|
|
||||||
prefetcher.prefetch(common.Hash{}, db.originalRoot, common.Address{}, [][]byte{skey.Bytes()})
|
|
||||||
a := prefetcher.trie(common.Hash{}, db.originalRoot)
|
|
||||||
prefetcher.close()
|
|
||||||
b := prefetcher.trie(common.Hash{}, db.originalRoot)
|
|
||||||
if a == nil {
|
|
||||||
t.Fatal("Prefetching before close should not return nil")
|
|
||||||
}
|
|
||||||
if b != nil {
|
|
||||||
t.Fatal("Trie after close should return nil")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
prefetcher.terminate(false)
|
||||||
|
|
||||||
func TestCopyClose(t *testing.T) {
|
if err := prefetcher.prefetch(common.Hash{}, db.originalRoot, common.Address{}, [][]byte{skey.Bytes()}); err == nil {
|
||||||
db := filledStateDB()
|
t.Errorf("Prefetch succeeded after terminate: %v", err)
|
||||||
prefetcher := newTriePrefetcher(db.db, db.originalRoot, "")
|
|
||||||
skey := common.HexToHash("aaa")
|
|
||||||
prefetcher.prefetch(common.Hash{}, db.originalRoot, common.Address{}, [][]byte{skey.Bytes()})
|
|
||||||
cpy := prefetcher.copy()
|
|
||||||
a := prefetcher.trie(common.Hash{}, db.originalRoot)
|
|
||||||
b := cpy.trie(common.Hash{}, db.originalRoot)
|
|
||||||
prefetcher.close()
|
|
||||||
c := prefetcher.trie(common.Hash{}, db.originalRoot)
|
|
||||||
d := cpy.trie(common.Hash{}, db.originalRoot)
|
|
||||||
if a == nil {
|
|
||||||
t.Fatal("Prefetching before close should not return nil")
|
|
||||||
}
|
}
|
||||||
if b == nil {
|
if _, err := prefetcher.trie(common.Hash{}, db.originalRoot); err != nil {
|
||||||
t.Fatal("Copy trie should return nil")
|
t.Errorf("Trie retrieval failed after terminate: %v", err)
|
||||||
}
|
|
||||||
if c != nil {
|
|
||||||
t.Fatal("Trie after close should return nil")
|
|
||||||
}
|
|
||||||
if d == nil {
|
|
||||||
t.Fatal("Copy trie should not return nil")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -240,8 +240,9 @@ func (st *StateTransition) buyGas() error {
|
||||||
if st.msg.GasFeeCap != nil {
|
if st.msg.GasFeeCap != nil {
|
||||||
balanceCheck.SetUint64(st.msg.GasLimit)
|
balanceCheck.SetUint64(st.msg.GasLimit)
|
||||||
balanceCheck = balanceCheck.Mul(balanceCheck, st.msg.GasFeeCap)
|
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 st.evm.ChainConfig().IsCancun(st.evm.Context.BlockNumber, st.evm.Context.Time) {
|
||||||
if blobGas := st.blobGasUsed(); blobGas > 0 {
|
if blobGas := st.blobGasUsed(); blobGas > 0 {
|
||||||
// Check that the user has enough funds to cover blobGasUsed * tx.BlobGasFeeCap
|
// Check that the user has enough funds to cover blobGasUsed * tx.BlobGasFeeCap
|
||||||
|
|
|
||||||
|
|
@ -2358,7 +2358,7 @@ func (s *Syncer) commitHealer(force bool) {
|
||||||
}
|
}
|
||||||
batch := s.db.NewBatch()
|
batch := s.db.NewBatch()
|
||||||
if err := s.healer.scheduler.Commit(batch); err != nil {
|
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 {
|
if err := batch.Write(); err != nil {
|
||||||
log.Crit("Failed to persist healing data", "err", err)
|
log.Crit("Failed to persist healing data", "err", err)
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
@ -805,9 +804,13 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block
|
||||||
// Execute the transaction and flush any traces to disk
|
// Execute the transaction and flush any traces to disk
|
||||||
vmenv := vm.NewEVM(vmctx, txContext, statedb, chainConfig, vmConf)
|
vmenv := vm.NewEVM(vmctx, txContext, statedb, chainConfig, vmConf)
|
||||||
statedb.SetTxContext(tx.Hash(), i)
|
statedb.SetTxContext(tx.Hash(), i)
|
||||||
|
if vmConf.Tracer.OnTxStart != nil {
|
||||||
vmConf.Tracer.OnTxStart(vmenv.GetVMContext(), tx, msg.From)
|
vmConf.Tracer.OnTxStart(vmenv.GetVMContext(), tx, msg.From)
|
||||||
|
}
|
||||||
vmRet, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.GasLimit))
|
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)
|
vmConf.Tracer.OnTxEnd(&types.Receipt{GasUsed: vmRet.UsedGas}, err)
|
||||||
|
}
|
||||||
if writer != nil {
|
if writer != nil {
|
||||||
writer.Flush()
|
writer.Flush()
|
||||||
}
|
}
|
||||||
|
|
@ -982,7 +985,8 @@ func (api *API) traceTx(ctx context.Context, tx *types.Transaction, message *cor
|
||||||
return nil, err
|
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)
|
statedb.SetLogger(tracer.Hooks)
|
||||||
|
|
||||||
// Define a meaningful timeout of a single transaction trace
|
// Define a meaningful timeout of a single transaction trace
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
"github.com/ethereum/go-ethereum/consensus"
|
"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/consensus/ethash"
|
||||||
"github.com/ethereum/go-ethereum/core"
|
"github.com/ethereum/go-ethereum/core"
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
"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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ type jsonLogger struct {
|
||||||
encoder *json.Encoder
|
encoder *json.Encoder
|
||||||
cfg *Config
|
cfg *Config
|
||||||
env *tracing.VMContext
|
env *tracing.VMContext
|
||||||
|
hooks *tracing.Hooks
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewJSONLogger creates a new EVM tracer that prints execution steps as JSON objects
|
// 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 {
|
if l.cfg == nil {
|
||||||
l.cfg = &Config{}
|
l.cfg = &Config{}
|
||||||
}
|
}
|
||||||
return &tracing.Hooks{
|
l.hooks = &tracing.Hooks{
|
||||||
OnTxStart: l.OnTxStart,
|
OnTxStart: l.OnTxStart,
|
||||||
OnExit: l.OnExit,
|
OnSystemCallStart: l.onSystemCallStart,
|
||||||
|
OnExit: l.OnEnd,
|
||||||
OnOpcode: l.OnOpcode,
|
OnOpcode: l.OnOpcode,
|
||||||
OnFault: l.OnFault,
|
OnFault: l.OnFault,
|
||||||
}
|
}
|
||||||
|
return l.hooks
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewJSONLoggerWithCallFrames creates a new EVM tracer that prints execution steps as JSON objects
|
// 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 {
|
if l.cfg == nil {
|
||||||
l.cfg = &Config{}
|
l.cfg = &Config{}
|
||||||
}
|
}
|
||||||
return &tracing.Hooks{
|
l.hooks = &tracing.Hooks{
|
||||||
OnTxStart: l.OnTxStart,
|
OnTxStart: l.OnTxStart,
|
||||||
|
OnSystemCallStart: l.onSystemCallStart,
|
||||||
OnEnter: l.OnEnter,
|
OnEnter: l.OnEnter,
|
||||||
OnExit: l.OnExit,
|
OnExit: l.OnExit,
|
||||||
OnOpcode: l.OnOpcode,
|
OnOpcode: l.OnOpcode,
|
||||||
OnFault: l.OnFault,
|
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) {
|
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)
|
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.
|
// 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) {
|
func (l *jsonLogger) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
|
||||||
frame := callFrame{
|
frame := callFrame{
|
||||||
|
|
|
||||||
|
|
@ -966,7 +966,7 @@ func (s *BlockChainAPI) GetBlockReceipts(ctx context.Context, blockNrOrHash rpc.
|
||||||
// of a message call.
|
// of a message call.
|
||||||
// Note, state and stateDiff can't be specified at the same time. If state is
|
// 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
|
// 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.
|
// message.
|
||||||
type OverrideAccount struct {
|
type OverrideAccount struct {
|
||||||
Nonce *hexutil.Uint64 `json:"nonce"`
|
Nonce *hexutil.Uint64 `json:"nonce"`
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ func (h *bufHandler) Handle(_ context.Context, r slog.Record) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *bufHandler) Enabled(_ context.Context, lvl slog.Level) bool {
|
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 {
|
func (h *bufHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ func TestExpDecaySample(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, v := range values {
|
for _, v := range values {
|
||||||
if v > int64(tc.updates) || v < 0 {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -195,7 +195,7 @@ func TestUniformSample(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, v := range values {
|
for _, v := range values {
|
||||||
if v > 1000 || v < 0 {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
17
node/api.go
17
node/api.go
|
|
@ -26,6 +26,7 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/internal/debug"
|
"github.com/ethereum/go-ethereum/internal/debug"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
"github.com/ethereum/go-ethereum/p2p"
|
"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/p2p/enode"
|
||||||
"github.com/ethereum/go-ethereum/rpc"
|
"github.com/ethereum/go-ethereum/rpc"
|
||||||
)
|
)
|
||||||
|
|
@ -39,6 +40,9 @@ func (n *Node) apis() []rpc.API {
|
||||||
}, {
|
}, {
|
||||||
Namespace: "debug",
|
Namespace: "debug",
|
||||||
Service: debug.Handler,
|
Service: debug.Handler,
|
||||||
|
}, {
|
||||||
|
Namespace: "debug",
|
||||||
|
Service: &p2pDebugAPI{n},
|
||||||
}, {
|
}, {
|
||||||
Namespace: "web3",
|
Namespace: "web3",
|
||||||
Service: &web3API{n},
|
Service: &web3API{n},
|
||||||
|
|
@ -333,3 +337,16 @@ func (s *web3API) ClientVersion() string {
|
||||||
func (s *web3API) Sha3(input hexutil.Bytes) hexutil.Bytes {
|
func (s *web3API) Sha3(input hexutil.Bytes) hexutil.Bytes {
|
||||||
return crypto.Keccak256(input)
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,11 @@ package discover
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
|
crand "crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/mclock"
|
"github.com/ethereum/go-ethereum/common/mclock"
|
||||||
|
|
@ -62,7 +66,7 @@ type Config struct {
|
||||||
func (cfg Config) withDefaults() Config {
|
func (cfg Config) withDefaults() Config {
|
||||||
// Node table configuration:
|
// Node table configuration:
|
||||||
if cfg.PingInterval == 0 {
|
if cfg.PingInterval == 0 {
|
||||||
cfg.PingInterval = 10 * time.Second
|
cfg.PingInterval = 3 * time.Second
|
||||||
}
|
}
|
||||||
if cfg.RefreshInterval == 0 {
|
if cfg.RefreshInterval == 0 {
|
||||||
cfg.RefreshInterval = 30 * time.Minute
|
cfg.RefreshInterval = 30 * time.Minute
|
||||||
|
|
@ -92,3 +96,44 @@ type ReadPacket struct {
|
||||||
Data []byte
|
Data []byte
|
||||||
Addr *net.UDPAddr
|
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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -140,32 +140,13 @@ func (it *lookup) slowdown() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (it *lookup) query(n *node, reply chan<- []*node) {
|
func (it *lookup) query(n *node, reply chan<- []*node) {
|
||||||
fails := it.tab.db.FindFails(n.ID(), n.IP())
|
|
||||||
r, err := it.queryfunc(n)
|
r, err := it.queryfunc(n)
|
||||||
if errors.Is(err, errClosed) {
|
if !errors.Is(err, errClosed) { // avoid recording failures on shutdown.
|
||||||
// Avoid recording failures on shutdown.
|
success := len(r) > 0
|
||||||
reply <- nil
|
it.tab.trackRequest(n, success, r)
|
||||||
return
|
if err != nil {
|
||||||
} else if len(r) == 0 {
|
it.tab.log.Trace("FINDNODE failed", "id", n.ID(), "err", err)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
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
|
reply <- r
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,22 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
"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.
|
// node represents a host on the network.
|
||||||
// The fields of Node may not be modified.
|
// The fields of Node may not be modified.
|
||||||
type node struct {
|
type node struct {
|
||||||
enode.Node
|
*enode.Node
|
||||||
addedAt time.Time // time when the node was added to the table
|
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
|
livenessChecks uint // how often liveness was checked
|
||||||
|
isValidatedLive bool // true if existence of node is considered validated right now
|
||||||
}
|
}
|
||||||
|
|
||||||
type encPubkey [64]byte
|
type encPubkey [64]byte
|
||||||
|
|
@ -65,7 +75,7 @@ func (e encPubkey) id() enode.ID {
|
||||||
}
|
}
|
||||||
|
|
||||||
func wrapNode(n *enode.Node) *node {
|
func wrapNode(n *enode.Node) *node {
|
||||||
return &node{Node: *n}
|
return &node{Node: n}
|
||||||
}
|
}
|
||||||
|
|
||||||
func wrapNodes(ns []*enode.Node) []*node {
|
func wrapNodes(ns []*enode.Node) []*node {
|
||||||
|
|
@ -77,7 +87,7 @@ func wrapNodes(ns []*enode.Node) []*node {
|
||||||
}
|
}
|
||||||
|
|
||||||
func unwrapNode(n *node) *enode.Node {
|
func unwrapNode(n *node) *enode.Node {
|
||||||
return &n.Node
|
return n.Node
|
||||||
}
|
}
|
||||||
|
|
||||||
func unwrapNodes(ns []*node) []*enode.Node {
|
func unwrapNodes(ns []*node) []*enode.Node {
|
||||||
|
|
|
||||||
|
|
@ -24,16 +24,15 @@ package discover
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
crand "crypto/rand"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
mrand "math/rand"
|
|
||||||
"net"
|
"net"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/common/mclock"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
"github.com/ethereum/go-ethereum/metrics"
|
"github.com/ethereum/go-ethereum/metrics"
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||||
|
|
@ -55,7 +54,6 @@ const (
|
||||||
bucketIPLimit, bucketSubnet = 2, 24 // at most 2 addresses from the same /24
|
bucketIPLimit, bucketSubnet = 2, 24 // at most 2 addresses from the same /24
|
||||||
tableIPLimit, tableSubnet = 10, 24
|
tableIPLimit, tableSubnet = 10, 24
|
||||||
|
|
||||||
copyNodesInterval = 30 * time.Second
|
|
||||||
seedMinTableTime = 5 * time.Minute
|
seedMinTableTime = 5 * time.Minute
|
||||||
seedCount = 30
|
seedCount = 30
|
||||||
seedMaxAge = 5 * 24 * time.Hour
|
seedMaxAge = 5 * 24 * time.Hour
|
||||||
|
|
@ -68,8 +66,9 @@ type Table struct {
|
||||||
mutex sync.Mutex // protects buckets, bucket content, nursery, rand
|
mutex sync.Mutex // protects buckets, bucket content, nursery, rand
|
||||||
buckets [nBuckets]*bucket // index of known nodes by distance
|
buckets [nBuckets]*bucket // index of known nodes by distance
|
||||||
nursery []*node // bootstrap nodes
|
nursery []*node // bootstrap nodes
|
||||||
rand *mrand.Rand // source of randomness, periodically reseeded
|
rand reseedingRandom // source of randomness, periodically reseeded
|
||||||
ips netutil.DistinctNetSet
|
ips netutil.DistinctNetSet
|
||||||
|
revalidation tableRevalidation
|
||||||
|
|
||||||
db *enode.DB // database of known nodes
|
db *enode.DB // database of known nodes
|
||||||
net transport
|
net transport
|
||||||
|
|
@ -78,6 +77,10 @@ type Table struct {
|
||||||
|
|
||||||
// loop channels
|
// loop channels
|
||||||
refreshReq chan chan struct{}
|
refreshReq chan chan struct{}
|
||||||
|
revalResponseCh chan revalidationResponse
|
||||||
|
addNodeCh chan addNodeOp
|
||||||
|
addNodeHandled chan bool
|
||||||
|
trackRequestCh chan trackRequestOp
|
||||||
initDone chan struct{}
|
initDone chan struct{}
|
||||||
closeReq chan struct{}
|
closeReq chan struct{}
|
||||||
closed chan struct{}
|
closed chan struct{}
|
||||||
|
|
@ -104,6 +107,17 @@ type bucket struct {
|
||||||
index int
|
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) {
|
func newTable(t transport, db *enode.DB, cfg Config) (*Table, error) {
|
||||||
cfg = cfg.withDefaults()
|
cfg = cfg.withDefaults()
|
||||||
tab := &Table{
|
tab := &Table{
|
||||||
|
|
@ -112,56 +126,49 @@ func newTable(t transport, db *enode.DB, cfg Config) (*Table, error) {
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
log: cfg.Log,
|
log: cfg.Log,
|
||||||
refreshReq: make(chan chan struct{}),
|
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{}),
|
initDone: make(chan struct{}),
|
||||||
closeReq: make(chan struct{}),
|
closeReq: make(chan struct{}),
|
||||||
closed: make(chan struct{}),
|
closed: make(chan struct{}),
|
||||||
rand: mrand.New(mrand.NewSource(0)),
|
|
||||||
ips: netutil.DistinctNetSet{Subnet: tableSubnet, Limit: tableIPLimit},
|
ips: netutil.DistinctNetSet{Subnet: tableSubnet, Limit: tableIPLimit},
|
||||||
}
|
}
|
||||||
if err := tab.setFallbackNodes(cfg.Bootnodes); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for i := range tab.buckets {
|
for i := range tab.buckets {
|
||||||
tab.buckets[i] = &bucket{
|
tab.buckets[i] = &bucket{
|
||||||
index: i,
|
index: i,
|
||||||
ips: netutil.DistinctNetSet{Subnet: bucketSubnet, Limit: bucketIPLimit},
|
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()
|
tab.loadSeedNodes()
|
||||||
|
|
||||||
return tab, nil
|
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.
|
// Nodes returns all nodes contained in the table.
|
||||||
func (tab *Table) Nodes() []*enode.Node {
|
func (tab *Table) Nodes() [][]BucketNode {
|
||||||
if !tab.isInitDone() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tab.mutex.Lock()
|
tab.mutex.Lock()
|
||||||
defer tab.mutex.Unlock()
|
defer tab.mutex.Unlock()
|
||||||
|
|
||||||
var nodes []*enode.Node
|
nodes := make([][]BucketNode, len(tab.buckets))
|
||||||
for _, b := range &tab.buckets {
|
for i, b := range &tab.buckets {
|
||||||
for _, n := range b.entries {
|
nodes[i] = make([]BucketNode, len(b.entries))
|
||||||
nodes = append(nodes, unwrapNode(n))
|
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
|
return nodes
|
||||||
|
|
@ -171,15 +178,6 @@ func (tab *Table) self() *enode.Node {
|
||||||
return tab.net.Self()
|
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.
|
// 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 {
|
func (tab *Table) getNode(id enode.ID) *enode.Node {
|
||||||
tab.mutex.Lock()
|
tab.mutex.Lock()
|
||||||
|
|
@ -239,52 +237,173 @@ func (tab *Table) refresh() <-chan struct{} {
|
||||||
return done
|
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() {
|
func (tab *Table) loop() {
|
||||||
var (
|
var (
|
||||||
revalidate = time.NewTimer(tab.nextRevalidateTime())
|
|
||||||
refresh = time.NewTimer(tab.nextRefreshTime())
|
refresh = time.NewTimer(tab.nextRefreshTime())
|
||||||
copyNodes = time.NewTicker(copyNodesInterval)
|
|
||||||
refreshDone = make(chan struct{}) // where doRefresh reports completion
|
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
|
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 refresh.Stop()
|
||||||
defer revalidate.Stop()
|
defer revalTimer.Stop()
|
||||||
defer copyNodes.Stop()
|
defer reseedRandTimer.Stop()
|
||||||
|
|
||||||
// Start initial refresh.
|
// Start initial refresh.
|
||||||
go tab.doRefresh(refreshDone)
|
go tab.doRefresh(refreshDone)
|
||||||
|
|
||||||
loop:
|
loop:
|
||||||
for {
|
for {
|
||||||
|
nextTime := tab.revalidation.run(tab, tab.cfg.Clock.Now())
|
||||||
|
revalTimer.Schedule(nextTime)
|
||||||
|
|
||||||
select {
|
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:
|
case <-refresh.C:
|
||||||
tab.seedRand()
|
|
||||||
if refreshDone == nil {
|
if refreshDone == nil {
|
||||||
refreshDone = make(chan struct{})
|
refreshDone = make(chan struct{})
|
||||||
go tab.doRefresh(refreshDone)
|
go tab.doRefresh(refreshDone)
|
||||||
}
|
}
|
||||||
|
|
||||||
case req := <-tab.refreshReq:
|
case req := <-tab.refreshReq:
|
||||||
waiting = append(waiting, req)
|
waiting = append(waiting, req)
|
||||||
if refreshDone == nil {
|
if refreshDone == nil {
|
||||||
refreshDone = make(chan struct{})
|
refreshDone = make(chan struct{})
|
||||||
go tab.doRefresh(refreshDone)
|
go tab.doRefresh(refreshDone)
|
||||||
}
|
}
|
||||||
|
|
||||||
case <-refreshDone:
|
case <-refreshDone:
|
||||||
for _, ch := range waiting {
|
for _, ch := range waiting {
|
||||||
close(ch)
|
close(ch)
|
||||||
}
|
}
|
||||||
waiting, refreshDone = nil, nil
|
waiting, refreshDone = nil, nil
|
||||||
refresh.Reset(tab.nextRefreshTime())
|
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:
|
case <-tab.closeReq:
|
||||||
break loop
|
break loop
|
||||||
}
|
}
|
||||||
|
|
@ -296,9 +415,6 @@ loop:
|
||||||
for _, ch := range waiting {
|
for _, ch := range waiting {
|
||||||
close(ch)
|
close(ch)
|
||||||
}
|
}
|
||||||
if revalidateDone != nil {
|
|
||||||
<-revalidateDone
|
|
||||||
}
|
|
||||||
close(tab.closed)
|
close(tab.closed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -335,169 +451,15 @@ func (tab *Table) loadSeedNodes() {
|
||||||
age := time.Since(tab.db.LastPongReceived(seed.ID(), seed.IP()))
|
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.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 {
|
func (tab *Table) nextRefreshTime() time.Duration {
|
||||||
tab.mutex.Lock()
|
|
||||||
defer tab.mutex.Unlock()
|
|
||||||
|
|
||||||
half := tab.cfg.RefreshInterval / 2
|
half := tab.cfg.RefreshInterval / 2
|
||||||
return half + time.Duration(tab.rand.Int63n(int64(half)))
|
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.
|
// bucket returns the bucket for the given node ID hash.
|
||||||
func (tab *Table) bucket(id enode.ID) *bucket {
|
func (tab *Table) bucket(id enode.ID) *bucket {
|
||||||
d := enode.LogDist(tab.self().ID(), id)
|
d := enode.LogDist(tab.self().ID(), id)
|
||||||
|
|
@ -511,95 +473,6 @@ func (tab *Table) bucketAtDistance(d int) *bucket {
|
||||||
return tab.buckets[d-bucketMinDistance-1]
|
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 {
|
func (tab *Table) addIP(b *bucket, ip net.IP) bool {
|
||||||
if len(ip) == 0 {
|
if len(ip) == 0 {
|
||||||
return false // Nodes without IP cannot be added.
|
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)
|
b.ips.Remove(ip)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tab *Table) addReplacement(b *bucket, n *node) {
|
// handleAddNode adds the node in the request to the table, if there is space.
|
||||||
for _, e := range b.replacements {
|
// The caller must hold tab.mutex.
|
||||||
if e.ID() == n.ID() {
|
func (tab *Table) handleAddNode(req addNodeOp) bool {
|
||||||
return // already in list
|
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()) {
|
if !tab.addIP(b, n.IP()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
n.addedToTable = time.Now()
|
||||||
var removed *node
|
var removed *node
|
||||||
b.replacements, removed = pushNode(b.replacements, n, maxReplacements)
|
b.replacements, removed = pushNode(b.replacements, n, maxReplacements)
|
||||||
if removed != nil {
|
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
|
func (tab *Table) nodeAdded(b *bucket, n *node) {
|
||||||
// last entry in the bucket. If 'last' isn't the last entry, it has either been replaced
|
if n.addedToTable == (time.Time{}) {
|
||||||
// with someone else or became active.
|
n.addedToTable = time.Now()
|
||||||
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
|
|
||||||
}
|
}
|
||||||
// Still the last entry.
|
n.addedToBucket = time.Now()
|
||||||
if len(b.replacements) == 0 {
|
tab.revalidation.nodeAdded(tab, n)
|
||||||
tab.deleteInBucket(b, last)
|
if tab.nodeAddedHook != nil {
|
||||||
return 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
|
func (tab *Table) nodeRemoved(b *bucket, n *node) {
|
||||||
// if it is contained in that list.
|
tab.revalidation.nodeRemoved(n)
|
||||||
func (tab *Table) bumpInBucket(b *bucket, n *node) bool {
|
if tab.nodeRemovedHook != nil {
|
||||||
for i := range b.entries {
|
tab.nodeRemovedHook(b, n)
|
||||||
if b.entries[i].ID() == n.ID() {
|
}
|
||||||
if !n.IP().Equal(b.entries[i].IP()) {
|
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.
|
// Endpoint has changed, ensure that the new IP fits into table limits.
|
||||||
tab.removeIP(b, b.entries[i].IP())
|
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.
|
// It doesn't, put the previous one back.
|
||||||
tab.addIP(b, b.entries[i].IP())
|
tab.addIP(b, b.entries[i].IP())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Move it to the front.
|
b.entries[i].Node = newRecord
|
||||||
copy(b.entries[1:], b.entries[:i])
|
|
||||||
b.entries[0] = n
|
|
||||||
return true
|
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) {
|
tab.mutex.Lock()
|
||||||
// Check if the node is actually in the bucket so the removed hook
|
defer tab.mutex.Unlock()
|
||||||
// isn't called multiple times for the same node.
|
|
||||||
if !contains(b.entries, n.ID()) {
|
b := tab.bucket(op.node.ID())
|
||||||
return
|
// 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())
|
// Add found nodes.
|
||||||
if tab.nodeRemovedHook != nil {
|
for _, n := range op.foundNodes {
|
||||||
tab.nodeRemovedHook(b, n)
|
tab.handleAddNode(addNodeOp{n, false})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
223
p2p/discover/table_reval.go
Normal file
223
p2p/discover/table_reval.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -20,14 +20,16 @@ import (
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"testing/quick"
|
"testing/quick"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common/mclock"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"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/enode"
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
"github.com/ethereum/go-ethereum/p2p/enr"
|
||||||
"github.com/ethereum/go-ethereum/p2p/netutil"
|
"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) {
|
func testPingReplace(t *testing.T, newNodeIsResponding, lastInBucketIsResponding bool) {
|
||||||
|
simclock := new(mclock.Simulated)
|
||||||
transport := newPingRecorder()
|
transport := newPingRecorder()
|
||||||
tab, db := newTestTable(transport)
|
tab, db := newTestTable(transport, Config{
|
||||||
|
Clock: simclock,
|
||||||
|
Log: testlog.Logger(t, log.LevelTrace),
|
||||||
|
})
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
defer tab.close()
|
defer tab.close()
|
||||||
|
|
||||||
<-tab.initDone
|
<-tab.initDone
|
||||||
|
|
||||||
// Fill up the sender's bucket.
|
// Fill up the sender's bucket.
|
||||||
pingKey, _ := crypto.HexToECDSA("45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8")
|
replacementNodeKey, _ := crypto.HexToECDSA("45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8")
|
||||||
pingSender := wrapNode(enode.NewV4(&pingKey.PublicKey, net.IP{127, 0, 0, 1}, 99, 99))
|
replacementNode := wrapNode(enode.NewV4(&replacementNodeKey.PublicKey, net.IP{127, 0, 0, 1}, 99, 99))
|
||||||
last := fillBucket(tab, pingSender)
|
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
|
// The revalidation process should replace
|
||||||
// its bucket if it is unresponsive. Revalidate again to ensure that
|
// this node in the bucket if it is unresponsive.
|
||||||
transport.dead[last.ID()] = !lastInBucketIsResponding
|
transport.dead[last.ID()] = !lastInBucketIsResponding
|
||||||
transport.dead[pingSender.ID()] = !newNodeIsResponding
|
transport.dead[replacementNode.ID()] = !newNodeIsResponding
|
||||||
tab.addSeenNode(pingSender)
|
|
||||||
tab.doRevalidate(make(chan struct{}, 1))
|
|
||||||
tab.doRevalidate(make(chan struct{}, 1))
|
|
||||||
|
|
||||||
if !transport.pinged[last.ID()] {
|
// Add replacement node to table.
|
||||||
// Oldest node in bucket is pinged to see whether it is still alive.
|
tab.addFoundNode(replacementNode)
|
||||||
t.Error("table did not ping last node in bucket")
|
|
||||||
|
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()
|
tab.mutex.Lock()
|
||||||
defer tab.mutex.Unlock()
|
defer tab.mutex.Unlock()
|
||||||
wantSize := bucketSize
|
wantSize := bucketSize
|
||||||
if !lastInBucketIsResponding && !newNodeIsResponding {
|
if !lastInBucketIsResponding && !newNodeIsResponding {
|
||||||
wantSize--
|
wantSize--
|
||||||
}
|
}
|
||||||
if l := len(tab.bucket(pingSender.ID()).entries); l != wantSize {
|
bucket := tab.bucket(replacementNode.ID())
|
||||||
t.Errorf("wrong bucket size after bond: got %d, want %d", l, wantSize)
|
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 {
|
if ok := contains(bucket.entries, last.ID()); ok != lastInBucketIsResponding {
|
||||||
t.Errorf("last entry found: %t, want: %t", found, lastInBucketIsResponding)
|
t.Errorf("revalidated node found: %t, want: %t", ok, lastInBucketIsResponding)
|
||||||
}
|
}
|
||||||
wantNewEntry := newNodeIsResponding && !lastInBucketIsResponding
|
wantNewEntry := newNodeIsResponding && !lastInBucketIsResponding
|
||||||
if found := contains(tab.bucket(pingSender.ID()).entries, pingSender.ID()); found != wantNewEntry {
|
if ok := contains(bucket.entries, replacementNode.ID()); ok != wantNewEntry {
|
||||||
t.Errorf("new entry found: %t, want: %t", found, wantNewEntry)
|
t.Errorf("replacement node found: %t, want: %t", ok, wantNewEntry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBucket_bumpNoDuplicates(t *testing.T) {
|
// waitForRevalidationPing waits until a PING message is sent to a node with the given id.
|
||||||
t.Parallel()
|
func waitForRevalidationPing(t *testing.T, transport *pingRecorder, tab *Table, id enode.ID) *enode.Node {
|
||||||
cfg := &quick.Config{
|
t.Helper()
|
||||||
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)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
prop := func(nodes []*node, bumps []int) (ok bool) {
|
simclock := tab.cfg.Clock.(*mclock.Simulated)
|
||||||
tab, db := newTestTable(newPingRecorder())
|
maxAttempts := tab.len() * 8
|
||||||
defer db.Close()
|
for i := 0; i < maxAttempts; i++ {
|
||||||
defer tab.close()
|
simclock.Run(tab.cfg.PingInterval)
|
||||||
|
p := transport.waitPing(2 * time.Second)
|
||||||
b := &bucket{entries: make([]*node, len(nodes))}
|
if p == nil {
|
||||||
copy(b.entries, nodes)
|
t.Fatal("Table did not send revalidation ping")
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
return false
|
if id == (enode.ID{}) || p.ID() == id {
|
||||||
|
return p
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
checkIPLimitInvariant(t, tab)
|
t.Fatalf("Table did not ping node %v (%d attempts)", id, maxAttempts)
|
||||||
return true
|
return nil
|
||||||
}
|
|
||||||
if err := quick.Check(prop, cfg); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This checks that the table-wide IP limit is applied correctly.
|
// This checks that the table-wide IP limit is applied correctly.
|
||||||
func TestTable_IPLimit(t *testing.T) {
|
func TestTable_IPLimit(t *testing.T) {
|
||||||
transport := newPingRecorder()
|
transport := newPingRecorder()
|
||||||
tab, db := newTestTable(transport)
|
tab, db := newTestTable(transport, Config{})
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
defer tab.close()
|
defer tab.close()
|
||||||
|
|
||||||
for i := 0; i < tableIPLimit+1; i++ {
|
for i := 0; i < tableIPLimit+1; i++ {
|
||||||
n := nodeAtDistance(tab.self().ID(), i, net.IP{172, 0, 1, byte(i)})
|
n := nodeAtDistance(tab.self().ID(), i, net.IP{172, 0, 1, byte(i)})
|
||||||
tab.addSeenNode(n)
|
tab.addFoundNode(n)
|
||||||
}
|
}
|
||||||
if tab.len() > tableIPLimit {
|
if tab.len() > tableIPLimit {
|
||||||
t.Errorf("too many nodes in table")
|
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.
|
// This checks that the per-bucket IP limit is applied correctly.
|
||||||
func TestTable_BucketIPLimit(t *testing.T) {
|
func TestTable_BucketIPLimit(t *testing.T) {
|
||||||
transport := newPingRecorder()
|
transport := newPingRecorder()
|
||||||
tab, db := newTestTable(transport)
|
tab, db := newTestTable(transport, Config{})
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
defer tab.close()
|
defer tab.close()
|
||||||
|
|
||||||
d := 3
|
d := 3
|
||||||
for i := 0; i < bucketIPLimit+1; i++ {
|
for i := 0; i < bucketIPLimit+1; i++ {
|
||||||
n := nodeAtDistance(tab.self().ID(), d, net.IP{172, 0, 1, byte(i)})
|
n := nodeAtDistance(tab.self().ID(), d, net.IP{172, 0, 1, byte(i)})
|
||||||
tab.addSeenNode(n)
|
tab.addFoundNode(n)
|
||||||
}
|
}
|
||||||
if tab.len() > bucketIPLimit {
|
if tab.len() > bucketIPLimit {
|
||||||
t.Errorf("too many nodes in table")
|
t.Errorf("too many nodes in table")
|
||||||
|
|
@ -196,7 +201,7 @@ func TestTable_findnodeByID(t *testing.T) {
|
||||||
test := func(test *closeTest) bool {
|
test := func(test *closeTest) bool {
|
||||||
// for any node table, Target and N
|
// for any node table, Target and N
|
||||||
transport := newPingRecorder()
|
transport := newPingRecorder()
|
||||||
tab, db := newTestTable(transport)
|
tab, db := newTestTable(transport, Config{})
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
defer tab.close()
|
defer tab.close()
|
||||||
fillTable(tab, test.All, true)
|
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) {
|
func TestTable_addVerifiedNode(t *testing.T) {
|
||||||
tab, db := newTestTable(newPingRecorder())
|
tab, db := newTestTable(newPingRecorder(), Config{})
|
||||||
<-tab.initDone
|
<-tab.initDone
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
defer tab.close()
|
defer tab.close()
|
||||||
|
|
@ -279,31 +284,32 @@ func TestTable_addVerifiedNode(t *testing.T) {
|
||||||
// Insert two nodes.
|
// Insert two nodes.
|
||||||
n1 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 1})
|
n1 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 1})
|
||||||
n2 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 2})
|
n2 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 2})
|
||||||
tab.addSeenNode(n1)
|
tab.addFoundNode(n1)
|
||||||
tab.addSeenNode(n2)
|
tab.addFoundNode(n2)
|
||||||
|
bucket := tab.bucket(n1.ID())
|
||||||
|
|
||||||
// Verify bucket content:
|
// Verify bucket content:
|
||||||
bcontent := []*node{n1, n2}
|
bcontent := []*node{n1, n2}
|
||||||
if !reflect.DeepEqual(tab.bucket(n1.ID()).entries, bcontent) {
|
if !reflect.DeepEqual(unwrapNodes(bucket.entries), unwrapNodes(bcontent)) {
|
||||||
t.Fatalf("wrong bucket content: %v", tab.bucket(n1.ID()).entries)
|
t.Fatalf("wrong bucket content: %v", bucket.entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a changed version of n2.
|
// Add a changed version of n2.
|
||||||
newrec := n2.Record()
|
newrec := n2.Record()
|
||||||
newrec.Set(enr.IP{99, 99, 99, 99})
|
newrec.Set(enr.IP{99, 99, 99, 99})
|
||||||
newn2 := wrapNode(enode.SignNull(newrec, n2.ID()))
|
newn2 := wrapNode(enode.SignNull(newrec, n2.ID()))
|
||||||
tab.addVerifiedNode(newn2)
|
tab.addInboundNode(newn2)
|
||||||
|
|
||||||
// Check that bucket is updated correctly.
|
// Check that bucket is updated correctly.
|
||||||
newBcontent := []*node{newn2, n1}
|
newBcontent := []*node{n1, newn2}
|
||||||
if !reflect.DeepEqual(tab.bucket(n1.ID()).entries, newBcontent) {
|
if !reflect.DeepEqual(unwrapNodes(bucket.entries), unwrapNodes(newBcontent)) {
|
||||||
t.Fatalf("wrong bucket content after update: %v", tab.bucket(n1.ID()).entries)
|
t.Fatalf("wrong bucket content after update: %v", bucket.entries)
|
||||||
}
|
}
|
||||||
checkIPLimitInvariant(t, tab)
|
checkIPLimitInvariant(t, tab)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTable_addSeenNode(t *testing.T) {
|
func TestTable_addSeenNode(t *testing.T) {
|
||||||
tab, db := newTestTable(newPingRecorder())
|
tab, db := newTestTable(newPingRecorder(), Config{})
|
||||||
<-tab.initDone
|
<-tab.initDone
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
defer tab.close()
|
defer tab.close()
|
||||||
|
|
@ -311,8 +317,8 @@ func TestTable_addSeenNode(t *testing.T) {
|
||||||
// Insert two nodes.
|
// Insert two nodes.
|
||||||
n1 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 1})
|
n1 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 1})
|
||||||
n2 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 2})
|
n2 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 2})
|
||||||
tab.addSeenNode(n1)
|
tab.addFoundNode(n1)
|
||||||
tab.addSeenNode(n2)
|
tab.addFoundNode(n2)
|
||||||
|
|
||||||
// Verify bucket content:
|
// Verify bucket content:
|
||||||
bcontent := []*node{n1, n2}
|
bcontent := []*node{n1, n2}
|
||||||
|
|
@ -324,7 +330,7 @@ func TestTable_addSeenNode(t *testing.T) {
|
||||||
newrec := n2.Record()
|
newrec := n2.Record()
|
||||||
newrec.Set(enr.IP{99, 99, 99, 99})
|
newrec.Set(enr.IP{99, 99, 99, 99})
|
||||||
newn2 := wrapNode(enode.SignNull(newrec, n2.ID()))
|
newn2 := wrapNode(enode.SignNull(newrec, n2.ID()))
|
||||||
tab.addSeenNode(newn2)
|
tab.addFoundNode(newn2)
|
||||||
|
|
||||||
// Check that bucket content is unchanged.
|
// Check that bucket content is unchanged.
|
||||||
if !reflect.DeepEqual(tab.bucket(n1.ID()).entries, bcontent) {
|
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.
|
// announces a new sequence number, the new record should be pulled.
|
||||||
func TestTable_revalidateSyncRecord(t *testing.T) {
|
func TestTable_revalidateSyncRecord(t *testing.T) {
|
||||||
transport := newPingRecorder()
|
transport := newPingRecorder()
|
||||||
tab, db := newTestTable(transport)
|
tab, db := newTestTable(transport, Config{
|
||||||
|
Clock: new(mclock.Simulated),
|
||||||
|
Log: testlog.Logger(t, log.LevelTrace),
|
||||||
|
})
|
||||||
<-tab.initDone
|
<-tab.initDone
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
defer tab.close()
|
defer tab.close()
|
||||||
|
|
@ -347,14 +356,18 @@ func TestTable_revalidateSyncRecord(t *testing.T) {
|
||||||
r.Set(enr.IP(net.IP{127, 0, 0, 1}))
|
r.Set(enr.IP(net.IP{127, 0, 0, 1}))
|
||||||
id := enode.ID{1}
|
id := enode.ID{1}
|
||||||
n1 := wrapNode(enode.SignNull(&r, id))
|
n1 := wrapNode(enode.SignNull(&r, id))
|
||||||
tab.addSeenNode(n1)
|
tab.addFoundNode(n1)
|
||||||
|
|
||||||
// Update the node record.
|
// Update the node record.
|
||||||
r.Set(enr.WithEntry("foo", "bar"))
|
r.Set(enr.WithEntry("foo", "bar"))
|
||||||
n2 := enode.SignNull(&r, id)
|
n2 := enode.SignNull(&r, id)
|
||||||
transport.updateRecord(n2)
|
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 able.
|
||||||
|
waitForRevalidationPing(t, transport, tab, n2.ID())
|
||||||
|
waitForRevalidationPing(t, transport, tab, n2.ID())
|
||||||
|
|
||||||
intable := tab.getNode(id)
|
intable := tab.getNode(id)
|
||||||
if !reflect.DeepEqual(intable, n2) {
|
if !reflect.DeepEqual(intable, n2) {
|
||||||
t.Fatalf("table contains old record with seq %d, want seq %d", intable.Seq(), n2.Seq())
|
t.Fatalf("table contains old record with seq %d, want seq %d", intable.Seq(), n2.Seq())
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"slices"
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||||
|
|
@ -40,8 +42,7 @@ func init() {
|
||||||
nullNode = enode.SignNull(&r, enode.ID{})
|
nullNode = enode.SignNull(&r, enode.ID{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestTable(t transport) (*Table, *enode.DB) {
|
func newTestTable(t transport, cfg Config) (*Table, *enode.DB) {
|
||||||
cfg := Config{}
|
|
||||||
db, _ := enode.OpenDB("")
|
db, _ := enode.OpenDB("")
|
||||||
tab, _ := newTable(t, db, cfg)
|
tab, _ := newTable(t, db, cfg)
|
||||||
go tab.loop()
|
go tab.loop()
|
||||||
|
|
@ -98,11 +99,14 @@ func intIP(i int) net.IP {
|
||||||
}
|
}
|
||||||
|
|
||||||
// fillBucket inserts nodes into the given bucket until it is full.
|
// fillBucket inserts nodes into the given bucket until it is full.
|
||||||
func fillBucket(tab *Table, n *node) (last *node) {
|
func fillBucket(tab *Table, id enode.ID) (last *node) {
|
||||||
ld := enode.LogDist(tab.self().ID(), n.ID())
|
ld := enode.LogDist(tab.self().ID(), id)
|
||||||
b := tab.bucket(n.ID())
|
b := tab.bucket(id)
|
||||||
for len(b.entries) < bucketSize {
|
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]
|
return b.entries[bucketSize-1]
|
||||||
}
|
}
|
||||||
|
|
@ -113,15 +117,18 @@ func fillTable(tab *Table, nodes []*node, setLive bool) {
|
||||||
for _, n := range nodes {
|
for _, n := range nodes {
|
||||||
if setLive {
|
if setLive {
|
||||||
n.livenessChecks = 1
|
n.livenessChecks = 1
|
||||||
|
n.isValidatedLive = true
|
||||||
}
|
}
|
||||||
tab.addSeenNode(n)
|
tab.addFoundNode(n)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type pingRecorder struct {
|
type pingRecorder struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
dead, pinged map[enode.ID]bool
|
cond *sync.Cond
|
||||||
|
dead map[enode.ID]bool
|
||||||
records map[enode.ID]*enode.Node
|
records map[enode.ID]*enode.Node
|
||||||
|
pinged []*enode.Node
|
||||||
n *enode.Node
|
n *enode.Node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,12 +137,13 @@ func newPingRecorder() *pingRecorder {
|
||||||
r.Set(enr.IP{0, 0, 0, 0})
|
r.Set(enr.IP{0, 0, 0, 0})
|
||||||
n := enode.SignNull(&r, enode.ID{})
|
n := enode.SignNull(&r, enode.ID{})
|
||||||
|
|
||||||
return &pingRecorder{
|
t := &pingRecorder{
|
||||||
dead: make(map[enode.ID]bool),
|
dead: make(map[enode.ID]bool),
|
||||||
pinged: make(map[enode.ID]bool),
|
|
||||||
records: make(map[enode.ID]*enode.Node),
|
records: make(map[enode.ID]*enode.Node),
|
||||||
n: n,
|
n: n,
|
||||||
}
|
}
|
||||||
|
t.cond = sync.NewCond(&t.mu)
|
||||||
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateRecord updates a node record. Future calls to ping and
|
// 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) lookupSelf() []*enode.Node { return nil }
|
||||||
func (t *pingRecorder) lookupRandom() []*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.
|
// ping simulates a ping request.
|
||||||
func (t *pingRecorder) ping(n *enode.Node) (seq uint64, err error) {
|
func (t *pingRecorder) ping(n *enode.Node) (seq uint64, err error) {
|
||||||
t.mu.Lock()
|
t.mu.Lock()
|
||||||
defer t.mu.Unlock()
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
t.pinged[n.ID()] = true
|
t.pinged = append(t.pinged, n)
|
||||||
|
t.cond.Broadcast()
|
||||||
|
|
||||||
if t.dead[n.ID()] {
|
if t.dead[n.ID()] {
|
||||||
return 0, errTimeout
|
return 0, errTimeout
|
||||||
}
|
}
|
||||||
|
|
@ -256,3 +292,57 @@ func hexEncPubkey(h string) (ret encPubkey) {
|
||||||
copy(ret[:], b)
|
copy(ret[:], b)
|
||||||
return ret
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ func ListenV4(c UDPConn, ln *enode.LocalNode, cfg Config) (*UDPv4, error) {
|
||||||
log: cfg.Log,
|
log: cfg.Log,
|
||||||
}
|
}
|
||||||
|
|
||||||
tab, err := newMeteredTable(t, ln.Database(), cfg)
|
tab, err := newTable(t, ln.Database(), cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -375,6 +375,10 @@ func (t *UDPv4) RequestENR(n *enode.Node) (*enode.Node, error) {
|
||||||
return respN, nil
|
return respN, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *UDPv4) TableBuckets() [][]BucketNode {
|
||||||
|
return t.tab.Nodes()
|
||||||
|
}
|
||||||
|
|
||||||
// pending adds a reply matcher to the pending reply queue.
|
// pending adds a reply matcher to the pending reply queue.
|
||||||
// see the documentation of type replyMatcher for a detailed explanation.
|
// 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 {
|
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))
|
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 {
|
if time.Since(t.db.LastPongReceived(n.ID(), from.IP)) > bondExpiration {
|
||||||
t.sendPing(fromID, from, func() {
|
t.sendPing(fromID, from, func() {
|
||||||
t.tab.addVerifiedNode(n)
|
t.tab.addInboundNode(n)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
t.tab.addVerifiedNode(n)
|
t.tab.addInboundNode(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update node database and endpoint predictor.
|
// Update node database and endpoint predictor.
|
||||||
|
|
|
||||||
|
|
@ -264,7 +264,7 @@ func TestUDPv4_findnode(t *testing.T) {
|
||||||
n := wrapNode(enode.NewV4(&key.PublicKey, ip, 0, 2000))
|
n := wrapNode(enode.NewV4(&key.PublicKey, ip, 0, 2000))
|
||||||
// Ensure half of table content isn't verified live yet.
|
// Ensure half of table content isn't verified live yet.
|
||||||
if i > numCandidates/2 {
|
if i > numCandidates/2 {
|
||||||
n.livenessChecks = 1
|
n.isValidatedLive = true
|
||||||
live[n.ID()] = true
|
live[n.ID()] = true
|
||||||
}
|
}
|
||||||
nodes.push(n, numCandidates)
|
nodes.push(n, numCandidates)
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,7 @@ func newUDPv5(conn UDPConn, ln *enode.LocalNode, cfg Config) (*UDPv5, error) {
|
||||||
cancelCloseCtx: cancelCloseCtx,
|
cancelCloseCtx: cancelCloseCtx,
|
||||||
}
|
}
|
||||||
t.talk = newTalkSystem(t)
|
t.talk = newTalkSystem(t)
|
||||||
tab, err := newMeteredTable(t, t.db, cfg)
|
tab, err := newTable(t, t.db, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -699,7 +699,7 @@ func (t *UDPv5) handlePacket(rawpacket []byte, fromAddr *net.UDPAddr) error {
|
||||||
}
|
}
|
||||||
if fromNode != nil {
|
if fromNode != nil {
|
||||||
// Handshake succeeded, add to table.
|
// Handshake succeeded, add to table.
|
||||||
t.tab.addSeenNode(wrapNode(fromNode))
|
t.tab.addInboundNode(wrapNode(fromNode))
|
||||||
}
|
}
|
||||||
if packet.Kind() != v5wire.WhoareyouPacket {
|
if packet.Kind() != v5wire.WhoareyouPacket {
|
||||||
// WHOAREYOU logged separately to report errors.
|
// WHOAREYOU logged separately to report errors.
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@ func TestUDPv5_unknownPacket(t *testing.T) {
|
||||||
|
|
||||||
// Make node known.
|
// Make node known.
|
||||||
n := test.getNode(test.remotekey, test.remoteaddr).Node()
|
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.packetIn(&v5wire.Unknown{Nonce: nonce})
|
||||||
test.waitPacketOut(func(p *v5wire.Whoareyou, addr *net.UDPAddr, _ v5wire.Nonce) {
|
test.waitPacketOut(func(p *v5wire.Whoareyou, addr *net.UDPAddr, _ v5wire.Nonce) {
|
||||||
|
|
|
||||||
|
|
@ -157,5 +157,5 @@ func SignNull(r *enr.Record, id ID) *Node {
|
||||||
if err := r.SetSig(NullID{}, []byte{}); err != nil {
|
if err := r.SetSig(NullID{}, []byte{}); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
return &Node{r: *r, id: id}
|
return newNodeWithID(r, id)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/bits"
|
"math/bits"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
"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 {
|
type Node struct {
|
||||||
r enr.Record
|
r enr.Record
|
||||||
id ID
|
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
|
// 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 {
|
if err := r.VerifySignature(validSchemes); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
node := &Node{r: *r}
|
var id ID
|
||||||
if n := copy(node.id[:], validSchemes.NodeAddr(&node.r)); n != len(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 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.
|
// 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()
|
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.
|
// Load retrieves an entry from the underlying record.
|
||||||
func (n *Node) Load(k enr.Entry) error {
|
func (n *Node) Load(k enr.Entry) error {
|
||||||
return n.r.Load(k)
|
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 {
|
func (n *Node) IP() net.IP {
|
||||||
var (
|
return net.IP(n.ip.AsSlice())
|
||||||
ip4 enr.IPv4
|
|
||||||
ip6 enr.IPv6
|
|
||||||
)
|
|
||||||
if n.Load(&ip4) == nil {
|
|
||||||
return net.IP(ip4)
|
|
||||||
}
|
}
|
||||||
if n.Load(&ip6) == nil {
|
|
||||||
return net.IP(ip6)
|
// IPAddr returns the IP address of the node.
|
||||||
}
|
func (n *Node) IPAddr() netip.Addr {
|
||||||
return nil
|
return n.ip
|
||||||
}
|
}
|
||||||
|
|
||||||
// UDP returns the UDP port of the node.
|
// UDP returns the UDP port of the node.
|
||||||
func (n *Node) UDP() int {
|
func (n *Node) UDP() int {
|
||||||
var port enr.UDP
|
return int(n.udp)
|
||||||
n.Load(&port)
|
|
||||||
return int(port)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TCP returns the TCP port of the node.
|
// TCP returns the TCP port of the node.
|
||||||
func (n *Node) TCP() int {
|
func (n *Node) TCP() int {
|
||||||
var port enr.TCP
|
return int(n.tcp)
|
||||||
n.Load(&port)
|
}
|
||||||
return int(port)
|
|
||||||
|
// 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.
|
// 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.
|
// ValidateComplete checks whether n has a valid IP and UDP port.
|
||||||
// Deprecated: don't use this method.
|
// Deprecated: don't use this method.
|
||||||
func (n *Node) ValidateComplete() error {
|
func (n *Node) ValidateComplete() error {
|
||||||
if n.Incomplete() {
|
if !n.ip.IsValid() {
|
||||||
return errors.New("missing IP address")
|
return errors.New("missing IP address")
|
||||||
}
|
}
|
||||||
if n.UDP() == 0 {
|
if n.ip.IsMulticast() || n.ip.IsUnspecified() {
|
||||||
return errors.New("missing UDP port")
|
|
||||||
}
|
|
||||||
ip := n.IP()
|
|
||||||
if ip.IsMulticast() || ip.IsUnspecified() {
|
|
||||||
return errors.New("invalid IP (multicast/unspecified)")
|
return errors.New("invalid IP (multicast/unspecified)")
|
||||||
}
|
}
|
||||||
|
if n.udp == 0 {
|
||||||
|
return errors.New("missing UDP port")
|
||||||
|
}
|
||||||
// Validate the node key (on curve, etc.).
|
// Validate the node key (on curve, etc.).
|
||||||
var key Secp256k1
|
var key Secp256k1
|
||||||
return n.Load(&key)
|
return n.Load(&key)
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
"net/netip"
|
||||||
"testing"
|
"testing"
|
||||||
"testing/quick"
|
"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) {
|
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}
|
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")
|
id1 := HexID("0x00000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc")
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/p2p/enr"
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
"github.com/ethereum/go-ethereum/rlp"
|
||||||
"github.com/syndtr/goleveldb/leveldb"
|
"github.com/syndtr/goleveldb/leveldb"
|
||||||
"github.com/syndtr/goleveldb/leveldb/errors"
|
"github.com/syndtr/goleveldb/leveldb/errors"
|
||||||
|
|
@ -242,13 +243,14 @@ func (db *DB) Node(id ID) *Node {
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustDecodeNode(id, data []byte) *Node {
|
func mustDecodeNode(id, data []byte) *Node {
|
||||||
node := new(Node)
|
var r enr.Record
|
||||||
if err := rlp.DecodeBytes(data, &node.r); err != nil {
|
if err := rlp.DecodeBytes(data, &r); err != nil {
|
||||||
panic(fmt.Errorf("p2p/enode: can't decode node %x in DB: %v", id, err))
|
panic(fmt.Errorf("p2p/enode: can't decode node %x in DB: %v", id, err))
|
||||||
}
|
}
|
||||||
// Restore node id cache.
|
if len(id) != len(ID{}) {
|
||||||
copy(node.id[:], id)
|
panic(fmt.Errorf("invalid id length %d", len(id)))
|
||||||
return node
|
}
|
||||||
|
return newNodeWithID(&r, ID(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateNode inserts - potentially overwriting - a node into the peer database.
|
// UpdateNode inserts - potentially overwriting - a node into the peer database.
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,7 @@ func (n *Node) URLv4() string {
|
||||||
nodeid = fmt.Sprintf("%s.%x", scheme, n.id[:])
|
nodeid = fmt.Sprintf("%s.%x", scheme, n.id[:])
|
||||||
}
|
}
|
||||||
u := url.URL{Scheme: "enode"}
|
u := url.URL{Scheme: "enode"}
|
||||||
if n.Incomplete() {
|
if !n.ip.IsValid() {
|
||||||
u.Host = nodeid
|
u.Host = nodeid
|
||||||
} else {
|
} else {
|
||||||
addr := net.TCPAddr{IP: n.IP(), Port: n.TCP()}
|
addr := net.TCPAddr{IP: n.IP(), Port: n.TCP()}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
"github.com/ethereum/go-ethereum/rlp"
|
||||||
)
|
)
|
||||||
|
|
@ -167,6 +168,60 @@ func (v *IPv6) DecodeRLP(s *rlp.Stream) error {
|
||||||
return nil
|
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.
|
// KeyError is an error related to a key.
|
||||||
type KeyError struct {
|
type KeyError struct {
|
||||||
Key string
|
Key string
|
||||||
|
|
|
||||||
|
|
@ -190,8 +190,8 @@ type Server struct {
|
||||||
|
|
||||||
nodedb *enode.DB
|
nodedb *enode.DB
|
||||||
localnode *enode.LocalNode
|
localnode *enode.LocalNode
|
||||||
ntab *discover.UDPv4
|
discv4 *discover.UDPv4
|
||||||
DiscV5 *discover.UDPv5
|
discv5 *discover.UDPv5
|
||||||
discmix *enode.FairMix
|
discmix *enode.FairMix
|
||||||
dialsched *dialScheduler
|
dialsched *dialScheduler
|
||||||
|
|
||||||
|
|
@ -400,6 +400,16 @@ func (srv *Server) Self() *enode.Node {
|
||||||
return ln.Node()
|
return ln.Node()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DiscoveryV4 returns the discovery v4 instance, if configured.
|
||||||
|
func (srv *Server) DiscoveryV4() *discover.UDPv4 {
|
||||||
|
return srv.discv4
|
||||||
|
}
|
||||||
|
|
||||||
|
// DiscoveryV4 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.
|
// Stop terminates the server and all active peer connections.
|
||||||
// It blocks until all active connections have been closed.
|
// It blocks until all active connections have been closed.
|
||||||
func (srv *Server) Stop() {
|
func (srv *Server) Stop() {
|
||||||
|
|
@ -547,13 +557,13 @@ func (srv *Server) setupDiscovery() error {
|
||||||
)
|
)
|
||||||
// If both versions of discovery are running, setup a shared
|
// If both versions of discovery are running, setup a shared
|
||||||
// connection, so v5 can read unhandled messages from v4.
|
// 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)
|
unhandled = make(chan discover.ReadPacket, 100)
|
||||||
sconn = &sharedUDPConn{conn, unhandled}
|
sconn = &sharedUDPConn{conn, unhandled}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start discovery services.
|
// Start discovery services.
|
||||||
if srv.DiscoveryV4 {
|
if srv.Config.DiscoveryV4 {
|
||||||
cfg := discover.Config{
|
cfg := discover.Config{
|
||||||
PrivateKey: srv.PrivateKey,
|
PrivateKey: srv.PrivateKey,
|
||||||
NetRestrict: srv.NetRestrict,
|
NetRestrict: srv.NetRestrict,
|
||||||
|
|
@ -565,17 +575,17 @@ func (srv *Server) setupDiscovery() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
srv.ntab = ntab
|
srv.discv4 = ntab
|
||||||
srv.discmix.AddSource(ntab.RandomNodes())
|
srv.discmix.AddSource(ntab.RandomNodes())
|
||||||
}
|
}
|
||||||
if srv.DiscoveryV5 {
|
if srv.Config.DiscoveryV5 {
|
||||||
cfg := discover.Config{
|
cfg := discover.Config{
|
||||||
PrivateKey: srv.PrivateKey,
|
PrivateKey: srv.PrivateKey,
|
||||||
NetRestrict: srv.NetRestrict,
|
NetRestrict: srv.NetRestrict,
|
||||||
Bootnodes: srv.BootstrapNodesV5,
|
Bootnodes: srv.BootstrapNodesV5,
|
||||||
Log: srv.log,
|
Log: srv.log,
|
||||||
}
|
}
|
||||||
srv.DiscV5, err = discover.ListenV5(sconn, srv.localnode, cfg)
|
srv.discv5, err = discover.ListenV5(sconn, srv.localnode, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -602,8 +612,8 @@ func (srv *Server) setupDialScheduler() {
|
||||||
dialer: srv.Dialer,
|
dialer: srv.Dialer,
|
||||||
clock: srv.clock,
|
clock: srv.clock,
|
||||||
}
|
}
|
||||||
if srv.ntab != nil {
|
if srv.discv4 != nil {
|
||||||
config.resolver = srv.ntab
|
config.resolver = srv.discv4
|
||||||
}
|
}
|
||||||
if config.dialer == nil {
|
if config.dialer == nil {
|
||||||
config.dialer = tcpDialer{&net.Dialer{Timeout: defaultDialTimeout}}
|
config.dialer = tcpDialer{&net.Dialer{Timeout: defaultDialTimeout}}
|
||||||
|
|
@ -799,11 +809,11 @@ running:
|
||||||
srv.log.Trace("P2P networking is spinning down")
|
srv.log.Trace("P2P networking is spinning down")
|
||||||
|
|
||||||
// Terminate discovery. If there is a running lookup it will terminate soon.
|
// Terminate discovery. If there is a running lookup it will terminate soon.
|
||||||
if srv.ntab != nil {
|
if srv.discv4 != nil {
|
||||||
srv.ntab.Close()
|
srv.discv4.Close()
|
||||||
}
|
}
|
||||||
if srv.DiscV5 != nil {
|
if srv.discv5 != nil {
|
||||||
srv.DiscV5.Close()
|
srv.discv5.Close()
|
||||||
}
|
}
|
||||||
// Disconnect all peers.
|
// Disconnect all peers.
|
||||||
for _, p := range peers {
|
for _, p := range peers {
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,10 @@ var (
|
||||||
|
|
||||||
// BeaconRootsAddress is the address where historical beacon roots are stored as per EIP-4788
|
// BeaconRootsAddress is the address where historical beacon roots are stored as per EIP-4788
|
||||||
BeaconRootsAddress = common.HexToAddress("0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02")
|
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 is where the system-transaction is sent from as per EIP-4788
|
||||||
SystemAddress = common.HexToAddress("0xfffffffffffffffffffffffffffffffffffffffe")
|
SystemAddress = common.HexToAddress("0xfffffffffffffffffffffffffffffffffffffffe")
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,7 @@ var Forks = map[string]*params.ChainConfig{
|
||||||
LondonBlock: big.NewInt(0),
|
LondonBlock: big.NewInt(0),
|
||||||
ArrowGlacierBlock: big.NewInt(0),
|
ArrowGlacierBlock: big.NewInt(0),
|
||||||
},
|
},
|
||||||
"ArrowGlacierToMergeAtDiffC0000": {
|
"ArrowGlacierToParisAtDiffC0000": {
|
||||||
ChainID: big.NewInt(1),
|
ChainID: big.NewInt(1),
|
||||||
HomesteadBlock: big.NewInt(0),
|
HomesteadBlock: big.NewInt(0),
|
||||||
EIP150Block: big.NewInt(0),
|
EIP150Block: big.NewInt(0),
|
||||||
|
|
@ -246,6 +246,23 @@ var Forks = map[string]*params.ChainConfig{
|
||||||
ArrowGlacierBlock: big.NewInt(0),
|
ArrowGlacierBlock: big.NewInt(0),
|
||||||
GrayGlacierBlock: big.NewInt(0),
|
GrayGlacierBlock: big.NewInt(0),
|
||||||
},
|
},
|
||||||
|
"Paris": {
|
||||||
|
ChainID: big.NewInt(1),
|
||||||
|
HomesteadBlock: big.NewInt(0),
|
||||||
|
EIP150Block: big.NewInt(0),
|
||||||
|
EIP155Block: big.NewInt(0),
|
||||||
|
EIP158Block: big.NewInt(0),
|
||||||
|
ByzantiumBlock: big.NewInt(0),
|
||||||
|
ConstantinopleBlock: big.NewInt(0),
|
||||||
|
PetersburgBlock: big.NewInt(0),
|
||||||
|
IstanbulBlock: big.NewInt(0),
|
||||||
|
MuirGlacierBlock: big.NewInt(0),
|
||||||
|
BerlinBlock: big.NewInt(0),
|
||||||
|
LondonBlock: big.NewInt(0),
|
||||||
|
ArrowGlacierBlock: big.NewInt(0),
|
||||||
|
MergeNetsplitBlock: big.NewInt(0),
|
||||||
|
TerminalTotalDifficulty: big.NewInt(0),
|
||||||
|
},
|
||||||
"Merge": {
|
"Merge": {
|
||||||
ChainID: big.NewInt(1),
|
ChainID: big.NewInt(1),
|
||||||
HomesteadBlock: big.NewInt(0),
|
HomesteadBlock: big.NewInt(0),
|
||||||
|
|
@ -281,7 +298,7 @@ var Forks = map[string]*params.ChainConfig{
|
||||||
TerminalTotalDifficulty: big.NewInt(0),
|
TerminalTotalDifficulty: big.NewInt(0),
|
||||||
ShanghaiTime: u64(0),
|
ShanghaiTime: u64(0),
|
||||||
},
|
},
|
||||||
"MergeToShanghaiAtTime15k": {
|
"ParisToShanghaiAtTime15k": {
|
||||||
ChainID: big.NewInt(1),
|
ChainID: big.NewInt(1),
|
||||||
HomesteadBlock: big.NewInt(0),
|
HomesteadBlock: big.NewInt(0),
|
||||||
EIP150Block: big.NewInt(0),
|
EIP150Block: big.NewInt(0),
|
||||||
|
|
|
||||||
|
|
@ -54,14 +54,6 @@ func initMatcher(st *testMatcher) {
|
||||||
// Uses 1GB RAM per tested fork
|
// Uses 1GB RAM per tested fork
|
||||||
st.skipLoad(`^stStaticCall/static_Call1MB`)
|
st.skipLoad(`^stStaticCall/static_Call1MB`)
|
||||||
|
|
||||||
// These tests fail as of https://github.com/ethereum/go-ethereum/pull/28666, since we
|
|
||||||
// no longer delete "leftover storage" when deploying a contract.
|
|
||||||
st.skipLoad(`^stSStoreTest/InitCollision\.json`)
|
|
||||||
st.skipLoad(`^stRevertTest/RevertInCreateInInit\.json`)
|
|
||||||
st.skipLoad(`^stExtCodeHash/dynamicAccountOverwriteEmpty\.json`)
|
|
||||||
st.skipLoad(`^stCreate2/create2collisionStorage\.json`)
|
|
||||||
st.skipLoad(`^stCreate2/RevertInCreateInInitCreate2\.json`)
|
|
||||||
|
|
||||||
// Broken tests:
|
// Broken tests:
|
||||||
// EOF is not part of cancun
|
// EOF is not part of cancun
|
||||||
st.skipLoad(`^stEOF/`)
|
st.skipLoad(`^stEOF/`)
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit fa51c5c164f79140730ccb8fe26a46c3d3994338
|
Subproject commit faf33b471465d3c6cdc3d04fbd690895f78d33f2
|
||||||
40
trie/sync.go
40
trie/sync.go
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"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/common/prque"
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"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
|
// 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.
|
// deletion to the specific node or a node write for persisting retrieved node.
|
||||||
type nodeOp struct {
|
type nodeOp struct {
|
||||||
|
del bool // flag if op stands for a delete operation
|
||||||
owner common.Hash // identifier of the trie (empty for account trie)
|
owner common.Hash // identifier of the trie (empty for account trie)
|
||||||
path []byte // path from the root to the specified node.
|
path []byte // path from the root to the specified node.
|
||||||
blob []byte // the content of the node (nil for deletion)
|
blob []byte // the content of the node (nil for deletion)
|
||||||
hash common.Hash // hash of the node content (empty for node deletion)
|
hash common.Hash // hash of the node content (empty for node deletion)
|
||||||
}
|
}
|
||||||
|
|
||||||
// isDelete indicates if the operation is a database deletion.
|
// valid checks whether the node operation is valid.
|
||||||
func (op *nodeOp) isDelete() bool {
|
func (op *nodeOp) valid() bool {
|
||||||
return len(op.blob) == 0
|
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
|
// 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.size += common.HashLength + uint64(len(path))
|
||||||
}
|
}
|
||||||
batch.nodes = append(batch.nodes, nodeOp{
|
batch.nodes = append(batch.nodes, nodeOp{
|
||||||
|
del: true,
|
||||||
owner: owner,
|
owner: owner,
|
||||||
path: path,
|
path: path,
|
||||||
})
|
})
|
||||||
|
|
@ -428,7 +457,10 @@ func (s *Sync) Commit(dbw ethdb.Batch) error {
|
||||||
storage int
|
storage int
|
||||||
)
|
)
|
||||||
for _, op := range s.membatch.nodes {
|
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.
|
// node deletion is only supported in path mode.
|
||||||
if op.owner == (common.Hash{}) {
|
if op.owner == (common.Hash{}) {
|
||||||
rawdb.DeleteAccountTrieNode(dbw, op.path)
|
rawdb.DeleteAccountTrieNode(dbw, op.path)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue